From ab848312e2499e92eae5bfb17b29c386965cde2b Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Sat, 27 Nov 2021 22:20:37 +1030 Subject: [PATCH] Refactoring in preparation for Step stuff --- formats/RGT.gd | 190 ++++++++++ formats/SM.gd | 167 +++++++++ formats/SRT.gd | 73 ++++ formats/Test.gd | 34 ++ scenes/Menu.tscn | 2 +- scenes/StepGame.tscn | 82 ++-- scenes/StepMenu.tscn | 28 ++ scripts/FileHelpers.gd | 76 ++++ scripts/NotePainter.gd | 3 +- scripts/StepMenu.gd | 585 +++++++++++++++++++++++++++++ scripts/{Menu.gd => TouchMenu.gd} | 0 singletons/FileLoader.gd | 596 +++--------------------------- 12 files changed, 1227 insertions(+), 609 deletions(-) create mode 100644 formats/RGT.gd create mode 100644 formats/SM.gd create mode 100644 formats/SRT.gd create mode 100644 formats/Test.gd create mode 100644 scenes/StepMenu.tscn create mode 100644 scripts/FileHelpers.gd create mode 100644 scripts/StepMenu.gd rename scripts/{Menu.gd => TouchMenu.gd} (100%) diff --git a/formats/RGT.gd b/formats/RGT.gd new file mode 100644 index 0000000..f374492 --- /dev/null +++ b/formats/RGT.gd @@ -0,0 +1,190 @@ +extends Node +# 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_STAR, + 'e': Note.NOTE_SLIDE, + 'b': Note.NOTE_TAP, # Break + 'x': Note.NOTE_STAR # Break star +} + +const SLIDE_TYPES = { + '0': null, # Seems to be used for stars without slides attached + '1': Note.SlideType.CHORD, + '2': Note.SlideType.ARC_ACW, + '3': Note.SlideType.ARC_CW, + '4': Note.SlideType.COMPLEX, # Orbit around center ACW on the way + '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, # Go to center then orbit off to the side ACW + '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 + 'd': Note.SlideType.CHORD_TRIPLE, # Triple cone. Spreads out to the adjacent receptors of the target. + 'e': Note.SlideType.CHORD, # Not used in any of our charts + 'f': Note.SlideType.CHORD, # Not used in any of our charts +} +const SLIDE_IN_R := sin(PI/8) # Circle radius circumscribed by chords 0-3, 1-4, 2-5 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 chart_ids = [] + var lines = [[]] + # This loop will segment the lines as if the file were RGTM + 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 + chart_ids.append(line.lstrip('[').rstrip(']')) + lines.append([]) + elif !line.empty(): + lines[-1].push_back(line) + file.close() + print('Parsing chart: ', filename) + + match format: + Format.RGTS: + var metadata_and_notes = parse_rgts(lines[0]) + return metadata_and_notes + Format.RGTX: + var metadata_and_notes = parse_rgtx(lines[0]) + return metadata_and_notes + Format.RGTM: + lines.pop_front() # Anything before the first [header] is meaningless + var charts = {} + for i in len(lines): + charts[chart_ids[i]] = parse_rgts(lines[i]) + return charts + return format + + +static func parse_rgtx(lines: PoolStringArray): + return [] # To be implemented later + + +const beats_per_measure = 4.0 # TODO: Bit of an ugly hack, need to revisit this later +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 := [] + for i in Rules.COLS: + last_star.append(null) + + 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]) * beats_per_measure + var note_hits := [] + var note_nonhits := [] + for i in range(1, len(s)): + var n = s[i] + var column := int(n[0]) + var ntype = n[1] + n = n.substr(2) + + match ntype: + 't', 'b': # tap + note_hits.append(Note.NoteTap.new(time, column, ntype=='b')) + num_taps += 1 + 'h': # hold + var duration = float(n) * beats_per_measure + note_hits.append(Note.NoteHold.new(time, column, duration)) + num_holds += 1 + 's', 'x': # slide star + var star = Note.NoteStar.new(time, column, ntype=='z') + note_hits.append(star) + num_slides += 1 + 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) + 'e': # slide end + var slide_type = n[0] # numeric digit, left as str just in case + var slide_id = int(n.substr(1)) + 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() + if SLIDE_TYPES[slide_type] == Note.SlideType.COMPLEX: + var col_hit = slide_ids[slide_id].column + var RUV = GameTheme.RADIAL_UNIT_VECTORS + var RCA = GameTheme.RADIAL_COL_ANGLES + slide_ids[slide_id].values.curve2d.add_point(RUV[col_hit]) # Start col + match slide_type: + '4': # Orbit 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 + Note.curve2d_make_orbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], true) + '5': # CW of above + Note.curve2d_make_orbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], false) + '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': # Orbit off-center ACW + Note.curve2d_make_sideorbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], true) + 'a': # CW of above + Note.curve2d_make_sideorbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], false) + 'b': # V into column 2 places ACW + slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit-2, Rules.COLS)]) + 'c': # V into column 2 places CW + slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit+2, Rules.COLS)]) + slide_ids[slide_id].values.curve2d.add_point(RUV[column]) # End col + 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) + '_': + print_debug('Unknown note type: ', ntype) + + if len(note_hits) > 1: + for note in note_hits: # Set multihit on each one + note.double_hit = true + notes += note_hits + note_nonhits + metadata['num_taps'] = num_taps + metadata['num_holds'] = num_holds + metadata['num_slides'] = num_slides + return [metadata, notes] diff --git a/formats/SM.gd b/formats/SM.gd new file mode 100644 index 0000000..cfd782f --- /dev/null +++ b/formats/SM.gd @@ -0,0 +1,167 @@ +extends Node +# 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', +# '#LYRICSPATH': '', + '#CDTITLE': 'image_cd_title', + '#MUSIC': 'audio_filelist', + '#OFFSET': 'audio_offsets', + '#SAMPLESTART': 'audio_preview_times', + '#SAMPLELENGTH': 'audio_preview_times', +# '#SELECTABLE': '', + '#BPMS': 'bpm_values', +# '#STOPS': '', +# '#BGCHANGES': '', +# '#KEYSOUNDS': '', +} + + +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.NoteTap.new(time, col)) + '2': # Hold + ongoing_holds[col] = len(notes) + notes.append(Note.NoteHold.new(time, col, 0.0)) + num_holds += 1 + '4': # Roll + ongoing_holds[col] = len(notes) + notes.append(Note.NoteRoll.new(time, col, 0.0)) + num_rolls += 1 + '3': # End Hold/Roll + assert(ongoing_holds.has(col)) + notes[ongoing_holds[col]].set_time_release(time) + ongoing_holds.erase(col) + 'M': # Mine + num_mines += 1 + pass + metadata['num_notes'] = num_notes + metadata['num_taps'] = num_notes - num_jumps + 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 + metadata['notes'] = notes + return metadata + + +static func load_file(filename: String) -> Array: + # Output is [metadata, [[meta0, chart0], ..., [metaN, chartN]]] + # 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() + match file.open(filename, File.READ): + OK: + pass + var err: + print_debug('Error loading file: ', err) + return [] + 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() + var line : String = file.get_line() + 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] + elif len(tokens) >= 2: + metadata[tokens[0]] = tokens[1] + var charts = [] + + for i in range(1, len(lines)): + charts.append(load_chart(lines[i])) + + return [metadata, charts] diff --git a/formats/SRT.gd b/formats/SRT.gd new file mode 100644 index 0000000..aee2f31 --- /dev/null +++ b/formats/SRT.gd @@ -0,0 +1,73 @@ +extends Node +# A legacy format that is relatively easily parsed. Radial game mode. +const TAP_DURATION := 0.062500 +const ID_BREAK := 4 +const ID_HOLD := 2 +const ID_SLIDE_END := 128 +const ID3_SLIDE_CHORD := 0 # Straight line +const ID3_SLIDE_ARC_CW := 1 +const ID3_SLIDE_ARC_ACW := 2 + +static func load_file(filename): + var file = File.new() + var err = file.open(filename, File.READ) + if err != OK: + print(err) + return err + var metadata := {} + var num_taps := 0 + var num_holds := 0 + var num_slides := 0 + var notes := [] + var beats_per_measure := 4 + var length = file.get_len() + var slide_ids = {} + while (file.get_position() < (length-2)): + var noteline = file.get_csv_line() + var time_hit := (float(noteline[0]) + (float(noteline[1]))) * beats_per_measure + 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.NoteHold.new(time_hit, column, duration)) + num_holds += 1 + ID_BREAK: + notes.push_back(Note.NoteTap.new(time_hit, column, true)) + num_taps += 1 + ID_SLIDE_END: + # id2 is slide ID + if id2 in slide_ids: + slide_ids[id2].column_release = column + slide_ids[id2].update_slide_variables() + _: + if id2 == 0: + notes.push_back(Note.NoteTap.new(time_hit, column)) + num_taps += 1 + 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 + var slide_type = Note.SlideType.CHORD + match id3: + ID3_SLIDE_CHORD: + slide_type = Note.SlideType.CHORD + ID3_SLIDE_ARC_CW: + slide_type = Note.SlideType.ARC_CW + ID3_SLIDE_ARC_ACW: + slide_type = Note.SlideType.ARC_ACW + _: + print('Unknown slide type: ', id3) + var note = Note.NoteStar.new(time_hit, column) + num_slides += 1 + 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 + metadata['num_taps'] = num_taps + metadata['num_holds'] = num_holds + metadata['num_slides'] = num_slides + return [metadata, notes] diff --git a/formats/Test.gd b/formats/Test.gd new file mode 100644 index 0000000..e606304 --- /dev/null +++ b/formats/Test.gd @@ -0,0 +1,34 @@ +extends Node +# In case things need to be tested without a library + +static func stress_pattern(): + var notes = [] + for bar in range(8): + notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1)) + for i in range(1, 8): + 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)) + for bar in range(8, 16): + notes.push_back(Note.NoteHold.new(bar*4, bar%8, 2)) + for i in range(1, 8): + 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)) + 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.NoteHold.new(bar*4, bar%8, 2)) + notes.push_back(Note.NoteHold.new(bar*4, (bar+1)%8, 1)) + for i in range(2, 8): + 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)) + for bar in range(24, 32): + notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1)) + for i in range(1, 32): + notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i)%8)) + if (i%2) > 0: + notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i + 4)%8)) + for bar in range(32, 48): + notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1)) + for i in range(1, 32): + 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)) + return notes diff --git a/scenes/Menu.tscn b/scenes/Menu.tscn index ce31539..2c0f40c 100644 --- a/scenes/Menu.tscn +++ b/scenes/Menu.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=6 format=2] [ext_resource path="res://scripts/ScoreText.gd" type="Script" id=1] -[ext_resource path="res://scripts/Menu.gd" type="Script" id=2] +[ext_resource path="res://scripts/TouchMenu.gd" type="Script" id=2] [ext_resource path="res://shaders/menu.tres" type="Material" id=3] [ext_resource path="res://shaders/scoretext.tres" type="Material" id=4] diff --git a/scenes/StepGame.tscn b/scenes/StepGame.tscn index 944bd6b..08df3be 100644 --- a/scenes/StepGame.tscn +++ b/scenes/StepGame.tscn @@ -1,32 +1,16 @@ -[gd_scene load_steps=20 format=2] +[gd_scene load_steps=16 format=2] [ext_resource path="res://scripts/NoteViewport.gd" type="Script" id=1] [ext_resource path="res://assets/text-4k.png" type="Texture" id=2] [ext_resource path="res://scripts/NotePainter.gd" type="Script" id=3] [ext_resource path="res://scripts/InputHandler.gd" type="Script" id=4] +[ext_resource path="res://scenes/StepMenu.tscn" type="PackedScene" id=5] [ext_resource path="res://scripts/NoteHandler.gd" type="Script" id=6] [ext_resource path="res://assets/fonts/Sniglet-Regular.ttf" type="DynamicFontData" id=7] -[ext_resource path="res://scripts/Receptors.gd" type="Script" id=8] [ext_resource path="res://scripts/ScreenFilter.gd" type="Script" id=9] -[ext_resource path="res://scenes/Menu.tscn" type="PackedScene" id=10] -[ext_resource path="res://shaders/receptors.shader" type="Shader" id=11] [ext_resource path="res://shaders/notemesh.shader" type="Shader" id=12] [ext_resource path="res://shaders/notelines.shader" type="Shader" id=13] -[sub_resource type="ShaderMaterial" id=1] -shader = ExtResource( 11 ) -shader_param/num_receptors = 8 -shader_param/receptor_offset = 0.392699 -shader_param/line_color = Color( 0, 0, 1, 1 ) -shader_param/dot_color = Color( 0, 0, 1, 1 ) -shader_param/shadow_color = Color( 0, 0, 0, 0.57 ) -shader_param/line_thickness = 0.00434783 -shader_param/dot_radius = 0.026087 -shader_param/shadow_thickness = 0.0173913 -shader_param/px = 0.00108696 -shader_param/px2 = 0.00217391 -shader_param/alpha = 1.0 - [sub_resource type="ShaderMaterial" id=2] shader = ExtResource( 12 ) shader_param/bps = null @@ -75,22 +59,11 @@ func _on_NoteHandler_finished_song(song_key, score_data) -> void: [sub_resource type="CanvasItemMaterial" id=6] blend_mode = 4 -[sub_resource type="Curve" id=7] -min_value = -1.0 -_data = [ Vector2( -1, -1 ), 0.0, 0.0, 0, 0, Vector2( 0, 0 ), 2.0, 2.0, 1, 1, Vector2( 1, 1 ), 0.0, 0.0, 0, 0 ] - -[node name="StepGame" type="AspectRatioContainer"] +[node name="StepGame" type="Control"] anchor_right = 1.0 anchor_bottom = 1.0 -__meta__ = { -"_edit_use_anchors_": false -} -[node name="Square" type="Control" parent="."] -margin_right = 1080.0 -margin_bottom = 1080.0 - -[node name="video" type="TextureRect" parent="Square" groups=["VideoTexRects"]] +[node name="video" type="TextureRect" parent="." groups=["VideoTexRects"]] anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 @@ -103,7 +76,7 @@ __meta__ = { "_edit_use_anchors_": false } -[node name="ScreenFilter" type="ColorRect" parent="Square"] +[node name="ScreenFilter" type="ColorRect" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 color = Color( 0, 0, 0, 1 ) @@ -112,16 +85,21 @@ __meta__ = { "_edit_use_anchors_": false } -[node name="Receptors" type="Control" parent="Square"] -material = SubResource( 1 ) +[node name="Receptors" type="Control" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 -script = ExtResource( 8 ) __meta__ = { "_edit_use_anchors_": false } -[node name="NoteHandler" type="Control" parent="Square"] +[node name="TextureRect" type="TextureRect" parent="Receptors"] +margin_right = 1080.0 +margin_bottom = 1080.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="NoteHandler" type="Control" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 script = ExtResource( 6 ) @@ -129,28 +107,28 @@ __meta__ = { "_edit_use_anchors_": false } -[node name="Viewport" type="Viewport" parent="Square/NoteHandler"] +[node name="Viewport" type="Viewport" parent="NoteHandler"] size = Vector2( 1080, 1080 ) transparent_bg = true usage = 1 render_target_v_flip = true script = ExtResource( 1 ) -[node name="Center" type="Node2D" parent="Square/NoteHandler/Viewport"] +[node name="Center" type="Node2D" parent="NoteHandler/Viewport"] position = Vector2( 540, 540 ) -[node name="SlideTrailHandler" type="Node2D" parent="Square/NoteHandler/Viewport/Center"] +[node name="SlideTrailHandler" type="Node2D" parent="NoteHandler/Viewport/Center"] -[node name="JudgeText" type="MeshInstance2D" parent="Square/NoteHandler/Viewport/Center"] +[node name="JudgeText" type="MeshInstance2D" parent="NoteHandler/Viewport/Center"] texture = ExtResource( 2 ) -[node name="meshinstance" type="MeshInstance2D" parent="Square/NoteHandler/Viewport/Center"] +[node name="meshinstance" type="MeshInstance2D" parent="NoteHandler/Viewport/Center"] material = SubResource( 2 ) -[node name="notelines" type="MeshInstance2D" parent="Square/NoteHandler/Viewport/Center"] +[node name="notelines" type="MeshInstance2D" parent="NoteHandler/Viewport/Center"] material = SubResource( 3 ) -[node name="lbl_combo" type="Label" parent="Square/NoteHandler"] +[node name="lbl_combo" type="Label" parent="NoteHandler"] visible = false anchor_left = 0.5 anchor_top = 0.5 @@ -171,7 +149,7 @@ __meta__ = { "_edit_use_anchors_": false } -[node name="Painter" type="Control" parent="Square"] +[node name="Painter" type="Control" parent="NoteHandler"] material = SubResource( 6 ) anchor_right = 1.0 anchor_bottom = 1.0 @@ -179,13 +157,11 @@ script = ExtResource( 3 ) __meta__ = { "_edit_use_anchors_": false } +ViewportPath = NodePath("../Viewport") -[node name="Menu" parent="Square" instance=ExtResource( 10 )] -NoteHandlerPath = NodePath("../NoteHandler") -ReceptorsPath = NodePath("../Receptors") -ease_curve = SubResource( 7 ) +[node name="StepMenu" parent="." instance=ExtResource( 5 )] -[node name="InputHandler" type="Control" parent="Square"] +[node name="InputHandler" type="Control" parent="."] anchor_right = 1.0 anchor_bottom = 1.0 script = ExtResource( 4 ) @@ -193,7 +169,7 @@ __meta__ = { "_edit_use_anchors_": false } -[connection signal="combo_changed" from="Square/NoteHandler" to="Square/NoteHandler/lbl_combo" method="_on_NoteHandler_combo_changed"] -[connection signal="finished_song" from="Square/NoteHandler" to="Square/NoteHandler/lbl_combo" method="_on_NoteHandler_finished_song"] -[connection signal="column_pressed" from="Square/InputHandler" to="Square/NoteHandler" method="_on_InputHandler_column_pressed"] -[connection signal="column_released" from="Square/InputHandler" to="Square/NoteHandler" method="_on_InputHandler_column_released"] +[connection signal="combo_changed" from="NoteHandler" to="NoteHandler/lbl_combo" method="_on_NoteHandler_combo_changed"] +[connection signal="finished_song" from="NoteHandler" to="NoteHandler/lbl_combo" method="_on_NoteHandler_finished_song"] +[connection signal="column_pressed" from="InputHandler" to="NoteHandler" method="_on_InputHandler_column_pressed"] +[connection signal="column_released" from="InputHandler" to="NoteHandler" method="_on_InputHandler_column_released"] diff --git a/scenes/StepMenu.tscn b/scenes/StepMenu.tscn new file mode 100644 index 0000000..a06ecd3 --- /dev/null +++ b/scenes/StepMenu.tscn @@ -0,0 +1,28 @@ +[gd_scene load_steps=6 format=2] + +[ext_resource path="res://scripts/ScoreText.gd" type="Script" id=1] +[ext_resource path="res://scripts/StepMenu.gd" type="Script" id=2] +[ext_resource path="res://shaders/menu.tres" type="Material" id=3] +[ext_resource path="res://shaders/scoretext.tres" type="Material" id=4] + +[sub_resource type="Curve" id=1] +min_value = -1.0 +_data = [ Vector2( -1, -1 ), 0.0, 0.0, 0, 0, Vector2( 0, 0 ), 2.0, 2.0, 1, 1, Vector2( 1, 1 ), 0.0, 0.0, 0, 0 ] + +[node name="StepMenu" type="Control"] +material = ExtResource( 3 ) +anchor_right = 1.0 +anchor_bottom = 1.0 +rect_clip_content = true +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": false +} +ease_curve = SubResource( 1 ) + +[node name="ScoreText" type="Node2D" parent="."] +material = ExtResource( 4 ) +script = ExtResource( 1 ) + +[node name="PVMusic" type="AudioStreamPlayer" parent="."] +bus = "Preview" diff --git a/scripts/FileHelpers.gd b/scripts/FileHelpers.gd new file mode 100644 index 0000000..26362a4 --- /dev/null +++ b/scripts/FileHelpers.gd @@ -0,0 +1,76 @@ +# Static functions mostly for FileLoader to make use of because of deficiancies in load() +extends Node + +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' +] + +static func load_image(filename: String) -> ImageTexture: + var tex := ImageTexture.new() + var img := Image.new() + img.load(filename) + tex.create_from_image(img) + return tex + + +static func load_ogg(filename: String) -> AudioStreamOGGVorbis: + # Loads the ogg file with that exact filename + 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 + + +static func load_video(filename: String): + return load(filename) + # This may need reenabling for some platforms: + #var videostream = VideoStreamGDNative.new() + #videostream.set_file(filename) + #return videostream + + +static func directory_list(directory: String, hidden: bool, sort:=true) -> Dictionary: + # 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() + + if sort: + output.folders.sort() + output.files.sort() + # Maybe convert the Arrays to PoolStringArrays? + return output + + +static 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 directory: ', directory, err, ERROR_CODES[err]) + return err diff --git a/scripts/NotePainter.gd b/scripts/NotePainter.gd index 04aa082..5df37dc 100644 --- a/scripts/NotePainter.gd +++ b/scripts/NotePainter.gd @@ -1,6 +1,7 @@ extends Control -onready var Viewport := get_node(@'../NoteHandler/Viewport') +export var ViewportPath := @'../NoteHandler/Viewport' +onready var Viewport := get_node(ViewportPath) func _draw(): draw_texture_rect(Viewport.get_texture(), Rect2(Vector2.ZERO, rect_size), false) diff --git a/scripts/StepMenu.gd b/scripts/StepMenu.gd new file mode 100644 index 0000000..a3f41ea --- /dev/null +++ b/scripts/StepMenu.gd @@ -0,0 +1,585 @@ +#tool +extends Control + +export var NoteHandlerPath := @'../NoteHandler' +export var ReceptorsPath := @'../Receptors' +onready var NoteHandler := get_node(NoteHandlerPath) +onready var Receptors := get_node(ReceptorsPath) +onready var ScoreText := $ScoreText +onready var PVMusic := SoundPlayer.music_player_pv + +var f_scale := 1.0 setget set_f_scale +func set_f_scale(value: float) -> void: + f_scale = value + TitleFont.size = int(round(32*f_scale)) + TitleFont.outline_size = int(max(round(2*f_scale), 1)) + GenreFont.size = int(round(48*f_scale)) + GenreFont.outline_size = int(max(round(2*f_scale), 1)) + DiffNumFont.size = int(round(36*f_scale)) + DiffNumFont.outline_size = int(max(round(1*f_scale), 1)) + ScoreText.set_f_scale(f_scale) +func update_scale() -> void: + self.f_scale = min(rect_size.x/1080, rect_size.y/1080) + +var genres = {} + +enum ChartDifficulty {EASY, BASIC, ADV, EXPERT, MASTER} +enum MenuMode {SONG_SELECT, CHART_SELECT, OPTIONS, GAMEPLAY, SCORE_SCREEN} + +var menu_mode = MenuMode.SONG_SELECT +var menu_mode_prev = MenuMode.SONG_SELECT +var menu_mode_prev_fade_timer := 0.0 +var menu_mode_prev_fade_timer_duration := 0.25 +var currently_playing := false + +var selected_genre: int = 0 +var selected_genre_vis: int = 0 +var selected_genre_delta: float = 0.0 # For floaty display scrolling +var target_song_idx: float = 0.0 setget set_target_song_idx +var target_song_delta: float = 0.0 # For floaty display scrolling +var selected_song_idx: int setget , get_song_idx +var selected_song_key: String setget , get_song_key +var selected_difficulty = ChartDifficulty.ADV + +func set_target_song_idx(index): + target_song_delta -= index - target_song_idx + target_song_idx = index + +func get_song_idx() -> int: + return int(round(self.target_song_idx + target_song_delta)) + +func get_song_key() -> String: + var songslist = genres[genres.keys()[selected_genre]] + return songslist[int(round(self.target_song_idx)) % len(songslist)] + +var scorescreen_song_key := '' +var scorescreen_score_data := {} +var scorescreen_datetime := {} +var scorescreen_saved := false + +var touch_rects = [] + +var TitleFont: DynamicFont = preload('res://assets/MenuTitleFont.tres').duplicate() +var GenreFont: DynamicFont = preload('res://assets/MenuGenreFont.tres').duplicate() +var DiffNumFont: DynamicFont = preload('res://assets/MenuDiffNumberFont.tres').duplicate() +var ScoreFont: DynamicFont = preload('res://assets/MenuScoreFont.tres').duplicate() +var snd_interact := preload('res://assets/softclap.wav') +var snd_error := preload('res://assets/miss.wav') + +export var ease_curve: Curve + + +class lerp_array extends Resource: + var array + func _init(array: Array): + self.array = array + + func value(index: float): + # Only >= 0 for now, but should be fine since it's an arraylike anyway + var i := min(int(floor(index)), len(array)-2) # Somewhat hacky - if we pass len(array)-1 as index, it will return lerp(a[-2], a[-1], 1) == a[-1] + var f := min(index - i, 1.0) + return lerp(array[i], array[i+1], f) + + func len(): + return len(array) + + +func get_rect_center(rect: Rect2) -> Vector2: + return rect.position + rect.size*0.5 + +func scan_library(): + var results = FileLoader.scan_library() + genres = results.genres + +func save_score() -> int: + var data = {'score_data': scorescreen_score_data, 'song_key': scorescreen_song_key} + var dt = scorescreen_datetime + var filename = 'scores/%04d%02d%02dT%02d%02d%02d.json'%[dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second] + match FileLoader.save_json(filename, data): + OK: + scorescreen_saved = true + return OK + var err: + print_debug('Error saving score file %s'%filename) + return err + +func load_score(filename: String): + var result = FileLoader.load_json('scores/%s'%filename) + if not (result is Dictionary): + print('An error occurred while trying to access the chosen score file: ', result) + return result + var data = {} + for key in result.score_data: + var value = {} + for k2 in result.score_data[key]: + if k2 != 'MISS': + k2 = int(k2) # Could use something more robust later + value[k2] = result.score_data[key][k2] + data[int(key)] = value + scorescreen_score_data = data + scorescreen_song_key = result.song_key + scorescreen_saved = true + set_menu_mode(MenuMode.SCORE_SCREEN) + +func load_preview(): + var tmp = self.selected_song_key + var data = Library.all_songs[tmp] + PVMusic.stop() + PVMusic.set_stream(FileLoader.load_ogg('songs/' + data.filepath.rstrip('/') + '/' + data.audio_filelist[0])) + PVMusic.play(16*60.0/data.BPM) + +func _ready(): + scan_library() + connect('item_rect_changed', self, 'update_scale') + NoteHandler.connect('finished_song', self, 'finished_song') + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta): + target_song_delta -= ease_curve.interpolate(clamp(target_song_delta, -2, 2)*0.5) * 10 * delta + if abs(target_song_delta) < 0.02: # Snap + target_song_delta = 0.0 + + var g_diff = selected_genre - (selected_genre_vis + selected_genre_delta) + selected_genre_delta += ease_curve.interpolate(clamp(g_diff, -1, 1)) * 10 * delta + if selected_genre_delta > 0.5: + selected_genre_delta -= 1.0 + selected_genre_vis += 1 + elif selected_genre_delta < -0.5: + selected_genre_delta += 1.0 + selected_genre_vis -= 1 + if abs(g_diff) < 0.02: # Snap + selected_genre_delta = 0.0 + selected_genre_vis = selected_genre + + menu_mode_prev_fade_timer = max(0.0, menu_mode_prev_fade_timer - delta) + update() + if (menu_mode == MenuMode.GAMEPLAY) and (menu_mode_prev_fade_timer <= 0.0) and not NoteHandler.running: + NoteHandler.load_track(self.selected_song_key, Library.Song.default_difficulty_keys[selected_difficulty]) + NoteHandler.running = true + + +func draw_string_centered(font: Font, position: Vector2, string: String, color := GameTheme.COLOR_MENU_TEXT, vcenter := false) -> Vector2: + # Draws horizontally centered from the baseline. Can vcenter via ascent but not perfectly reliable. + # Returns size of the string. + var ss := font.get_string_size(string) + var v := -(font.get_descent() - font.get_height()*0.475) if vcenter else 0.0 # This VCentering is a little fudgey but works for our current fonts + draw_string(font, Vector2(position.x - ss.x*0.5, position.y + v).round(), string, color) + return ss + +func draw_string_ralign(font: Font, position: Vector2, string: String, color := GameTheme.COLOR_MENU_TEXT, vcenter := false) -> Vector2: + # Draws from the bottom-right. Can vcenter via ascent but not perfectly reliable. + # Returns size of the string. + var ss := font.get_string_size(string) + var ascent := font.get_ascent() if vcenter else 0.0 + draw_string(font, Vector2(position.x - ss.x, position.y + ascent*0.5).round(), string, color) + return ss + +func draw_songtile(song_key, position, size, title_text:=false, difficulty=selected_difficulty, outline_px:=3.0, disabled:=false): + # Draws from top left-corner. Returns Rect2 of the image (not the outline). + # Draw difficulty-colored outline + if typeof(difficulty) == TYPE_STRING: + difficulty = Library.Song.difficulty_key_ids.get(difficulty, 0) + + outline_px *= f_scale + + var song_diffs = Library.all_songs[song_key]['chart_difficulties'] + if not (Library.Song.default_difficulty_keys[difficulty] in song_diffs): + difficulty = Library.Song.difficulty_key_ids.get(song_diffs.keys()[-1], 0) + var diff_color := GameTheme.COLOR_DIFFICULTY[difficulty*2] + var rect := Rect2(position.x, position.y, size, size) + draw_rect(Rect2(position.x - outline_px, position.y - outline_px, size + outline_px*2, size + outline_px*2), diff_color) + draw_texture_rect(Library.get_song_tile_texture(song_key), rect, false, Color.white if not disabled else Color(0.5, 0.2, 0.1)) + # Draw track difficulty rating + draw_string_ralign(DiffNumFont, position+Vector2(size-2*f_scale, size-5*f_scale), song_diffs.get(Library.Song.default_difficulty_keys[difficulty], '0'), diff_color) + if disabled: + draw_string_centered(DiffNumFont, position+Vector2(size/2, size/2), 'No Chart!', diff_color, true) + if title_text: + draw_string_centered(TitleFont, position+Vector2(size/2.0, size+40*f_scale), str(Library.all_songs[song_key].title), diff_color.lightened(0.33)) + return rect + +func diff_f2str(difficulty: float): # Convert .5 to + + return str(int(floor(difficulty))) + ('+' if fmod(difficulty, 1.0)>0.4 else '') + +var sel_scales := lerp_array.new([1.0, 0.8, 0.64, 0.5, 0.4, 0.4, 0.4, 0.4, 0.4]) +var bg_scales := lerp_array.new([0.64, 0.64, 0.64, 0.5, 0.4, 0.4, 0.4, 0.4, 0.4]) +func _draw_song_select(center: Vector2) -> Array: + var size = 200 * f_scale + var spacer_x = 12 * f_scale + var spacer_y = 64 * f_scale + var title_spacer_y = 48 * f_scale + var gy: float = center.y - 500 * f_scale - size*selected_genre_delta + var touchrects := [] + + if len(genres) <= 0: + draw_string_centered(GenreFont, Vector2(center.x, center.y-440*f_scale), 'No Songs in Library!', Color.aqua) + draw_string_centered(DiffNumFont, Vector2(center.x, center.y-390*f_scale), FileLoader.userroot, Color.lightgreen) + return touchrects + + var ssid = self.selected_song_idx + var s_delta = target_song_delta-round(target_song_delta) + for gi in [-2, -1, 0, 1, 2]: + var g = (selected_genre_vis + gi) % len(genres) + var selected: bool = (gi == 0) + var scales = sel_scales if selected else bg_scales + + var subsize = size * scales.value(abs(s_delta)) + var gx = center.x - (subsize + spacer_x) * s_delta + var songslist = Library.genre_songs[g].keys() + var genre_str = '%s (%d songs)'%[genres.keys()[g], len(songslist)] + draw_string_centered(GenreFont, Vector2(center.x, gy), genre_str, Color.lightblue) + + var s = len(songslist) + var key = songslist[self.selected_song_idx % s] + var y = gy + 16*f_scale + var x = -subsize/2.0 + var r = draw_songtile(key, Vector2(gx+x, y), subsize, selected) + touchrects.append({rect=r, song_idx=self.selected_song_idx, genre_idx=g}) + + var subsize_p = subsize + var subsize_n = subsize + var x_p = x + var x_n = x + for i in range(1, scales.len()): + x_p += subsize_p + spacer_x + x_n += subsize_n + spacer_x + subsize_p = size * scales.value(abs(i-s_delta)) + subsize_n = size * scales.value(abs(-i-s_delta)) + r = draw_songtile(songslist[(ssid+i) % s], Vector2(gx+x_p, y), subsize_p) + touchrects.append({rect=r, song_idx=ssid+i, genre_idx=g}) + r = draw_songtile(songslist[(ssid-i) % s], Vector2(gx-x_n - subsize_n, y), subsize_n) + touchrects.append({rect=r, song_idx=ssid-i, genre_idx=g}) + gy += size*scales.value(0) + spacer_y + (title_spacer_y if selected else 0) + var b = 1080 * f_scale + var v1 = -590 * f_scale + var v2 = -230 * f_scale + var v4 = -v2 + var v3 = -v1 + var ps = PoolVector2Array([center+Vector2(-b, v1), center+Vector2(b, v1), center+Vector2(b, v2), center+Vector2(-b, v2)]) + var ps2 = PoolVector2Array([center+Vector2(-b, v3), center+Vector2(b, v3), center+Vector2(b, v4), center+Vector2(-b, v4)]) + var cs = PoolColorArray([Color(0,0,0.1,1.25), Color(0,0,0.1,1.25), Color(0,0,0,0), Color(0,0,0,0)]) + draw_polygon(ps, cs) + draw_polygon(ps2, cs) + draw_string_centered(GenreFont, Vector2(center.x, center.y-440*f_scale), 'Select Song', Color.aqua) + draw_string_centered(DiffNumFont, Vector2(center.x, center.y-390*f_scale), 'Tap to scroll, tap focused to select', Color.lightgreen) + return touchrects + +func _draw_chart_select(center: Vector2) -> Array: + # Select difficulty for chosen song + var charts: Dictionary = Library.get_song_charts(self.selected_song_key) + var song_data = Library.all_songs[self.selected_song_key] + var diffs = song_data.chart_difficulties + var n = len(diffs) + var spacer_x = max(14, 70/n) * f_scale + var size = min(192, (1000-spacer_x*(n-1))/n) * f_scale + var rect_back = Rect2(center + Vector2(-300.0, 390.0)*f_scale, Vector2(600.0, 140.0)*f_scale) + draw_rect(rect_back, Color.red) + draw_string_centered(TitleFont, get_rect_center(rect_back), 'Back to song selection', Color.white, true) + draw_string_centered(GenreFont, center+Vector2(0, -360*f_scale), 'Select Difficulty', Color.aqua) + draw_string_centered(DiffNumFont, center+Vector2(0, -300*f_scale), 'Tap to show stats, tap focused to play', Color.lightgreen) + var touchrects = [{rect=rect_back, chart_idx=-1, enabled=true}] # invisible back button + var x = center.x - (size*n + spacer_x*(n-1))/2 + + for diff in diffs: + var i_diff = Library.Song.difficulty_key_ids.get(diff, 0) + var width = 8 if i_diff == selected_difficulty else 3 + var chart_exists: bool = (diff in charts) + var r = draw_songtile(self.selected_song_key, Vector2(x, center.y-160*f_scale), size, false, i_diff, width, not chart_exists) + touchrects.append({rect=r, chart_idx=i_diff, enabled=chart_exists}) + x += size + spacer_x + draw_string_centered(TitleFont, center+Vector2(0, size-116*f_scale), str(Library.all_songs[self.selected_song_key].title)) + + draw_string_centered(TitleFont, center+Vector2(-50*f_scale, size-64*f_scale), 'BPM:') + draw_string_centered(TitleFont, center+Vector2(+50*f_scale, size-64*f_scale), str(song_data.BPM)) + + if len(charts) > 0: + var sel_chart: Array = charts.values()[min(selected_difficulty, len(charts)-1)] + var all_notes: Array = sel_chart[1] + var meta: Dictionary = sel_chart[0] + + var notestrs = ['Taps:', 'Holds:', 'Slides:'] + var notetypes = [0, 1, 2] + var note_counts = [meta.num_taps, meta.num_holds, meta.num_slides] + for i in len(notestrs): + draw_string_centered(TitleFont, center+Vector2(-50*f_scale, size+(12+i*50)*f_scale), notestrs[i]) + draw_string_centered(TitleFont, center+Vector2(+50*f_scale, size+(12+i*50)*f_scale), str(note_counts[notetypes[i]])) + else: + draw_string_centered(TitleFont, center+Vector2(0, size-12*f_scale), 'No available charts!', Color.red) + + return touchrects + +func _draw_score_screen(center: Vector2) -> Array: + var size = 192 * f_scale + var spacer_x = 12 * f_scale + var touchrects = [] + var songslist = genres[genres.keys()[selected_genre]] + var song_key = scorescreen_song_key +# var song_data = Library.all_songs[song_key] + var chart: Array = Library.get_song_charts(song_key)[Library.Song.default_difficulty_keys[selected_difficulty]] + var all_notes: Array = chart[1] + var meta: Dictionary = chart[0] + + var x = center.x + var y = -160*f_scale + var x_score = 110 + var y_score = -380 + var x2 = -360*f_scale + var x_spacing = 124*f_scale + var y_spacing = 42*f_scale + var y2 = y + y_spacing*1.5 + var y3 = y2 + y_spacing + + var tex_judgement_text = GameTheme.tex_judgement_text + var judgement_text_scale = 0.667 + var judgement_text_size = Vector2(256, 64) * judgement_text_scale + + draw_songtile(song_key, center + Vector2.LEFT*size*0.5 + Vector2(-x_score, y_score)*f_scale, size, false, selected_difficulty, 3) + draw_string_centered(TitleFont, center + Vector2.DOWN*size + Vector2(-x_score, y_score+48)*f_scale, str(Library.all_songs[song_key].title)) + var notestrs = ['Taps (%d):'%meta.num_taps, 'Holds (%d) Hit:'%meta.num_holds, 'Released:', 'Stars (%d):'%meta.num_slides, 'Slides:'] + var notetypes = [0, 1, -1, 2, -2] + var note_spacing = [0.0, 1.25, 2.25, 3.5, 4.5] + var judgestrs = Array(Rules.JUDGEMENT_STRINGS + ['Miss']) + var judge_scores = [1.0, 0.9, 0.75, 0.5, 0.0] + var notetype_weights = [1.0, 1.0, 1.0, 1.0, 1.0] + var notecount_total = 0 + var notecount_early = 0 + var notecount_late = 0 + var total_score = 0.0 + var total_scoremax = 0.0 + + for i in len(judgestrs): + # For each judgement type, print a column header +# draw_string_centered(TitleFont, Vector2(x2+x_spacing*(i+1), y2), judgestrs[i]) + var dst_rect = Rect2(center+Vector2(x2+x_spacing*(i+1)-judgement_text_size.x*f_scale/2.0, y2), judgement_text_size*f_scale) + draw_texture_rect_region(tex_judgement_text, dst_rect, Rect2(0, 128*(i+3), 512, 128)) + draw_string_centered(TitleFont, center+Vector2(x2+x_spacing*(len(judgestrs)+1), y2+34*f_scale), 'Score') + + for i in len(notestrs): + # For each note type, make a row and print scores + var idx = notetypes[i] + var note_score = 0 + var note_count = 0 + var y_row = y3 + y_spacing * (note_spacing[i]+1) + draw_string_centered(TitleFont, center+Vector2(x2-20*f_scale, y_row), notestrs[i]) + for j in len(judgestrs): + var score + if j == 0: + score = scorescreen_score_data[idx][0] + elif j >= len(judgestrs)-1: + score = scorescreen_score_data[idx]['MISS'] + else: + score = scorescreen_score_data[idx][j] + scorescreen_score_data[idx][-j] + notecount_early += scorescreen_score_data[idx][-j] + notecount_late += scorescreen_score_data[idx][j] + if (j >= len(judgestrs)-1) and (idx == -1): + draw_string_centered(TitleFont, center+Vector2(x2+x_spacing*(j+1), y_row), '^') + else: + draw_string_centered(TitleFont, center+Vector2(x2+x_spacing*(j+1), y_row), str(score)) + notecount_total += score # Kinda redundant, will probably refactor eventually + note_count += score + note_score += score * judge_scores[j] + draw_string_centered(TitleFont, center+Vector2(x2+x_spacing*(len(judgestrs)+1), y_row), '%2.2f%%'%(note_score/max(note_count, 1)*100.0)) + total_score += note_score * notetype_weights[i] + total_scoremax += note_count * notetype_weights[i] + + var overall_score = total_score/max(total_scoremax, 1.0) + var score_idx = 0 + for cutoff in Rules.SCORE_CUTOFFS: + if overall_score >= cutoff: + break + else: + score_idx += 1 + ScoreText.position = center+Vector2(x_score, y_score)*f_scale + ScoreText.score = Rules.SCORE_STRINGS[score_idx] + ScoreText.score_sub = '%2.3f%%'%(overall_score*100.0) + ScoreText.update() + + draw_string_centered(TitleFont, center+Vector2(-150, y3+y_spacing*7), 'Early : Late') + draw_string_centered(TitleFont, center+Vector2(-150, y3+y_spacing*8), '%3d%% : %3d%%'%[notecount_early*100/max(notecount_total, 1), notecount_late*100/max(notecount_total, 1)]) + draw_string_centered(TitleFont, center+Vector2(150, y3+y_spacing*7.5), 'Max Combo: %d'%scorescreen_score_data.get('max_combo', 0)) # Safety for older saves + + var txt_offset = Vector2.DOWN*10*f_scale + var rect_songs := Rect2(center+Vector2(-100.0, 300.0)*f_scale, Vector2(400.0, 100.0)*f_scale) + draw_rect(rect_songs, Color.red) + draw_string_centered(TitleFont, get_rect_center(rect_songs), 'Song Select', Color.white, true) + touchrects.append({rect=rect_songs, next_menu=MenuMode.SONG_SELECT}) + + var rect_save := Rect2(center+Vector2(-300.0, 300.0)*f_scale, Vector2(180.0, 100.0)*f_scale) + if not scorescreen_saved: + draw_rect(rect_save, Color(0.0, 0.01, 1.0)) + draw_string_centered(TitleFont, get_rect_center(rect_save), 'Save', Color.white, true) + touchrects.append({rect=rect_save, action='save'}) + else: + draw_rect(rect_save, Color.darkgray) + draw_string_centered(TitleFont, get_rect_center(rect_save), 'Saved', Color.white, true) + + draw_string_centered(GenreFont, center+Vector2.UP*410*f_scale, 'Results', Color.aqua) + return touchrects + +func _draw_gameplay(center: Vector2) -> Array: + var touchrects = [] + + var rect_songselect := Rect2(center+Vector2(+860.0, 480.0)*f_scale, Vector2(100.0, 50.0)*f_scale) + draw_rect(rect_songselect, Color.red) + draw_string_centered(TitleFont, get_rect_center(rect_songselect), 'Stop', Color.white, true) + touchrects.append({rect=rect_songselect, action='stop'}) + return touchrects + + +func _draw(): + var songs = len(Library.all_songs) + var score_screen_filter_alpha := 0.65 + var size = 216 + var outline_px = 3 + var center = rect_size * 0.5 + touch_rects = [] + ScoreText.hide() + for i in MenuMode: + touch_rects.append([]) + + if menu_mode_prev_fade_timer > 0.0: + var progress = 1.0 - menu_mode_prev_fade_timer/menu_mode_prev_fade_timer_duration + var center_prev = lerp(center, center+Vector2(0.0, 1200.0), progress) + var center_next = center_prev + Vector2(0.0, -1200.0) + match menu_mode_prev: + MenuMode.SONG_SELECT: + _draw_song_select(center_prev) + MenuMode.CHART_SELECT: + _draw_chart_select(center_prev) + MenuMode.OPTIONS: + pass + MenuMode.GAMEPLAY: + GameTheme.set_screen_filter_alpha(lerp(0.0, score_screen_filter_alpha, progress)) + MenuMode.SCORE_SCREEN: + _draw_score_screen(center_prev) + match menu_mode: + MenuMode.SONG_SELECT: + _draw_song_select(center_next) + MenuMode.CHART_SELECT: + _draw_chart_select(center_next) + MenuMode.OPTIONS: + pass + MenuMode.GAMEPLAY: + GameTheme.set_screen_filter_alpha(1.0 - progress) + MenuMode.SCORE_SCREEN: + _draw_score_screen(center_next) + ScoreText.show() + else: + match menu_mode: + MenuMode.SONG_SELECT: + GameTheme.set_screen_filter_alpha(1.0) + touch_rects[menu_mode] = _draw_song_select(center) + MenuMode.CHART_SELECT: + GameTheme.set_screen_filter_alpha(1.0) + touch_rects[menu_mode] = _draw_chart_select(center) + MenuMode.OPTIONS: + pass + MenuMode.GAMEPLAY: + GameTheme.set_screen_filter_alpha(0.0) + touch_rects[menu_mode] = _draw_gameplay(center) + MenuMode.SCORE_SCREEN: + GameTheme.set_screen_filter_alpha(score_screen_filter_alpha) + touch_rects[menu_mode] = _draw_score_screen(center) + ScoreText.show() + +func set_menu_mode(mode): + Receptors.fade(mode == MenuMode.GAMEPLAY) + if mode == MenuMode.GAMEPLAY: + PVMusic.stop() + rect_clip_content = false + else: + rect_clip_content = true + menu_mode_prev = menu_mode + menu_mode = mode + menu_mode_prev_fade_timer = menu_mode_prev_fade_timer_duration + +func touch_select_song(touchdict): + if (self.selected_genre == touchdict.genre_idx) and (self.selected_song_idx == touchdict.song_idx): + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0) + # var songslist = genres[genres.keys()[selected_genre]] + # selected_song_key = songslist[self.target_song_idx % len(songslist)] + set_menu_mode(MenuMode.CHART_SELECT) + else: + self.selected_genre = touchdict.genre_idx + self.target_song_idx = touchdict.song_idx + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -4.5) + load_preview() + +func touch_select_chart(touchdict): + if touchdict.chart_idx == selected_difficulty: + if touchdict.enabled: + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0) + set_menu_mode(MenuMode.GAMEPLAY) + else: + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_error, 0.0) + elif touchdict.chart_idx < 0: + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -3.0, 0.7) + set_menu_mode(MenuMode.SONG_SELECT) + else: + self.selected_difficulty = touchdict.chart_idx + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -4.5) + +func touch_gameplay(touchdict): + if touchdict.has('action'): + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0) + if touchdict.action == 'stop': + NoteHandler.stop() + +func touch_score_screen(touchdict): + if touchdict.has('next_menu'): + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0) + set_menu_mode(touchdict.next_menu) + ScoreText.score = '' + ScoreText.score_sub = '' + # TODO: time this to coincide with the menu going fully offscreen + ScoreText.update() + elif touchdict.has('action'): + SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0) + if touchdict.action == 'save': + save_score() + +func finished_song(song_key, score_data): + scorescreen_song_key = song_key + scorescreen_score_data = score_data + scorescreen_datetime = OS.get_datetime() + scorescreen_saved = false + set_menu_mode(MenuMode.SCORE_SCREEN) + + +func _input(event): + if !visible: + return + if (event is InputEventMouseButton): # Add this if we ever manage to be rid of the curse of Touch->Mouse emulation: (event is InputEventScreenTouch) +# print(event) + if event.pressed: + var pos = event.position - get_global_transform_with_canvas().get_origin() + match menu_mode: + MenuMode.SONG_SELECT: + for d in touch_rects[MenuMode.SONG_SELECT]: + if d.rect.has_point(pos): + touch_select_song(d) + MenuMode.CHART_SELECT: + for d in touch_rects[MenuMode.CHART_SELECT]: + if d.rect.has_point(pos): + touch_select_chart(d) + MenuMode.GAMEPLAY: + for d in touch_rects[MenuMode.GAMEPLAY]: + if d.rect.has_point(pos): + touch_gameplay(d) + MenuMode.SCORE_SCREEN: + for d in touch_rects[MenuMode.SCORE_SCREEN]: + if d.rect.has_point(pos): + touch_score_screen(d) + match menu_mode: + MenuMode.SONG_SELECT: + if event.is_action_pressed('ui_right'): # Sadly can't use match with this input system + self.target_song_idx += 1 + elif event.is_action_pressed('ui_left'): + self.target_song_idx -= 1 + elif event.is_action_pressed('ui_up'): + selected_genre = posmod(selected_genre - 1, len(genres)) + elif event.is_action_pressed('ui_down'): + selected_genre = posmod(selected_genre + 1, len(genres)) + elif event.is_action_pressed('ui_page_up'): + selected_difficulty = int(max(0, selected_difficulty - 1)) + elif event.is_action_pressed('ui_page_down'): + selected_difficulty = int(min(6, selected_difficulty + 1)) diff --git a/scripts/Menu.gd b/scripts/TouchMenu.gd similarity index 100% rename from scripts/Menu.gd rename to scripts/TouchMenu.gd diff --git a/singletons/FileLoader.gd b/singletons/FileLoader.gd index c26fec4..556d285 100644 --- a/singletons/FileLoader.gd +++ b/singletons/FileLoader.gd @@ -1,17 +1,14 @@ #extends Object extends Node +var FileHelpers := preload('res://scripts/FileHelpers.gd') -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 RGT := preload('res://formats/RGT.gd') +var SM := preload('res://formats/SM.gd') +var SRT := preload('res://formats/SRT.gd') + +const NOT_FOUND := '' + +const default_difficulty_keys = ['Z', 'B', 'A', 'E', 'M', 'R'] var userroot := OS.get_user_data_dir().rstrip('/')+'/' if OS.get_name() != 'Android' else '/storage/emulated/0/RhythmGame/' var PATHS := PoolStringArray([userroot, '/media/fridge-q/Games/RTG/slow_userdir/']) # Temporary hardcoded testing @@ -21,42 +18,38 @@ var PATHS := PoolStringArray([userroot, '/media/fridge-q/Games/RTG/slow_userdir/ func _ready() -> void: print('Library paths: ', PATHS) -func find_file(name: String) -> String: + +func find_file(name: String, print_notfound:=false) -> 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 '' + if print_notfound: + print('File not found in any libraries: ', name) + return NOT_FOUND -func directory_list(directory: String, hidden: bool, sort:=true) -> Dictionary: - # 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() +var fallback_audiostream = AudioStreamOGGVorbis.new() +var fallback_videostream = VideoStreamWebm.new() +var fallback_texture := ImageTexture.new() + +func load_ogg(name: String) -> AudioStreamOGGVorbis: # Searches through all of the paths to find the file + match find_file(name): + NOT_FOUND: return fallback_audiostream + var filename: return FileHelpers.load_ogg(filename) + +func load_video(name: String): # Searches through all of the paths to find the file + match find_file(name): + NOT_FOUND: return fallback_videostream + var filename: return FileHelpers.load_video(filename) + +func load_image(name: String) -> ImageTexture: # Searches through all of the paths to find the file + match find_file(name, true): + NOT_FOUND: return fallback_texture + var filename: return FileHelpers.load_image(filename) - if sort: - output.folders.sort() - output.files.sort() - # Maybe convert the Arrays to PoolStringArrays? - return output func find_by_extensions(array, extensions=null) -> Dictionary: # Both args can be Array or PoolStringArray @@ -78,8 +71,8 @@ func find_by_extensions(array, extensions=null) -> Dictionary: output[ext] = [filename] return output -const default_difficulty_keys = ['Z', 'B', 'A', 'E', 'M', 'R'] -func scan_library(): + +func scan_library() -> Dictionary: print('Scanning library') var song_defs = {} var collections = {} @@ -93,7 +86,7 @@ func scan_library(): print_debug('An error occurred while trying to create the songs directory: ', err) return err - var songslist = directory_list(rootdir, false) + var songslist = FileHelpers.directory_list(rootdir, false) if songslist.err != OK: print('An error occurred when trying to access the songs directory: ', songslist.err) return songslist.err @@ -138,7 +131,7 @@ func scan_library(): genres[song_defs[song_key]['genre']] = [song_key] else: - var files_by_ext = find_by_extensions(directory_list(full_folder, false).files) + var files_by_ext = find_by_extensions(FileHelpers.directory_list(full_folder, false).files) if 'sm' in files_by_ext: var sm_filename = files_by_ext['sm'][0] print(sm_filename) @@ -153,465 +146,6 @@ func scan_library(): return {song_defs=song_defs, genres=genres} - -class SRT: - const TAP_DURATION := 0.062500 - const ID_BREAK := 4 - const ID_HOLD := 2 - const ID_SLIDE_END := 128 - const ID3_SLIDE_CHORD := 0 # Straight line - const ID3_SLIDE_ARC_CW := 1 - const ID3_SLIDE_ARC_ACW := 2 - - static func load_file(filename): - var file = File.new() - var err = file.open(filename, File.READ) - if err != OK: - print(err) - return err - var metadata := {} - var num_taps := 0 - var num_holds := 0 - var num_slides := 0 - var notes := [] - var beats_per_measure := 4 - var length = file.get_len() - var slide_ids = {} - while (file.get_position() < (length-2)): - var noteline = file.get_csv_line() - var time_hit := (float(noteline[0]) + (float(noteline[1]))) * beats_per_measure - 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.NoteHold.new(time_hit, column, duration)) - num_holds += 1 - ID_BREAK: - notes.push_back(Note.NoteTap.new(time_hit, column, true)) - num_taps += 1 - ID_SLIDE_END: - # id2 is slide ID - if id2 in slide_ids: - slide_ids[id2].column_release = column - slide_ids[id2].update_slide_variables() - _: - if id2 == 0: - notes.push_back(Note.NoteTap.new(time_hit, column)) - num_taps += 1 - 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 - var slide_type = Note.SlideType.CHORD - match id3: - ID3_SLIDE_CHORD: - slide_type = Note.SlideType.CHORD - ID3_SLIDE_ARC_CW: - slide_type = Note.SlideType.ARC_CW - ID3_SLIDE_ARC_ACW: - slide_type = Note.SlideType.ARC_ACW - _: - print('Unknown slide type: ', id3) - var note = Note.NoteStar.new(time_hit, column) - num_slides += 1 - 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 - metadata['num_taps'] = num_taps - metadata['num_holds'] = num_holds - metadata['num_slides'] = num_slides - return [metadata, notes] - - -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_STAR, - 'e': Note.NOTE_SLIDE, - 'b': Note.NOTE_TAP, # Break - 'x': Note.NOTE_STAR # Break star - } - const SLIDE_TYPES = { - '0': null, # Seems to be used for stars without slides attached - '1': Note.SlideType.CHORD, - '2': Note.SlideType.ARC_ACW, - '3': Note.SlideType.ARC_CW, - '4': Note.SlideType.COMPLEX, # Orbit around center ACW on the way - '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, # Go to center then orbit off to the side ACW - '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 - 'd': Note.SlideType.CHORD_TRIPLE, # Triple cone. Spreads out to the adjacent receptors of the target. - 'e': Note.SlideType.CHORD, # Not used in any of our charts - 'f': Note.SlideType.CHORD, # Not used in any of our charts - } - const SLIDE_IN_R := sin(PI/8) # Circle radius circumscribed by chords 0-3, 1-4, 2-5 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 chart_ids = [] - var lines = [[]] - # This loop will segment the lines as if the file were RGTM - 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 - chart_ids.append(line.lstrip('[').rstrip(']')) - lines.append([]) - elif !line.empty(): - lines[-1].push_back(line) - file.close() - print('Parsing chart: ', filename) - - match format: - Format.RGTS: - var metadata_and_notes = parse_rgts(lines[0]) - return metadata_and_notes - Format.RGTX: - var metadata_and_notes = parse_rgtx(lines[0]) - return metadata_and_notes - Format.RGTM: - lines.pop_front() # Anything before the first [header] is meaningless - var charts = {} - for i in len(lines): - charts[chart_ids[i]] = parse_rgts(lines[i]) - return charts - return format - - static func parse_rgtx(lines: PoolStringArray): - return [] # To be implemented later - - const beats_per_measure = 4.0 # TODO: Bit of an ugly hack, need to revisit this later - 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 := [] - for i in Rules.COLS: - last_star.append(null) - - 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]) * beats_per_measure - var note_hits := [] - var note_nonhits := [] - for i in range(1, len(s)): - var n = s[i] - var column := int(n[0]) - var ntype = n[1] - n = n.substr(2) - - match ntype: - 't', 'b': # tap - note_hits.append(Note.NoteTap.new(time, column, ntype=='b')) - num_taps += 1 - 'h': # hold - var duration = float(n) * beats_per_measure - note_hits.append(Note.NoteHold.new(time, column, duration)) - num_holds += 1 - 's', 'x': # slide star - var star = Note.NoteStar.new(time, column, ntype=='z') - note_hits.append(star) - num_slides += 1 - 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) - 'e': # slide end - var slide_type = n[0] # numeric digit, left as str just in case - var slide_id = int(n.substr(1)) - 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() - if SLIDE_TYPES[slide_type] == Note.SlideType.COMPLEX: - var col_hit = slide_ids[slide_id].column - var RUV = GameTheme.RADIAL_UNIT_VECTORS - var RCA = GameTheme.RADIAL_COL_ANGLES - slide_ids[slide_id].values.curve2d.add_point(RUV[col_hit]) # Start col - match slide_type: - '4': # Orbit 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 - Note.curve2d_make_orbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], true) - '5': # CW of above - Note.curve2d_make_orbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], false) - '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': # Orbit off-center ACW - Note.curve2d_make_sideorbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], true) - 'a': # CW of above - Note.curve2d_make_sideorbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], false) - 'b': # V into column 2 places ACW - slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit-2, Rules.COLS)]) - 'c': # V into column 2 places CW - slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit+2, Rules.COLS)]) - slide_ids[slide_id].values.curve2d.add_point(RUV[column]) # End col - 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) - '_': - print_debug('Unknown note type: ', ntype) - - if len(note_hits) > 1: - for note in note_hits: # Set multihit on each one - note.double_hit = true - notes += note_hits + note_nonhits - metadata['num_taps'] = num_taps - metadata['num_holds'] = num_holds - metadata['num_slides'] = num_slides - return [metadata, notes] - - -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', -# '#LYRICSPATH': '', - '#CDTITLE': 'image_cd_title', - '#MUSIC': 'audio_filelist', - '#OFFSET': 'audio_offsets', - '#SAMPLESTART': 'audio_preview_times', - '#SAMPLELENGTH': 'audio_preview_times', -# '#SELECTABLE': '', - '#BPMS': 'bpm_values', -# '#STOPS': '', -# '#BGCHANGES': '', -# '#KEYSOUNDS': '', - } - - 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.NoteTap.new(time, col)) - '2': # Hold - ongoing_holds[col] = len(notes) - notes.append(Note.NoteHold.new(time, col, 0.0)) - num_holds += 1 - '4': # Roll - ongoing_holds[col] = len(notes) - notes.append(Note.NoteRoll.new(time, col, 0.0)) - num_rolls += 1 - '3': # End Hold/Roll - assert(ongoing_holds.has(col)) - notes[ongoing_holds[col]].set_time_release(time) - ongoing_holds.erase(col) - 'M': # Mine - num_mines += 1 - pass - metadata['num_notes'] = num_notes - metadata['num_taps'] = num_notes - num_jumps - 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 - metadata['notes'] = notes - return metadata - - static func load_file(filename: String) -> Array: - # Output is [metadata, [[meta0, chart0], ..., [metaN, chartN]]] - # 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() - match file.open(filename, File.READ): - OK: - pass - var err: - print_debug('Error loading file: ', err) - return [] - 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() - var line : String = file.get_line() - 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] - elif len(tokens) >= 2: - metadata[tokens[0]] = tokens[1] - var charts = [] - - for i in range(1, len(lines)): - charts.append(load_chart(lines[i])) - - return [metadata, charts] - - -class Test: - static func stress_pattern(): - var notes = [] - for bar in range(8): - notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1)) - for i in range(1, 8): - 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)) - for bar in range(8, 16): - notes.push_back(Note.NoteHold.new(bar*4, bar%8, 2)) - for i in range(1, 8): - 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)) - 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.NoteHold.new(bar*4, bar%8, 2)) - notes.push_back(Note.NoteHold.new(bar*4, (bar+1)%8, 1)) - for i in range(2, 8): - 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)) - for bar in range(24, 32): - notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1)) - for i in range(1, 32): - notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i)%8)) - if (i%2) > 0: - notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i + 4)%8)) - for bar in range(32, 48): - notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1)) - for i in range(1, 32): - 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)) - return notes - - func load_folder(folder, filename='song'): var file = File.new() var err = file.open('%s/%s.json' % [folder, filename], File.READ) @@ -629,6 +163,7 @@ func load_folder(folder, filename='song'): result.directory = folder return result + func load_filelist(filelist: Array, directory=''): var charts = {} var key := 0 @@ -636,7 +171,7 @@ func load_filelist(filelist: Array, directory=''): var extension: String = name.rsplit('.', true, 1)[-1] name = directory.rstrip('/') + '/' + name var filename = find_file(name) - if filename != '': + if filename != NOT_FOUND: match extension: 'rgtm': # multiple charts var res = RGT.load_file(filename) @@ -659,63 +194,15 @@ func load_filelist(filelist: Array, directory=''): pass return charts -func direct_load_ogg(filename: String) -> AudioStreamOGGVorbis: - # Loads the ogg file with that exact filename - 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 - -var fallback_audiostream = AudioStreamOGGVorbis.new() -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) - -var fallback_videostream = VideoStreamWebm.new() -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: - var tex := ImageTexture.new() - var img := Image.new() - img.load(filename) - tex.create_from_image(img) - return tex - -var fallback_texture := ImageTexture.new() -func load_image(name: String) -> ImageTexture: - var filename = find_file(name) - if filename != '': - return direct_load_image(filename) - print('File not found: ', name) - 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] - match FileLoader.init_directory(dir): + match FileHelpers.init_directory(dir): OK: pass var err: - print_debug('Error making directory for JSON file: ', err, ERROR_CODES[err]) + print_debug('Error making directory for JSON file: ', err, FileHelpers.ERROR_CODES[err]) return err var json = JSON.print(data) var file = File.new() @@ -725,9 +212,10 @@ func save_json(filename: String, data: Dictionary): file.close() return OK var err: - print_debug('Error saving JSON file: ', err, ERROR_CODES[err]) + print_debug('Error saving JSON file: ', err, FileHelpers.ERROR_CODES[err]) return err + func load_json(filename: String): var file = File.new() var err @@ -736,7 +224,7 @@ func load_json(filename: String): 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]) + print('An error occurred while trying to open file: ', filename1, err, FileHelpers.ERROR_CODES[err]) continue # return err var result_json = JSON.parse(file.get_as_text()) file.close()