import sqlite3 from datetime import datetime, timedelta from pathlib import Path from typing import Optional, Union import pyray as ray 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 def load_navigator(self): 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") 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) def draw_3d(self): pass 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[str] = 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_path = box_texture self.box_texture = None 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 self.tja = tja self.hash = dict() 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() # 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() if self.tja_count is not None and self.tja_count > 0 and self.tja_count_text is None: 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) if self.box_texture is None and self.box_texture_path is not None: self.box_texture = ray.load_texture(self.box_texture_path) 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.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-43 + 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), 126, 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-60, 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) 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.hash = global_data.song_paths[path] 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.hash = global_data.song_hashes[self.hash][0]["diff_hashes"] self.box.get_scores() class FileNavigator: """Manages navigation through pre-generated Directory and SongFile objects""" def __init__(self, root_dirs: list[str]): self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in 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 self.song_count = 0 # 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 = 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: song_obj = SongFile(tja_path, tja_path.name, texture_index) self.song_count += 1 global_data.song_progress = self.song_count / global_data.total_songs 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 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.song_count += 1 global_data.song_progress = self.song_count / global_data.total_songs 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 hash_val in global_data.song_hashes: file_path = Path(global_data.song_hashes[hash_val][0]["file_path"]) if file_path.exists(): tja_files.append(file_path) else: # Try to find by title and subtitle for key, value in global_data.song_hashes.items(): for i in range(len(value)): song = value[i] if (song["title"]["en"] == title and song["subtitle"]["en"][2:] == subtitle and Path(song["file_path"]).exists()): hash_val = key tja_files.append(Path(global_data.song_hashes[hash_val][i]["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()