import sqlite3 from datetime import datetime, timedelta from pathlib import Path from typing import Optional, Union import pyray as ray from libs import song_hash from libs.animation import Animation from libs.audio import audio from libs.tja import TJAParser from libs.utils import ( OutlinedText, get_current_ms, global_data, is_l_don_pressed, is_l_kat_pressed, is_r_don_pressed, is_r_kat_pressed, load_all_textures_from_zip, session_data, ) class State: BROWSING = 0 SONG_SELECTED = 1 class SongSelectScreen: BOX_CENTER = 444 def __init__(self, screen_width: int, screen_height: int): self.screen_init = False self.root_dir = global_data.config["paths"]["tja_path"] self.screen_width = screen_width self.screen_height = screen_height self.navigator = FileNavigator(self.root_dir) def load_textures(self): self.textures = load_all_textures_from_zip(Path('Graphics/lumendata/song_select.zip')) self.textures['custom'] = [ray.load_texture('1.png'), ray.load_texture('2.png')] def load_sounds(self): sounds_dir = Path("Sounds") self.sound_don = audio.load_sound(sounds_dir / "inst_00_don.wav") self.sound_kat = audio.load_sound(sounds_dir / "inst_00_katsu.wav") self.sound_skip = audio.load_sound(sounds_dir / 'song_select' / 'Skip.ogg') self.sound_ura_switch = audio.load_sound(sounds_dir / 'song_select' / 'SE_SELECT [4].ogg') audio.set_sound_volume(self.sound_ura_switch, 0.25) self.sound_bgm = audio.load_sound(sounds_dir / "song_select" / "JINGLE_GENRE [1].ogg") #self.sound_cancel = audio.load_sound(sounds_dir / "cancel.wav") def on_screen_start(self): if not self.screen_init: self.load_textures() self.load_sounds() self.selected_song = None self.selected_difficulty = -1 self.game_transition = None self.move_away = Animation.create_move(float('inf')) self.diff_fade_out = Animation.create_fade(0, final_opacity=1.0) self.background_move = Animation.create_move(15000, start_position=0, total_distance=1280) self.state = State.BROWSING self.text_fade_out = None self.text_fade_in = None self.texture_index = 784 self.last_texture_index = 784 self.background_fade_change = None self.demo_song = None for item in self.navigator.items: item.box.reset() self.navigator.get_current_item().box.get_scores() self.screen_init = True self.last_moved = get_current_ms() self.ura_toggle = 0 self.ura_switch_animation = None self.is_ura = False if str(global_data.selected_song) in self.navigator.all_song_files: self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(global_data.selected_song)]) def on_screen_end(self, next_screen): self.screen_init = False global_data.selected_song = self.navigator.get_current_item().path session_data.selected_difficulty = self.selected_difficulty audio.unload_sound(self.sound_bgm) self.reset_demo_music() for zip in self.textures: for texture in self.textures[zip]: ray.unload_texture(texture) return next_screen def reset_demo_music(self): if self.demo_song is not None: audio.stop_music_stream(self.demo_song) audio.unload_music_stream(self.demo_song) audio.play_sound(self.sound_bgm) self.demo_song = None self.navigator.get_current_item().box.wait = get_current_ms() def handle_input(self): if self.state == State.BROWSING: if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT_CONTROL) or (is_l_kat_pressed() and get_current_ms() <= self.last_moved + 100): self.reset_demo_music() self.wait = get_current_ms() for i in range(10): self.navigator.navigate_left() audio.play_sound(self.sound_skip) self.last_moved = get_current_ms() elif ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT_CONTROL) or (is_r_kat_pressed() and get_current_ms() <= self.last_moved + 100): self.reset_demo_music() for i in range(10): self.navigator.navigate_right() audio.play_sound(self.sound_skip) self.last_moved = get_current_ms() elif is_l_kat_pressed(): self.reset_demo_music() self.navigator.navigate_left() audio.play_sound(self.sound_kat) self.last_moved = get_current_ms() elif is_r_kat_pressed(): self.reset_demo_music() self.navigator.navigate_right() audio.play_sound(self.sound_kat) self.last_moved = get_current_ms() # Select/Enter if is_l_don_pressed() or is_r_don_pressed(): selected_item = self.navigator.items[self.navigator.selected_index] if selected_item is not None and selected_item.box.texture_index == 552: self.navigator.go_back() #audio.play_sound(self.sound_cancel) else: selected_song = self.navigator.select_current_item() if selected_song: self.state = State.SONG_SELECTED if 4 not in selected_song.tja.metadata.course_data: self.is_ura = False audio.play_sound(self.sound_don) self.move_away = Animation.create_move(233, total_distance=500) self.diff_fade_out = Animation.create_fade(83) elif self.state == State.SONG_SELECTED: # Handle song selection confirmation or cancel if is_l_don_pressed() or is_r_don_pressed(): if self.selected_difficulty == -1: self.selected_song = None self.move_away = Animation.create_move(float('inf')) self.diff_fade_out = Animation.create_fade(0, final_opacity=1.0) self.text_fade_out = None self.text_fade_in = None self.state = State.BROWSING for item in self.navigator.items: item.box.reset() else: audio.play_sound(self.sound_don) self.game_transition = Transition(self.screen_height) if is_l_kat_pressed(): audio.play_sound(self.sound_kat) selected_song = self.navigator.get_current_item() if not isinstance(selected_song, Directory): diffs = sorted([item for item in selected_song.tja.metadata.course_data]) if self.is_ura and self.selected_difficulty == 4: self.selected_difficulty = 2 elif self.selected_difficulty == -1: pass elif self.selected_difficulty not in diffs: self.selected_difficulty = min(diffs) elif self.selected_difficulty == min(diffs): self.selected_difficulty = -1 elif self.selected_difficulty > min(diffs): self.selected_difficulty = (diffs[diffs.index(self.selected_difficulty) - 1]) else: raise Exception("Directory was chosen instead of song") if is_r_kat_pressed(): audio.play_sound(self.sound_kat) selected_song = self.navigator.get_current_item() if not isinstance(selected_song, Directory): diffs = sorted([item for item in selected_song.tja.metadata.course_data]) if self.is_ura and self.selected_difficulty == 2: self.selected_difficulty = 4 if (self.selected_difficulty == 3 or self.selected_difficulty == 4) and 4 in diffs: self.ura_toggle = (self.ura_toggle + 1) % 10 if self.ura_toggle == 0: self.is_ura = not self.is_ura self.ura_switch_animation = UraSwitchAnimation(not self.is_ura) audio.play_sound(self.sound_ura_switch) self.selected_difficulty = 7 - self.selected_difficulty elif self.selected_difficulty not in diffs: self.selected_difficulty = min(diffs) elif self.selected_difficulty < max(diffs): self.selected_difficulty = (diffs[diffs.index(self.selected_difficulty) + 1]) else: raise Exception("Directory was chosen instead of song") if ray.is_key_pressed(ray.KeyboardKey.KEY_TAB) and (self.selected_difficulty == 3 or self.selected_difficulty == 4): self.ura_toggle = 0 self.is_ura = not self.is_ura self.ura_switch_animation = UraSwitchAnimation(not self.is_ura) audio.play_sound(self.sound_ura_switch) self.selected_difficulty = 7 - self.selected_difficulty def update(self): self.on_screen_start() if self.background_move.is_finished: self.background_move = Animation.create_move(15000, start_position=0, total_distance=1280) self.background_move.update(get_current_ms()) if self.game_transition is not None: self.game_transition.update(get_current_ms()) if self.game_transition.is_finished: return self.on_screen_end("GAME") else: self.handle_input() if self.demo_song is not None: audio.update_music_stream(self.demo_song) if self.background_fade_change is None: self.last_texture_index = self.texture_index for song in self.navigator.items: song.box.update(self.state == State.SONG_SELECTED) song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 if not isinstance(song, Directory) and song.box.is_open: if self.demo_song is None and get_current_ms() >= song.box.wait + (83.33*3): song.box.get_scores() if song.tja.metadata.wave.exists() and song.tja.metadata.wave.is_file(): self.demo_song = audio.load_music_stream(song.tja.metadata.wave, preview=song.tja.metadata.demostart, normalize=0.1935) audio.play_music_stream(self.demo_song) audio.stop_sound(self.sound_bgm) if song.box.is_open: current_box = song.box if current_box.texture_index != 552 and get_current_ms() >= song.box.wait + (83.33*3): self.texture_index = SongBox.BACKGROUND_MAP[current_box.texture_index] if self.last_texture_index != self.texture_index and self.background_fade_change is None: self.background_fade_change = Animation.create_fade(200) self.move_away.update(get_current_ms()) self.diff_fade_out.update(get_current_ms()) if self.background_fade_change is not None: self.background_fade_change.update(get_current_ms()) if self.background_fade_change.is_finished: self.background_fade_change = None if self.move_away.is_finished and self.text_fade_out is None: self.text_fade_out = Animation.create_fade(33) self.text_fade_in = Animation.create_fade(33, initial_opacity=0.0, final_opacity=1.0, delay=self.text_fade_out.duration) if self.text_fade_out is not None: self.text_fade_out.update(get_current_ms()) if self.text_fade_out.is_finished: self.selected_song = True if self.text_fade_in is not None: self.text_fade_in.update(get_current_ms()) if self.ura_switch_animation is not None: self.ura_switch_animation.update(get_current_ms()) if self.navigator.genre_bg is not None: self.navigator.genre_bg.update(get_current_ms()) if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): return self.on_screen_end('ENTRY') def draw_selector(self): if self.selected_difficulty == -1: ray.draw_texture(self.textures['song_select'][133], 314, 110, ray.WHITE) else: difficulty = min(3, self.selected_difficulty) ray.draw_texture(self.textures['song_select'][140], 450 + (difficulty * 115), 7, ray.WHITE) ray.draw_texture(self.textures['song_select'][131], 461 + (difficulty * 115), 132, ray.WHITE) def draw(self): # Draw file/directory list texture_back = self.textures['song_select'][self.last_texture_index] texture = self.textures['song_select'][self.texture_index] for i in range(0, texture.width * 4, texture.width): if self.background_fade_change is not None: color = ray.fade(ray.WHITE, self.background_fade_change.attribute) ray.draw_texture(texture_back, i - int(self.background_move.attribute), 0, color) reverse_color = ray.fade(ray.WHITE, 1 - self.background_fade_change.attribute) ray.draw_texture(texture, i - int(self.background_move.attribute), 0, reverse_color) else: ray.draw_texture(texture, i - int(self.background_move.attribute), 0, ray.WHITE) if self.navigator.genre_bg is not None and self.state == State.BROWSING: self.navigator.genre_bg.draw(self.textures, 95) for item in self.navigator.items: box = item.box if -156 <= box.position <= self.screen_width + 144: if box.position <= 500: box.draw(box.position - int(self.move_away.attribute), 95, self.textures, self.is_ura, fade_override=self.diff_fade_out.attribute) else: box.draw(box.position + int(self.move_away.attribute), 95, self.textures, self.is_ura, fade_override=self.diff_fade_out.attribute) if self.ura_switch_animation is not None: self.ura_switch_animation.draw(self.textures) if self.selected_song and self.state == State.SONG_SELECTED: self.draw_selector() fade = ray.WHITE if self.text_fade_in is not None: fade = ray.fade(ray.WHITE, self.text_fade_in.attribute) ray.draw_texture(self.textures['song_select'][192], 5, 5, fade) else: fade = ray.WHITE if self.text_fade_out is not None: fade = ray.fade(ray.WHITE, self.text_fade_out.attribute) ray.draw_texture(self.textures['song_select'][244], 5, 5, fade) ray.draw_texture(self.textures['song_select'][394], 0, self.screen_height - self.textures['song_select'][394].height, ray.WHITE) if self.game_transition is not None: self.game_transition.draw(self.screen_height) class SongBox: OUTLINE_MAP = { 555: ray.Color(0, 77, 104, 255), 560: ray.Color(156, 64, 2, 255), 565: ray.Color(153, 4, 46, 255), 570: ray.Color(60, 104, 0, 255), 575: ray.Color(134, 88, 0, 255), 580: ray.Color(79, 40, 134, 255), 585: ray.Color(148, 24, 0, 255), 615: ray.Color(84, 101, 126, 255) } FOLDER_HEADER_MAP = { 555: 643, 560: 645, 565: 647, 570: 649, 575: 651, 580: 653, 585: 655, 615: 667, 620: 670 } FULL_FOLDER_HEADER_MAP = { 555: 736, 560: 738, 565: 740, 570: 742, 575: 744, 580: 746, 585: 748, 615: 760, 620: 762, } BACKGROUND_MAP = { 555: 772, 560: 773, 565: 774, 570: 775, 575: 776, 580: 777, 585: 778, 615: 783, 620: 784 } GENRE_CHAR_MAP = { 555: 507, 560: 509, 565: 511, 570: 513, 575: 515, 580: 517, 585: 519, 615: 532, } def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, tja_count: Optional[int] = None, box_texture: Optional[ray.Texture] = None, name_texture_index: Optional[int] = None): self.text_name = name self.texture_index = texture_index if name_texture_index is None: self.name_texture_index = texture_index else: self.name_texture_index = name_texture_index self.box_texture = box_texture self.scores = dict() self.crown = dict() self.position = -11111 self.start_position = -1 self.target_position = -1 self.is_open = False self.name = None self.subtitle = None self.black_name = None self.hori_name = None self.yellow_box = None self.open_anim = None self.open_fade = None self.move = None self.wait = 0 self.is_dir = is_dir self.is_genre_start = 0 self.is_genre_end = False self.genre_distance = 0 self.tja_count = tja_count self.tja_count_text = None if self.tja_count is not None and self.tja_count != 0: self.tja_count_text = OutlinedText(str(self.tja_count), 35, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5)#, horizontal_spacing=1.2) self.tja = tja self.hash = dict() self.update(False) def reset(self): if self.black_name is not None: if self.tja is not None: subtitle = OutlinedText(self.tja.metadata.subtitle.get(global_data.config['general']['language'], ''), 30, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True) else: subtitle = None self.yellow_box = YellowBox(self.black_name, self.texture_index == 552, tja=self.tja, subtitle=subtitle) self.open_anim = None self.open_fade = None def get_scores(self): if self.tja is None: return with sqlite3.connect('scores.db') as con: cursor = con.cursor() diffs_to_compute = [] for diff in self.tja.metadata.course_data: if diff not in self.hash: diffs_to_compute.append(diff) if diffs_to_compute: for diff in diffs_to_compute: notes, _, bars = TJAParser.notes_to_position(TJAParser(self.tja.file_path), diff) self.hash[diff] = self.tja.hash_note_data(notes, bars) # Batch database query for all diffs at once if self.tja.metadata.course_data: hash_values = [self.hash[diff] for diff in self.tja.metadata.course_data] placeholders = ','.join('?' * len(hash_values)) batch_query = f""" SELECT hash, score, good, ok, bad, clear FROM Scores WHERE hash IN ({placeholders}) """ cursor.execute(batch_query, hash_values) hash_to_score = {row[0]: row[1:] for row in cursor.fetchall()} for diff in self.tja.metadata.course_data: diff_hash = self.hash[diff] self.scores[diff] = hash_to_score.get(diff_hash) def update(self, is_diff_select): self.is_diff_select = is_diff_select if self.yellow_box is not None: self.yellow_box.update(is_diff_select) is_open_prev = self.is_open if self.position != self.target_position and self.move is None: if self.position < self.target_position: direction = 1 else: direction = -1 if abs(self.target_position - self.position) > 250: direction *= -1 self.move = Animation.create_move(83.3, start_position=0, total_distance=100 * direction, ease_out='cubic') if self.is_open or self.target_position == SongSelectScreen.BOX_CENTER + 150: self.move.total_distance = 250 * direction self.start_position = self.position if self.move is not None: self.move.update(get_current_ms()) self.position = self.start_position + int(self.move.attribute) if self.move.is_finished: self.position = self.target_position self.move = None self.is_open = self.position == SongSelectScreen.BOX_CENTER + 150 if not is_open_prev and self.is_open: if self.black_name is None: self.black_name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True) #print(f"loaded black name {self.text_name}") if self.tja is not None or self.texture_index == 552: if self.tja is not None: subtitle = OutlinedText(self.tja.metadata.subtitle.get(global_data.config['general']['language'], ''), 30, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True) else: subtitle = None self.yellow_box = YellowBox(self.black_name, self.texture_index == 552, tja=self.tja, subtitle=subtitle) self.yellow_box.create_anim() else: self.hori_name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5) #print(f"loaded hori name {self.text_name}") self.open_anim = Animation.create_move(133, start_position=0, total_distance=150, delay=83.33) self.open_fade = Animation.create_fade(200, initial_opacity=0, final_opacity=1.0) self.wait = get_current_ms() elif not self.is_open: if self.black_name is not None: self.black_name.unload() self.black_name = None if self.yellow_box is not None: self.yellow_box = None if self.hori_name is not None: self.hori_name.unload() self.hori_name = None if self.open_anim is not None: self.open_anim.update(get_current_ms()) if self.open_fade is not None: self.open_fade.update(get_current_ms()) ''' if self.black_name is None: self.black_name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5, vertical=True) if self.name is None: self.name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), SongBox.OUTLINE_MAP.get(self.texture_index, ray.Color(101, 0, 82, 255)), outline_thickness=5, vertical=True) ''' if self.name is None and -56 <= self.position <= 1280: self.name = OutlinedText(self.text_name, 40, ray.Color(255, 255, 255, 255), SongBox.OUTLINE_MAP.get(self.name_texture_index, ray.Color(101, 0, 82, 255)), outline_thickness=5, vertical=True) #print(f"loaded {self.text_name}") elif self.name is not None and (self.position < -56 or self.position > 1280): self.name.unload() self.name = None def _draw_closed(self, x: int, y: int, textures): ray.draw_texture(textures['song_select'][self.texture_index+1], x, y, ray.WHITE) offset = 0 if 555 <= self.texture_index <= 600: offset = 1 for i in range(0, textures['song_select'][self.texture_index].width * 4, textures['song_select'][self.texture_index].width): ray.draw_texture(textures['song_select'][self.texture_index], (x+32)+i, y - offset, ray.WHITE) ray.draw_texture(textures['song_select'][self.texture_index+2], x+64, y, ray.WHITE) if self.texture_index == 620: ray.draw_texture(textures['song_select'][self.texture_index+3], x+12, y+16, ray.WHITE) if self.texture_index != 552 and self.is_dir: ray.draw_texture(textures['song_select'][SongBox.FOLDER_HEADER_MAP[self.texture_index]], x+4 - offset, y-6, ray.WHITE) if self.texture_index == 552: ray.draw_texture(textures['song_select'][422], x + 47 - int(textures['song_select'][422].width / 2), y+35, ray.WHITE) elif self.name is not None: src = ray.Rectangle(0, 0, self.name.texture.width, self.name.texture.height) dest = ray.Rectangle(x + 47 - int(self.name.texture.width / 2), y+35, self.name.texture.width, min(self.name.texture.height, 417)) self.name.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE) if self.tja is not None: if self.tja.ex_data.new: ray.draw_texture(textures['song_select'][677], x-5, y-85, ray.WHITE) if self.scores: highest_key = max(self.scores.keys()) score = self.scores[highest_key] if score and score[3] == 0: ray.draw_texture(textures['song_select'][683+highest_key], x+20, y-30, ray.WHITE) elif score and score[4] == 1: ray.draw_texture(textures['song_select'][688+highest_key], x+20, y-30, ray.WHITE) if self.crown: highest_crown = max(self.crown) if self.crown[highest_crown] == 'FC': ray.draw_texture(textures['song_select'][683+highest_crown], x+20, y-30, ray.WHITE) else: ray.draw_texture(textures['song_select'][688+highest_crown], x+20, y-30, ray.WHITE) #ray.draw_text(str(self.position), x, y-25, 25, ray.GREEN) def _draw_open(self, x: int, y: int, textures, fade_override): if self.open_anim is not None: color = ray.WHITE if fade_override is not None: color = ray.fade(ray.WHITE, fade_override) if self.hori_name is not None and self.open_anim.attribute >= 100: texture = textures['song_select'][SongBox.FULL_FOLDER_HEADER_MAP[self.texture_index]] src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(x-115+48, (y-56) + 150 - int(self.open_anim.attribute), texture.width+220, texture.height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0,0), 0, color) texture = textures['song_select'][SongBox.FULL_FOLDER_HEADER_MAP[self.texture_index]+1] src = ray.Rectangle(0, 0, -texture.width, texture.height) dest = ray.Rectangle(x-115, y-56 + 150 - int(self.open_anim.attribute), texture.width, texture.height) ray.draw_texture(texture, x+160, y-56 + 150 - int(self.open_anim.attribute), color) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0,0), 0, color) src = ray.Rectangle(0, 0, self.hori_name.texture.width, self.hori_name.texture.height) dest_width = min(300, self.hori_name.texture.width) dest = ray.Rectangle((x + 48) - (dest_width//2), y-52 + 150 - int(self.open_anim.attribute), dest_width, self.hori_name.texture.height) self.hori_name.draw(src, dest, ray.Vector2(0, 0), 0, color) ray.draw_texture(textures['song_select'][self.texture_index+1], x - int(self.open_anim.attribute), y, ray.WHITE) offset = 0 if 555 <= self.texture_index <= 600: offset = 1 for i in range(0, textures['song_select'][self.texture_index].width * (5+int(self.open_anim.attribute / 4)), textures['song_select'][self.texture_index].width): ray.draw_texture(textures['song_select'][self.texture_index], ((x- int(self.open_anim.attribute))+32)+i, y - offset, ray.WHITE) ray.draw_texture(textures['song_select'][self.texture_index+2], x+64 + int(self.open_anim.attribute), y, ray.WHITE) color = ray.WHITE if self.texture_index == 620: ray.draw_texture(textures['song_select'][self.texture_index+4], x+12 - 150, y+16, color) if fade_override is not None: color = ray.fade(ray.WHITE, min(0.5, fade_override)) ray.draw_texture(textures['song_select'][492], 470, 125, color) color = ray.WHITE if fade_override is not None: color = ray.fade(ray.WHITE, fade_override) if self.tja_count_text is not None: ray.draw_texture(textures['song_select'][493], 475, 125, color) ray.draw_texture(textures['song_select'][494], 600, 125, color) src = ray.Rectangle(0, 0, self.tja_count_text.texture.width, self.tja_count_text.texture.height) dest_width = min(124, self.tja_count_text.texture.width) dest = ray.Rectangle(560 - (dest_width//2), 118, dest_width, self.tja_count_text.texture.height) self.tja_count_text.draw(src, dest, ray.Vector2(0, 0), 0, color) if self.texture_index in SongBox.GENRE_CHAR_MAP: ray.draw_texture(textures['song_select'][SongBox.GENRE_CHAR_MAP[self.texture_index]+1], 650, 125, color) ray.draw_texture(textures['song_select'][SongBox.GENRE_CHAR_MAP[self.texture_index]], 470, 180, color) elif self.box_texture is not None: ray.draw_texture(self.box_texture, (x+48) - (self.box_texture.width//2), (y+240) - (self.box_texture.height//2), color) def draw(self, x: int, y: int, textures, is_ura: bool, fade_override=None): if self.is_open and get_current_ms() >= self.wait + 83.33: if self.yellow_box is not None: self.yellow_box.draw(textures, self, fade_override, is_ura) else: if self.open_fade is not None: self._draw_open(x, y, textures, self.open_fade.attribute) else: self._draw_closed(x, y, textures) class GenreBG: BG_MAP = { 555: 547, 560: 558, 565: 563, 570: 568, 575: 573, 580: 578, 585: 583, 615: 613, 620: 618 } HEADER_MAP = { 555: 423, 560: 425, 565: 427, 570: 429, 575: 431, 580: 433, 585: 435, 615: 768, 620: 447 } def __init__(self, start_box: SongBox, end_box: SongBox, title: OutlinedText): self.start_box = start_box self.end_box = end_box self.start_position = start_box.position self.end_position = end_box.position self.title = title self.fade_in = Animation.create_fade(116, initial_opacity=0.0, final_opacity=1.0, ease_in='quadratic', delay=50) def update(self, current_ms): self.start_position = self.start_box.position self.end_position = self.end_box.position self.fade_in.update(current_ms) def draw(self, textures, y): texture_index = GenreBG.BG_MAP[self.end_box.texture_index] color = ray.fade(ray.WHITE, self.fade_in.attribute) offset = -150 if self.start_box.is_open else 0 texture = textures['song_select'][texture_index] src = ray.Rectangle(0, 0, -texture.width, texture.height) dest = ray.Rectangle(self.start_position+offset-5, y-70, texture.width, texture.height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0,0), 0, color) extra_distance = 155 if self.end_box.is_open or self.start_box.is_open else 0 x = self.start_position+18+offset texture = textures['song_select'][texture_index+1] src = ray.Rectangle(0, 0, texture.width, texture.height) if self.start_position >= -56 and self.end_position < self.start_position: dest = ray.Rectangle(x, y-70, self.start_position + 1280 + 56, texture.height) else: dest = ray.Rectangle(x, y-70, abs(self.end_position) - self.start_position + extra_distance + 57, texture.height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0,0), 0, color) if self.end_position < self.start_position and self.end_position >= -56: dest = ray.Rectangle(0, y-70, min(self.end_position+75, 1280) + extra_distance, texture.height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0,0), 0, color) offset = 150 if self.end_box.is_open else 0 ray.draw_texture(textures['song_select'][texture_index], self.end_position+75+offset, y-70, ray.WHITE) if ((self.start_position <= 594 and self.end_position >= 594) or ((self.start_position <= 594 or self.end_position >= 594) and (self.start_position > self.end_position))): dest_width = min(300, self.title.texture.width) texture = textures['song_select'][GenreBG.HEADER_MAP[self.end_box.texture_index]] src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle((1280//2) - (dest_width//2), y-68, dest_width, texture.height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, color) texture = textures['song_select'][GenreBG.HEADER_MAP[self.end_box.texture_index]+1] src = ray.Rectangle(0, 0, -texture.width, texture.height) dest = ray.Rectangle((1280//2) - (dest_width//2) - (texture.width//2), y-68, texture.width, texture.height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, color) ray.draw_texture(texture, (1280//2) + (dest_width//2) - (texture.width//2), y-68, color) src = ray.Rectangle(0, 0, self.title.texture.width, self.title.texture.height) dest = ray.Rectangle((1280//2) - (dest_width//2), y-68, dest_width, self.title.texture.height) self.title.draw(src, dest, ray.Vector2(0, 0), 0, color) class YellowBox: def __init__(self, name: OutlinedText, is_back: bool, tja: Optional[TJAParser] = None, subtitle: Optional[OutlinedText] = None): self.is_diff_select = False self.right_x = 803 self.left_x = 443 self.top_y = 96 self.bottom_y = 543 self.center_width = 332 self.center_height = 422 self.edge_height = 32 self.name = name self.subtitle = subtitle self.is_back = is_back self.tja = tja self.anim_created = False self.left_out = Animation.create_move(83.33, total_distance=-152, delay=83.33) self.right_out = Animation.create_move(83.33, total_distance=145, delay=83.33) self.center_out = Animation.create_move(83.33, total_distance=300, delay=83.33) self.fade = Animation.create_fade(83.33, initial_opacity=1.0, final_opacity=1.0, delay=83.33) self.reset_animations() def reset_animations(self): self.fade_in = Animation.create_fade(float('inf'), initial_opacity=0.0, final_opacity=1.0, delay=83.33) self.left_out_2 = Animation.create_move(float('inf'), total_distance=-213) self.right_out_2 = Animation.create_move(float('inf'), total_distance=0) self.center_out_2 = Animation.create_move(float('inf'), total_distance=423) self.top_y_out = Animation.create_move(float('inf'), total_distance=-62) self.center_h_out = Animation.create_move(float('inf'), total_distance=60) def create_anim(self): self.left_out = Animation.create_move(83.33, total_distance=-152, delay=83.33) self.right_out = Animation.create_move(83.33, total_distance=145, delay=83.33) self.center_out = Animation.create_move(83.33, total_distance=300, delay=83.33) self.fade = Animation.create_fade(83.33, initial_opacity=0.0, final_opacity=1.0, delay=83.33) def create_anim_2(self): self.left_out_2 = Animation.create_move(116.67, total_distance=-213) self.right_out_2 = Animation.create_move(116.67, total_distance=211) self.center_out_2 = Animation.create_move(116.67, total_distance=423) self.top_y_out = Animation.create_move(133.33, total_distance=-62, delay=self.left_out_2.duration) self.center_h_out = Animation.create_move(133.33, total_distance=60, delay=self.left_out_2.duration) self.fade_in = Animation.create_fade(116.67, initial_opacity=0.0, final_opacity=1.0, delay=self.left_out_2.duration + self.top_y_out.duration + 16.67) def update(self, is_diff_select: bool): self.left_out.update(get_current_ms()) self.right_out.update(get_current_ms()) self.center_out.update(get_current_ms()) self.fade.update(get_current_ms()) self.fade_in.update(get_current_ms()) self.left_out_2.update(get_current_ms()) self.right_out_2.update(get_current_ms()) self.center_out_2.update(get_current_ms()) self.top_y_out.update(get_current_ms()) self.center_h_out.update(get_current_ms()) self.is_diff_select = is_diff_select if self.is_diff_select: if not self.anim_created: self.anim_created = True self.create_anim_2() self.right_x = 803 + int(self.right_out_2.attribute) self.left_x = 443 + int(self.left_out_2.attribute) self.top_y = 96 + int(self.top_y_out.attribute) self.center_width = 332 + int(self.center_out_2.attribute) self.center_height = 422 + int(self.center_h_out.attribute) else: self.anim_created = False self.right_x = 658 + int(self.right_out.attribute) self.left_x = 595 + int(self.left_out.attribute) self.top_y = 96 self.center_width = 32 + int(self.center_out.attribute) self.center_height = 422 def draw(self, textures: dict[str, list[ray.Texture]], song_box: SongBox, fade_override: Optional[float], is_ura: bool): # Draw corners ray.draw_texture(textures['song_select'][235], self.right_x, self.bottom_y, ray.WHITE) # Bottom right ray.draw_texture(textures['song_select'][236], self.left_x, self.bottom_y, ray.WHITE) # Bottom left ray.draw_texture(textures['song_select'][237], self.right_x, self.top_y, ray.WHITE) # Top right ray.draw_texture(textures['song_select'][238], self.left_x, self.top_y, ray.WHITE) # Top left # Edges # Bottom edge texture = textures['song_select'][231] src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(self.left_x + self.edge_height, self.bottom_y, self.center_width, texture.height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE) # Right edge texture = textures['song_select'][232] src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(self.right_x, self.top_y + self.edge_height, texture.width, self.center_height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE) # Left edge texture = textures['song_select'][233] src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(self.left_x, self.top_y + self.edge_height, texture.width, self.center_height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE) # Top edge texture = textures['song_select'][234] src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(self.left_x + self.edge_height, self.top_y, self.center_width, texture.height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE) # Center texture = textures['song_select'][230] src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(self.left_x + self.edge_height, self.top_y + self.edge_height, self.center_width, self.center_height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE) if self.is_diff_select and self.tja is not None: #Back Button color = ray.fade(ray.WHITE, self.fade_in.attribute) ray.draw_texture(textures['song_select'][153], 314, 110, color) #Difficulties ray.draw_texture(textures['song_select'][154], 450, 90, color) if 0 not in self.tja.metadata.course_data: ray.draw_texture(textures['song_select'][161], 450, 90, ray.fade(ray.WHITE, min(self.fade_in.attribute, 0.25))) ray.draw_texture(textures['song_select'][182], 565, 90, color) if 1 not in self.tja.metadata.course_data: ray.draw_texture(textures['song_select'][183], 565, 90, ray.fade(ray.WHITE, min(self.fade_in.attribute, 0.25))) ray.draw_texture(textures['song_select'][185], 680, 90, color) if 2 not in self.tja.metadata.course_data: ray.draw_texture(textures['song_select'][186], 680, 90, ray.fade(ray.WHITE, min(self.fade_in.attribute, 0.25))) if is_ura: ray.draw_texture(textures['song_select'][190], 795, 90, color) ray.draw_texture(textures['song_select'][191], 807, 130, color) else: ray.draw_texture(textures['song_select'][188], 795, 90, color) if 3 not in self.tja.metadata.course_data: ray.draw_texture(textures['song_select'][189], 795, 90, ray.fade(ray.WHITE, min(self.fade_in.attribute, 0.25))) #Stars for course in self.tja.metadata.course_data: if course == 4 and not is_ura: continue if course == 3 and is_ura: continue for j in range(self.tja.metadata.course_data[course].level): ray.draw_texture(textures['song_select'][155], 482+(min(course, 3)*115), 471+(j*-20), color) else: #Crowns fade = self.fade.attribute if fade_override is not None: fade = min(self.fade.attribute, fade_override) color = ray.fade(ray.WHITE, fade) if self.is_back: ray.draw_texture(textures['song_select'][421], 498, 250, color) elif self.tja is not None: for diff in self.tja.metadata.course_data: if diff == 4: continue elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][3] == 0: ray.draw_texture(textures['song_select'][160], 473 + (diff*60), 175, color) elif diff in song_box.scores and song_box.scores[diff] is not None and song_box.scores[diff][4] == 1: ray.draw_texture(textures['song_select'][159], 473 + (diff*60), 175, color) ray.draw_texture(textures['song_select'][158], 473 + (diff*60), 175, ray.fade(ray.WHITE, min(fade, 0.25))) #EX Data if self.tja.ex_data.new_audio: ray.draw_texture(textures['custom'][0], 458, 120, color) elif self.tja.ex_data.old_audio: ray.draw_texture(textures['custom'][1], 458, 120, color) elif self.tja.ex_data.limited_time: ray.draw_texture(textures['song_select'][418], 458, 120, color) elif self.tja.ex_data.new: ray.draw_texture(textures['song_select'][408], 458, 120, color) #Difficulties ray.draw_texture(textures['song_select'][395], 458, 210, color) if 0 not in self.tja.metadata.course_data: ray.draw_texture(textures['song_select'][400], 458, 210, ray.fade(ray.WHITE, min(fade, 0.25))) ray.draw_texture(textures['song_select'][401], 518, 210, color) if 1 not in self.tja.metadata.course_data: ray.draw_texture(textures['song_select'][402], 518, 210, ray.fade(ray.WHITE, min(fade, 0.25))) ray.draw_texture(textures['song_select'][403], 578, 210, color) if 2 not in self.tja.metadata.course_data: ray.draw_texture(textures['song_select'][404], 578, 210, ray.fade(ray.WHITE, min(fade, 0.25))) ray.draw_texture(textures['song_select'][406], 638, 210, color) if 3 not in self.tja.metadata.course_data: ray.draw_texture(textures['song_select'][407], 638, 210, ray.fade(ray.WHITE, min(fade, 0.25))) #Stars for diff in self.tja.metadata.course_data: if diff == 4: continue for j in range(self.tja.metadata.course_data[diff].level): ray.draw_texture(textures['song_select'][396], 474+(diff*60), 490+(j*-17), color) if self.is_back: texture = textures['song_select'][422] x = int(((song_box.position + 55) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute))) y = self.top_y+35 ray.draw_texture(texture, x, y, ray.WHITE) elif self.name is not None: texture = self.name.texture x = int(((song_box.position + 55) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute))) y = self.top_y+35 src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(x, y, texture.width, min(texture.height, 417)) self.name.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE) if self.subtitle is not None: texture = self.subtitle.texture x = int(((song_box.position + 10) - texture.width / 2) + (int(self.right_out.attribute)*0.85) + (int(self.right_out_2.attribute))) y = self.bottom_y - min(texture.height, 410) + 10 + int(self.top_y_out.attribute) src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(x, y, texture.width, min(texture.height, 410)) self.subtitle.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE) class UraSwitchAnimation: def __init__(self, is_backwards: bool) -> None: forwards_animation = ((0, 32, 166), (32, 80, 167), (80, 112, 168), (112, 133, 169)) backwards_animation = ((0, 32, 169), (32, 80, 170), (80, 112, 171), (112, 133, 166)) if is_backwards: self.texture_change = Animation.create_texture_change(133, textures=backwards_animation) else: self.texture_change = Animation.create_texture_change(133, textures=forwards_animation) self.fade_out = Animation.create_fade(166, delay=133) def update(self, current_ms: float): self.texture_change.update(current_ms) self.fade_out.update(current_ms) def draw(self, textures: dict[str, list[ray.Texture]]): ray.draw_texture(textures['song_select'][self.texture_change.attribute], 815, 134, ray.fade(ray.WHITE, self.fade_out.attribute)) class Transition: def __init__(self, screen_height: int) -> None: self.is_finished = False self.rainbow_up = Animation.create_move(266, start_position=0, total_distance=screen_height + global_data.textures['scene_change_rainbow'][2].height, ease_in='cubic') self.chara_down = None def update(self, current_time_ms: float): self.rainbow_up.update(current_time_ms) if self.rainbow_up.is_finished and self.chara_down is None: self.chara_down = Animation.create_move(33, start_position=0, total_distance=30) if self.chara_down is not None: self.chara_down.update(current_time_ms) self.is_finished = self.chara_down.is_finished def draw(self, screen_height: int): ray.draw_texture(global_data.textures['scene_change_rainbow'][2], 0, screen_height - int(self.rainbow_up.attribute), ray.WHITE) texture = global_data.textures['scene_change_rainbow'][0] src = ray.Rectangle(0, 0, texture.width, texture.height) dest = ray.Rectangle(0, screen_height - int(self.rainbow_up.attribute) + global_data.textures['scene_change_rainbow'][2].height, texture.width, screen_height) ray.draw_texture_pro(texture, src, dest, ray.Vector2(0, 0), 0, ray.WHITE) texture = global_data.textures['scene_change_rainbow'][3] offset = 0 if self.chara_down is not None: offset = int(self.chara_down.attribute) ray.draw_texture(texture, 76, 816 - int(self.rainbow_up.attribute) + offset, ray.WHITE) class FileSystemItem: GENRE_MAP = { 'J-POP': 555, 'アニメ': 560, 'どうよう': 565, 'バラエティー': 570, 'クラシック': 575, 'ゲームミュージック': 580, 'ナムコオリジナル': 585, 'VOCALOID': 615, } """Base class for files and directories in the navigation system""" def __init__(self, path: Path, name: str): self.path = path self.selected = False class Directory(FileSystemItem): """Represents a directory in the navigation system""" COLLECTIONS = [ 'NEW', 'RECENT', 'FAVORITE', 'DIFFICULTY', 'RECOMMENDED' ] def __init__(self, path: Path, name: str, texture_index: int, has_box_def=False, to_root=False, back=False, tja_count=0, box_texture=None, collection=None): super().__init__(path, name) self.has_box_def = has_box_def self.to_root = to_root self.back = back self.tja_count = tja_count self.collection = None if collection in Directory.COLLECTIONS: self.collection = collection if self.to_root or self.back: texture_index = 552 self.box = SongBox(name, texture_index, True, tja_count=tja_count, box_texture=box_texture) class SongFile(FileSystemItem): """Represents a song file (TJA) in the navigation system""" def __init__(self, path: Path, name: str, texture_index: int, tja=None, name_texture_index: Optional[int]=None): super().__init__(path, name) self.is_recent = (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)) <= timedelta(days=7) if self.is_recent: print(name, (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime))) self.tja = tja or TJAParser(path) if self.is_recent: self.tja.ex_data.new = True title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) self.box = SongBox(title, texture_index, False, tja=self.tja, name_texture_index=name_texture_index if name_texture_index is not None else texture_index) self.box.get_scores() class FileNavigator: """Manages navigation through pre-generated Directory and SongFile objects""" def __init__(self, root_dirs: list[str]): # Handle both single path and list of paths if isinstance(root_dirs, (list, tuple)): self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in root_dirs] else: self.root_dirs = [Path(root_dirs) if not isinstance(root_dirs, Path) else root_dirs] # Pre-generated objects storage self.all_directories: dict[str, Directory] = {} # path -> Directory self.all_song_files: dict[str, SongFile] = {} # path -> SongFile self.directory_contents: dict[str, list[Union[Directory, SongFile]]] = {} # path -> list of items self.root_items: list[Union[Directory, SongFile]] = [] # OPTION 2: Lazy crown calculation with caching self.directory_crowns: dict[str, dict] = dict() # path -> crown list self.crown_cache_dirty: set[str] = set() # directories that need crown recalculation # Navigation state self.in_root_selection = True self.current_dir = Path() self.current_root_dir = Path() self.items: list[Directory | SongFile] = [] self.new_items: list[Directory | SongFile] = [] self.selected_index = 0 self.history = [] self.box_open = False self.genre_bg = None # Generate all objects upfront self._generate_all_objects() self.load_root_directories() def _generate_all_objects(self): """Generate all Directory and SongFile objects in advance""" print("Generating all Directory and SongFile objects...") # First, generate objects for each root directory for root_path in self.root_dirs: if not root_path.exists(): print(f"Root directory does not exist: {root_path}") continue self._generate_objects_recursive(root_path, is_root=True) print(f"Object generation complete. " f"Directories: {len(self.all_directories)}, " f"Songs: {len(self.all_song_files)}") def _generate_objects_recursive(self, dir_path: Path, is_root=False): """Recursively generate Directory and SongFile objects for a directory""" if not dir_path.is_dir(): return dir_key = str(dir_path) # Check for box.def has_box_def = (dir_path / "box.def").exists() # Parse box.def if it exists name = dir_path.name if dir_path.name else str(dir_path) texture_index = 620 box_texture = None collection = None if has_box_def: name, texture_index, collection = self._parse_box_def(dir_path) box_png_path = dir_path / "box.png" if box_png_path.exists(): box_texture = ray.load_texture(str(box_png_path)) # Count TJA files for this directory tja_count = self._count_tja_files(dir_path) # Create Directory object directory_obj = Directory( dir_path, name, texture_index, has_box_def=has_box_def, tja_count=tja_count, box_texture=box_texture, collection=collection ) self.all_directories[dir_key] = directory_obj # Generate content list for this directory content_items = [] # Add child directories that have box.def child_dirs = [] for item_path in dir_path.iterdir(): if item_path.is_dir(): child_has_box_def = (item_path / "box.def").exists() if child_has_box_def: child_dirs.append(item_path) # Recursively generate objects for child directory self._generate_objects_recursive(item_path) # Sort and add child directories for child_path in sorted(child_dirs): child_key = str(child_path) if child_key in self.all_directories: content_items.append(self.all_directories[child_key]) # Get TJA files for this directory tja_files = self._get_tja_files_for_directory(dir_path) # Create SongFile objects for i, tja_path in enumerate(sorted(tja_files)): song_key = str(tja_path) if song_key not in self.all_song_files: try: song_obj = SongFile(tja_path, tja_path.name, texture_index) if song_obj.is_recent: self.new_items.append(SongFile(tja_path, tja_path.name, 620, name_texture_index=texture_index)) self.all_song_files[song_key] = song_obj except Exception as e: print(f"Error creating SongFile for {tja_path}: {e}") continue content_items.append(self.all_song_files[song_key]) # Store content for this directory self.directory_contents[dir_key] = content_items # OPTION 2: Mark directory for lazy crown calculation self.crown_cache_dirty.add(dir_key) # If this is a root directory, add to root items if is_root: if has_box_def: self.root_items.append(directory_obj) else: # For roots without box.def, add their TJA files directly all_tja_files = self._find_tja_files_recursive(dir_path) for tja_path in sorted(all_tja_files): song_key = str(tja_path) if song_key not in self.all_song_files: try: song_obj = SongFile(tja_path, tja_path.name, 620) self.all_song_files[song_key] = song_obj except Exception as e: print(f"Error creating SongFile for {tja_path}: {e}") continue self.root_items.append(self.all_song_files[song_key]) def _count_tja_files(self, folder_path: Path): """Count TJA files in directory (matching original logic)""" tja_count = 0 # Find all song_list.txt files recursively song_list_files = list(folder_path.rglob("song_list.txt")) if song_list_files: # Process all song_list.txt files found for song_list_path in song_list_files: try: with open(song_list_path, 'r', encoding='utf-8-sig') as song_list_file: tja_count += len([line for line in song_list_file.readlines() if line.strip()]) except (IOError, UnicodeDecodeError) as e: # Handle potential file reading errors print(f"Warning: Could not read {song_list_path}: {e}") continue else: # Fallback: Use recursive counting of .tja files tja_count = sum(1 for _ in folder_path.rglob("*.tja")) return tja_count def _get_directory_crowns_cached(self, dir_key: str) -> dict: """Get crowns for a directory, calculating only if needed""" if dir_key in self.crown_cache_dirty or dir_key not in self.directory_crowns: # Calculate crowns on-demand dir_path = Path(dir_key) tja_files = self._get_tja_files_for_directory(dir_path) self._calculate_directory_crowns(dir_key, tja_files) self.crown_cache_dirty.discard(dir_key) return self.directory_crowns.get(dir_key, dict()) def _calculate_directory_crowns(self, dir_key: str, tja_files: list): """Pre-calculate crowns for a directory""" all_scores = dict() crowns = dict() for tja_path in sorted(tja_files): song_key = str(tja_path) if song_key in self.all_song_files: song_obj = self.all_song_files[song_key] for diff in song_obj.box.scores: if diff not in all_scores: all_scores[diff] = [] all_scores[diff].append(song_obj.box.scores[diff]) for diff in all_scores: if all(score is not None and score[3] == 0 for score in all_scores[diff]): crowns[diff] = 'FC' elif all(score is not None and score[4] == 1 for score in all_scores[diff]): crowns[diff] = 'CLEAR' self.directory_crowns[dir_key] = crowns def _get_tja_files_for_directory(self, directory: Path): """Get TJA files for a specific directory""" if (directory / 'song_list.txt').exists(): return self._read_song_list(directory) else: # For directories with box.def, we want their direct TJA files # Set box_def_dirs_only=False to ensure we get files from this directory return self._find_tja_files_in_directory_only(directory) def _find_tja_files_in_directory_only(self, directory: Path): """Find TJA files only in the specified directory, not recursively in subdirectories with box.def""" tja_files: list[Path] = [] try: for path in directory.iterdir(): if path.is_file() and path.suffix.lower() == ".tja": tja_files.append(path) elif path.is_dir(): # Only recurse into subdirectories that don't have box.def sub_dir_has_box_def = (path / "box.def").exists() if not sub_dir_has_box_def: tja_files.extend(self._find_tja_files_in_directory_only(path)) except (PermissionError, OSError): pass return tja_files def _find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True): tja_files: list[Path] = [] try: has_box_def = (directory / "box.def").exists() # Fixed: Only skip if box_def_dirs_only is True AND has_box_def AND it's not the directory we're currently processing # During object generation, we want to get files from directories with box.def if box_def_dirs_only and has_box_def and directory != self.current_dir: # This logic should only apply during navigation, not during object generation # During object generation, we want to collect all TJA files return [] for path in directory.iterdir(): if path.is_file() and path.suffix.lower() == ".tja": tja_files.append(path) elif path.is_dir(): sub_dir_has_box_def = (path / "box.def").exists() if not sub_dir_has_box_def: tja_files.extend(self._find_tja_files_recursive(path, box_def_dirs_only)) except (PermissionError, OSError): pass return tja_files def _parse_box_def(self, path: Path): """Parse box.def file for directory metadata""" texture_index = 620 name = path.name collection = None try: with open(path / "box.def", 'r', encoding='utf-8') as box_def: for line in box_def: line = line.strip() if line.startswith("#GENRE:"): genre = line.split(":", 1)[1].strip() texture_index = FileSystemItem.GENRE_MAP.get(genre, 620) elif line.startswith("#TITLE:"): name = line.split(":", 1)[1].strip() elif line.startswith("#TITLEJA:"): if global_data.config['general']['language'] == 'ja': name = line.split(":", 1)[1].strip() elif line.startswith("#COLLECTION"): collection = line.split(":", 1)[1].strip() except Exception as e: print(f"Error parsing box.def in {path}: {e}") return name, texture_index, collection def _read_song_list(self, path: Path): """Read and process song_list.txt file""" tja_files: list[Path] = [] updated_lines = [] file_updated = False with open(path / 'song_list.txt', 'r', encoding='utf-8-sig') as song_list: for line in song_list: line = line.strip() if not line: continue parts = line.split('|') if len(parts) < 3: continue hash_val, title, subtitle = parts[0], parts[1], parts[2] original_hash = hash_val if song_hash.song_hashes is not None: if hash_val in song_hash.song_hashes: file_path = Path(song_hash.song_hashes[hash_val]["file_path"]) if file_path.exists(): tja_files.append(file_path) else: # Try to find by title and subtitle for key, value in song_hash.song_hashes.items(): if (value["title"]["en"] == title and value["subtitle"]["en"][2:] == subtitle and Path(value["file_path"]).exists()): hash_val = key tja_files.append(Path(song_hash.song_hashes[hash_val]["file_path"])) break if hash_val != original_hash: file_updated = True updated_lines.append(f"{hash_val}|{title}|{subtitle}") # Write back updated song list if needed if file_updated: with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list: for line in updated_lines: song_list.write(line + '\n') return tja_files def calculate_box_positions(self): """Dynamically calculate box positions based on current selection with wrap-around support""" if not self.items: return for i, item in enumerate(self.items): offset = i - self.selected_index if offset > len(self.items) // 2: offset -= len(self.items) elif offset < -len(self.items) // 2: offset += len(self.items) position = SongSelectScreen.BOX_CENTER + (100 * offset) if position == SongSelectScreen.BOX_CENTER: position += 150 elif position > SongSelectScreen.BOX_CENTER: position += 300 else: position -= 0 if item.box.position == -11111: item.box.position = position item.box.target_position = position else: item.box.target_position = position def set_base_positions(self): """Set initial positions for all items""" self.calculate_box_positions() def load_root_directories(self): """Load the pre-generated root directory items""" self.items = self.root_items.copy() self.in_root_selection = True self.current_dir = Path() self.current_root_dir = Path() # Reset selection self.selected_index = 0 if self.items else -1 self.calculate_box_positions() def load_current_directory(self, selected_item: Optional[Directory]=None): """Load pre-generated items for the current directory""" has_children = any(item.is_dir() and (item / "box.def").exists() for item in self.current_dir.iterdir()) self.genre_bg = None if has_children: self.items = [] if not self.box_open: self.selected_index = 0 dir_key = str(self.current_dir) start_box = None end_box = None # Add back/to_root navigation items if self.current_dir != self.current_root_dir: back_dir = Directory(self.current_dir.parent, "", 552, back=True) if not has_children: start_box = back_dir.box self.items.insert(self.selected_index, back_dir) elif not self.in_root_selection: to_root_dir = Directory(Path(), "", 552, to_root=True) self.items.append(to_root_dir) # Add pre-generated content for this directory if dir_key in self.directory_contents: content_items = self.directory_contents[dir_key] if isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[0]: content_items = self.new_items i = 1 for item in content_items: if isinstance(item, SongFile): if i % 10 == 0 and i != 0: back_dir = Directory(self.current_dir.parent, "", 552, back=True) self.items.insert(self.selected_index+i, back_dir) i += 1 if not has_children: self.items.insert(self.selected_index+i, item) else: self.items.append(item) i += 1 if not has_children: self.box_open = True end_box = content_items[-1].box if selected_item in self.items: self.items.remove(selected_item) # OPTIMIZED: Use cached crowns (calculated on-demand) for item in self.items: if isinstance(item, Directory): item_key = str(item.path) if item_key in self.directory_contents: # Only for real directories item.box.crown = self._get_directory_crowns_cached(item_key) else: # Navigation items (back/to_root) item.box.crown = dict() self.calculate_box_positions() if (not has_children and start_box is not None and end_box is not None and selected_item is not None and selected_item.box.hori_name is not None): self.genre_bg = GenreBG(start_box, end_box, selected_item.box.hori_name) def mark_crowns_dirty_for_song(self, song_file: SongFile): """Mark directories as needing crown recalculation when a song's score changes""" song_path = song_file.path # Find all directories that contain this song and mark them as dirty for dir_key, content_items in self.directory_contents.items(): for item in content_items: if isinstance(item, SongFile) and item.path == song_path: self.crown_cache_dirty.add(dir_key) break def navigate_left(self): """Move selection left with wrap-around""" if self.items: self.selected_index = (self.selected_index - 1) % len(self.items) self.calculate_box_positions() def navigate_right(self): """Move selection right with wrap-around""" if self.items: self.selected_index = (self.selected_index + 1) % len(self.items) self.calculate_box_positions() def select_current_item(self): """Select the currently highlighted item""" if not self.items or self.selected_index >= len(self.items): return selected_item = self.items[self.selected_index] if isinstance(selected_item, Directory): if self.box_open: self.go_back() if selected_item.to_root: self.load_root_directories() else: # Save current state to history if self.current_dir is not None: self.history.append((self.current_dir, self.selected_index, self.in_root_selection, self.current_root_dir)) self.current_dir = selected_item.path if self.in_root_selection: self.current_root_dir = selected_item.path self.in_root_selection = False self.load_current_directory(selected_item=selected_item) elif isinstance(selected_item, SongFile): return selected_item def go_back(self): """Navigate back to the previous directory""" if self.history: previous_dir, previous_index, previous_in_root, previous_root_dir = self.history.pop() self.current_dir = previous_dir self.selected_index = previous_index self.in_root_selection = previous_in_root self.current_root_dir = previous_root_dir if self.in_root_selection: self.load_root_directories() else: self.load_current_directory() self.box_open = False def get_current_item(self): """Get the currently selected item""" if self.items and 0 <= self.selected_index < len(self.items): return self.items[self.selected_index] raise Exception("No current item available") def regenerate_objects(self): """Regenerate all objects (useful if files have changed on disk)""" print("Regenerating all objects...") # Clear existing objects self.all_directories.clear() self.all_song_files.clear() self.directory_contents.clear() self.root_items.clear() self.directory_crowns.clear() # Clear crown cache self.crown_cache_dirty.clear() # Clear dirty flags # Regenerate everything self._generate_all_objects() # Reset navigation state self.current_dir = Path() self.current_root_dir = Path() self.history.clear() self.load_root_directories() def get_stats(self): """Get statistics about the pre-generated objects""" song_count_by_dir = {} for dir_path, items in self.directory_contents.items(): song_count_by_dir[dir_path] = len([item for item in items if isinstance(item, SongFile)]) return { 'total_directories': len(self.all_directories), 'total_songs': len(self.all_song_files), 'root_items': len(self.root_items), 'directories_with_content': len(self.directory_contents), 'songs_by_directory': song_count_by_dir }