Files
PyTaiko/scenes/song_select.py
2025-07-23 18:28:12 -04:00

1549 lines
72 KiB
Python

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
}