add difficulty sorting menu

This commit is contained in:
Yonokid
2025-08-13 02:14:02 -04:00
parent f27dfc023b
commit d4b90436be
6 changed files with 379 additions and 82 deletions

View File

@@ -828,7 +828,7 @@ class AudioEngine:
def set_sound_volume(self, sound: str, volume: float) -> None: def set_sound_volume(self, sound: str, volume: float) -> None:
if sound in self.sounds: if sound in self.sounds:
self.sounds[sound].volume = max(0.0, min(1.0, volume)) self.sounds[sound].volume = max(0.0, volume)
def set_sound_pan(self, sound: str, pan: float) -> None: def set_sound_pan(self, sound: str, pan: float) -> None:
if sound in self.sounds: if sound in self.sounds:

View File

@@ -1,5 +1,6 @@
import csv import csv
import json import json
import sqlite3
import sys import sys
import time import time
from collections import deque from collections import deque
@@ -28,7 +29,6 @@ def build_song_hashes(output_dir=Path("cache")):
output_dir.mkdir() output_dir.mkdir()
song_hashes: dict[str, list[dict]] = dict() song_hashes: dict[str, list[dict]] = dict()
path_to_hash: dict[str, str] = dict() # New index for O(1) path lookups path_to_hash: dict[str, str] = dict() # New index for O(1) path lookups
output_path = Path(output_dir / "song_hashes.json") output_path = Path(output_dir / "song_hashes.json")
index_path = Path(output_dir / "path_to_hash.json") index_path = Path(output_dir / "path_to_hash.json")
@@ -36,7 +36,6 @@ def build_song_hashes(output_dir=Path("cache")):
if output_path.exists(): if output_path.exists():
with open(output_path, "r", encoding="utf-8") as f: with open(output_path, "r", encoding="utf-8") as f:
song_hashes = json.load(f, cls=DiffHashesDecoder) song_hashes = json.load(f, cls=DiffHashesDecoder)
if index_path.exists(): if index_path.exists():
with open(index_path, "r", encoding="utf-8") as f: with open(index_path, "r", encoding="utf-8") as f:
path_to_hash = json.load(f) path_to_hash = json.load(f)
@@ -59,21 +58,17 @@ def build_song_hashes(output_dir=Path("cache")):
all_tja_files.extend(root_path.rglob("*.tja")) all_tja_files.extend(root_path.rglob("*.tja"))
global_data.total_songs = len(all_tja_files) global_data.total_songs = len(all_tja_files)
files_to_process = [] files_to_process = []
for tja_path in all_tja_files: for tja_path in all_tja_files:
tja_path_str = str(tja_path) tja_path_str = str(tja_path)
current_modified = tja_path.stat().st_mtime current_modified = tja_path.stat().st_mtime
if current_modified <= saved_timestamp: if current_modified <= saved_timestamp:
current_hash = path_to_hash.get(tja_path_str) current_hash = path_to_hash.get(tja_path_str)
if current_hash is not None: if current_hash is not None:
global_data.song_paths[tja_path] = current_hash global_data.song_paths[tja_path] = current_hash
continue continue
current_hash = path_to_hash.get(tja_path_str) current_hash = path_to_hash.get(tja_path_str)
if current_hash is None: if current_hash is None:
files_to_process.append(tja_path) files_to_process.append(tja_path)
else: else:
@@ -82,15 +77,19 @@ def build_song_hashes(output_dir=Path("cache")):
del song_hashes[current_hash] del song_hashes[current_hash]
del path_to_hash[tja_path_str] del path_to_hash[tja_path_str]
# Prepare database connection for updates
db_path = Path("scores.db")
db_updates = [] # Store updates to batch process later
# Process only files that need updating # Process only files that need updating
song_count = 0 song_count = 0
total_songs = len(files_to_process) total_songs = len(files_to_process)
if total_songs > 0: if total_songs > 0:
global_data.total_songs = total_songs global_data.total_songs = total_songs
for tja_path in files_to_process: for tja_path in files_to_process:
tja_path_str = str(tja_path) tja_path_str = str(tja_path)
current_modified = tja_path.stat().st_mtime current_modified = tja_path.stat().st_mtime
tja = TJAParser(tja_path) tja = TJAParser(tja_path)
all_notes = deque() all_notes = deque()
all_bars = deque() all_bars = deque()
@@ -106,7 +105,6 @@ def build_song_hashes(output_dir=Path("cache")):
continue continue
hash_val = tja.hash_note_data(all_notes, all_bars) hash_val = tja.hash_note_data(all_notes, all_bars)
if hash_val not in song_hashes: if hash_val not in song_hashes:
song_hashes[hash_val] = [] song_hashes[hash_val] = []
@@ -121,16 +119,48 @@ def build_song_hashes(output_dir=Path("cache")):
# Update both indexes # Update both indexes
path_to_hash[tja_path_str] = hash_val path_to_hash[tja_path_str] = hash_val
global_data.song_paths[tja_path] = hash_val global_data.song_paths[tja_path] = hash_val
# Prepare database updates for each difficulty
en_name = tja.metadata.title.get('en', '') if isinstance(tja.metadata.title, dict) else str(tja.metadata.title)
jp_name = tja.metadata.title.get('jp', '') if isinstance(tja.metadata.title, dict) else ''
for diff, diff_hash in diff_hashes.items():
db_updates.append((diff_hash, en_name, jp_name, diff))
song_count += 1 song_count += 1
global_data.song_progress = song_count / total_songs global_data.song_progress = song_count / total_songs
# Update database with new difficulty hashes
if db_updates and db_path.exists():
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
for diff_hash, en_name, jp_name, diff in db_updates:
# Update existing entries that match by name and difficulty
cursor.execute("""
UPDATE scores
SET hash = ?
WHERE (en_name = ? AND jp_name = ?) AND diff = ?
""", (diff_hash, en_name, jp_name, diff))
if cursor.rowcount > 0:
print(f"Updated {cursor.rowcount} entries for {en_name} ({diff})")
conn.commit()
conn.close()
print(f"Database update completed. Processed {len(db_updates)} difficulty hash updates.")
except sqlite3.Error as e:
print(f"Database error: {e}")
except Exception as e:
print(f"Error updating database: {e}")
elif db_updates:
print(f"Warning: scores.db not found, skipping {len(db_updates)} database updates")
# Save both files # Save both files
with open(output_path, "w", encoding="utf-8") as f: with open(output_path, "w", encoding="utf-8") as f:
json.dump(song_hashes, f, indent=2, ensure_ascii=False) json.dump(song_hashes, f, indent=2, ensure_ascii=False)
with open(index_path, "w", encoding="utf-8") as f: with open(index_path, "w", encoding="utf-8") as f:
json.dump(path_to_hash, f, indent=2, ensure_ascii=False) json.dump(path_to_hash, f, indent=2, ensure_ascii=False)
with open(output_dir / 'timestamp.txt', 'w') as f: with open(output_dir / 'timestamp.txt', 'w') as f:
f.write(str(current_timestamp)) f.write(str(current_timestamp))

View File

@@ -40,11 +40,13 @@ class Note:
return self.hit_ms == other.hit_ms return self.hit_ms == other.hit_ms
def _get_hash_data(self) -> bytes: def _get_hash_data(self) -> bytes:
"""Get deterministic byte representation for hashing""" hash_fields = ['type', 'hit_ms', 'load_ms']
field_values = [] field_values = []
for f in sorted([f.name for f in fields(self)]):
value = getattr(self, f, None) for field_name in sorted(hash_fields):
field_values.append((f, value)) value = getattr(self, field_name, None)
field_values.append((field_name, value))
field_values.append(('__class__', self.__class__.__name__)) field_values.append(('__class__', self.__class__.__name__))
hash_string = str(field_values) hash_string = str(field_values)
return hash_string.encode('utf-8') return hash_string.encode('utf-8')

View File

@@ -240,6 +240,7 @@ for file in Path('cache/image').iterdir():
class OutlinedText: class OutlinedText:
def __init__(self, text: str, font_size: int, color: ray.Color, outline_color: ray.Color, outline_thickness=5.0, vertical=False): def __init__(self, text: str, font_size: int, color: ray.Color, outline_color: ray.Color, outline_thickness=5.0, vertical=False):
self.text = text
self.hash = self._hash_text(text, font_size, color, vertical) self.hash = self._hash_text(text, font_size, color, vertical)
if self.hash in text_cache: if self.hash in text_cache:
self.texture = ray.load_texture(f'cache/image/{self.hash}.png') self.texture = ray.load_texture(f'cache/image/{self.hash}.png')

View File

@@ -60,8 +60,8 @@ class GameScreen:
session_data.song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) session_data.song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])
if not hasattr(self, 'song_music'): if not hasattr(self, 'song_music'):
if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file(): if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file():
self.song_music = audio.load_sound(self.tja.metadata.wave) self.song_music = audio.load_music_stream(self.tja.metadata.wave)
audio.normalize_sound(self.song_music, 0.1935) audio.normalize_music_stream(self.song_music, 0.1935)
else: else:
self.song_music = None self.song_music = None
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
@@ -88,7 +88,7 @@ class GameScreen:
self.screen_init = False self.screen_init = False
tex.unload_textures() tex.unload_textures()
if self.song_music is not None: if self.song_music is not None:
audio.unload_sound(self.song_music) audio.unload_music_stream(self.song_music)
del self.song_music del self.song_music
self.song_started = False self.song_started = False
self.end_ms = 0 self.end_ms = 0
@@ -132,8 +132,8 @@ class GameScreen:
if self.tja is not None: if self.tja is not None:
if (self.current_ms >= self.tja.metadata.offset*1000 + self.start_delay - global_data.config["general"]["judge_offset"]) and not self.song_started: if (self.current_ms >= self.tja.metadata.offset*1000 + self.start_delay - global_data.config["general"]["judge_offset"]) and not self.song_started:
if self.song_music is not None: if self.song_music is not None:
if not audio.is_sound_playing(self.song_music): if not audio.is_music_stream_playing(self.song_music):
audio.play_sound(self.song_music) audio.play_music_stream(self.song_music)
print(f"Song started at {self.current_ms}") print(f"Song started at {self.current_ms}")
if self.movie is not None: if self.movie is not None:
self.movie.start(get_current_ms()) self.movie.start(get_current_ms())
@@ -146,6 +146,9 @@ class GameScreen:
self.player_1.update(self) self.player_1.update(self)
self.song_info.update(get_current_ms()) self.song_info.update(get_current_ms())
if self.song_music is not None:
audio.update_music_stream(self.song_music)
self.result_transition.update(get_current_ms()) self.result_transition.update(get_current_ms())
if self.result_transition.is_finished: if self.result_transition.is_finished:
return self.on_screen_end('RESULT') return self.on_screen_end('RESULT')
@@ -162,13 +165,15 @@ class GameScreen:
self.end_ms = get_current_ms() self.end_ms = get_current_ms()
if ray.is_key_pressed(ray.KeyboardKey.KEY_F1): if ray.is_key_pressed(ray.KeyboardKey.KEY_F1):
audio.stop_sound(self.song_music) if self.song_music is not None:
audio.stop_music_stream(self.song_music)
self.init_tja(global_data.selected_song, session_data.selected_difficulty) self.init_tja(global_data.selected_song, session_data.selected_difficulty)
audio.play_sound(self.sound_restart) audio.play_sound(self.sound_restart)
self.song_started = False self.song_started = False
if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE):
audio.stop_sound(self.song_music) if self.song_music is not None:
audio.stop_music_stream(self.song_music)
return self.on_screen_end('SONG_SELECT') return self.on_screen_end('SONG_SELECT')
def draw(self): def draw(self):
@@ -613,7 +618,7 @@ class Player:
self.draw_balloon(game_screen, note, current_eighth) self.draw_balloon(game_screen, note, current_eighth)
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position) tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position)
else: else:
tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+192) tex.draw_texture('notes', str(note.type), frame=current_eighth % 2, x=x_position, y=y_position+192, center=True)
tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position) tex.draw_texture('notes', 'moji', frame=note.moji, x=x_position - (168//2) + 64, y=323 + y_position)
def draw(self, game_screen: GameScreen): def draw(self, game_screen: GameScreen):

View File

@@ -1,3 +1,4 @@
import random
import sqlite3 import sqlite3
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
@@ -25,6 +26,7 @@ from libs.utils import (
class State: class State:
BROWSING = 0 BROWSING = 0
SONG_SELECTED = 1 SONG_SELECTED = 1
DIFF_SORTING = 2
class SongSelectScreen: class SongSelectScreen:
BOX_CENTER = 444 BOX_CENTER = 444
@@ -43,37 +45,43 @@ class SongSelectScreen:
self.sound_kat = audio.load_sound(sounds_dir / "inst_00_katsu.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_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') self.sound_ura_switch = audio.load_sound(sounds_dir / 'song_select' / 'SE_SELECT [4].ogg')
self.sound_add_favorite = audio.load_sound(sounds_dir / 'song_select' / 'add_favorite.ogg')
audio.set_sound_volume(self.sound_ura_switch, 0.25) audio.set_sound_volume(self.sound_ura_switch, 0.25)
audio.set_sound_volume(self.sound_add_favorite, 3.0)
self.sound_bgm = audio.load_sound(sounds_dir / "song_select" / "JINGLE_GENRE [1].ogg") self.sound_bgm = audio.load_sound(sounds_dir / "song_select" / "JINGLE_GENRE [1].ogg")
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: if not self.screen_init:
tex.load_screen_textures('song_select') tex.load_screen_textures('song_select')
self.load_sounds() self.load_sounds()
self.selected_song = None
self.selected_difficulty = -1
self.background_move = tex.get_animation(0) self.background_move = tex.get_animation(0)
self.background_move.start()
self.move_away = tex.get_animation(1) self.move_away = tex.get_animation(1)
self.diff_fade_out = tex.get_animation(2) self.diff_fade_out = tex.get_animation(2)
self.state = State.BROWSING
self.text_fade_out = tex.get_animation(3) self.text_fade_out = tex.get_animation(3)
self.text_fade_in = tex.get_animation(4) self.text_fade_in = tex.get_animation(4)
self.background_fade_change = tex.get_animation(5) self.background_fade_change = tex.get_animation(5)
self.background_move.start()
self.state = State.BROWSING
self.selected_difficulty = -1
self.selected_song = None
self.game_transition = None self.game_transition = None
self.demo_song = None
self.diff_sort_selector = None
self.texture_index = 9 self.texture_index = 9
self.last_texture_index = 9 self.last_texture_index = 9
self.demo_song = None
self.navigator.reset_items()
self.navigator.get_current_item().box.get_scores()
self.screen_init = True
self.last_moved = get_current_ms() self.last_moved = get_current_ms()
self.ura_toggle = 0 self.ura_toggle = 0
self.ura_switch_animation = UraSwitchAnimation()
self.is_ura = False self.is_ura = False
self.screen_init = True
self.ura_switch_animation = UraSwitchAnimation()
if str(global_data.selected_song) in self.navigator.all_song_files: 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)]) self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(global_data.selected_song)])
self.navigator.reset_items()
self.navigator.get_current_item().box.get_scores()
self.navigator.add_recent()
def on_screen_end(self, next_screen): def on_screen_end(self, next_screen):
self.screen_init = False self.screen_init = False
global_data.selected_song = self.navigator.get_current_item().path global_data.selected_song = self.navigator.get_current_item().path
@@ -124,6 +132,9 @@ class SongSelectScreen:
if selected_item is not None and selected_item.box.is_back: if selected_item is not None and selected_item.box.is_back:
self.navigator.go_back() self.navigator.go_back()
#audio.play_sound(self.sound_cancel) #audio.play_sound(self.sound_cancel)
elif isinstance(selected_item, Directory) and selected_item.collection == Directory.COLLECTIONS[3]:
self.state = State.DIFF_SORTING
self.diff_sort_selector = DiffSortSelect()
else: else:
selected_song = self.navigator.select_current_item() selected_song = self.navigator.select_current_item()
if selected_song: if selected_song:
@@ -136,6 +147,11 @@ class SongSelectScreen:
self.text_fade_out.start() self.text_fade_out.start()
self.text_fade_in.start() self.text_fade_in.start()
if ray.is_key_pressed(ray.KeyboardKey.KEY_SPACE):
success = self.navigator.add_favorite()
if success:
audio.play_sound(self.sound_add_favorite)
def handle_input_selected(self): def handle_input_selected(self):
# Handle song selection confirmation or cancel # Handle song selection confirmation or cancel
if is_l_don_pressed() or is_r_don_pressed(): if is_l_don_pressed() or is_r_don_pressed():
@@ -164,6 +180,34 @@ class SongSelectScreen:
self.selected_difficulty in [3, 4]): self.selected_difficulty in [3, 4]):
self._toggle_ura_mode() self._toggle_ura_mode()
def handle_input_diff_sort(self):
if self.diff_sort_selector is None:
raise Exception("Diff sort selector was not able to be created")
if is_l_kat_pressed():
self.diff_sort_selector.input_left()
audio.play_sound(self.sound_kat)
if is_r_kat_pressed():
self.diff_sort_selector.input_right()
audio.play_sound(self.sound_kat)
if is_l_don_pressed() or is_r_don_pressed():
diff, level = self.diff_sort_selector.input_select()
if diff == -2:
audio.play_sound(self.sound_don)
return
elif diff == -1:
self.diff_sort_selector = None
self.state = State.BROWSING
if level == 0:
audio.play_sound(self.sound_don)
self.navigator.select_current_item()
else:
self.diff_sort_selector = None
self.state = State.BROWSING
audio.play_sound(self.sound_don)
self.navigator.diff_sort_diff = diff
self.navigator.diff_sort_level = level
self.navigator.select_current_item()
def _cancel_selection(self): def _cancel_selection(self):
"""Reset to browsing state""" """Reset to browsing state"""
self.selected_song = None self.selected_song = None
@@ -229,6 +273,8 @@ class SongSelectScreen:
self.handle_input_browsing() self.handle_input_browsing()
elif self.state == State.SONG_SELECTED: elif self.state == State.SONG_SELECTED:
self.handle_input_selected() self.handle_input_selected()
elif self.state == State.DIFF_SORTING:
self.handle_input_diff_sort()
def update(self): def update(self):
self.on_screen_start() self.on_screen_start()
@@ -266,6 +312,9 @@ class SongSelectScreen:
if self.navigator.genre_bg is not None: if self.navigator.genre_bg is not None:
self.navigator.genre_bg.update(get_current_ms()) self.navigator.genre_bg.update(get_current_ms())
if self.diff_sort_selector is not None:
self.diff_sort_selector.update(get_current_ms())
for song in self.navigator.items: for song in self.navigator.items:
song.box.update(self.state == State.SONG_SELECTED) song.box.update(self.state == State.SONG_SELECTED)
song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150 song.box.is_open = song.box.position == SongSelectScreen.BOX_CENTER + 150
@@ -311,13 +360,19 @@ class SongSelectScreen:
self.ura_switch_animation.draw() self.ura_switch_animation.draw()
if self.selected_song and self.state == State.SONG_SELECTED: tex.draw_texture('global', 'footer')
if self.diff_sort_selector is not None:
self.diff_sort_selector.draw()
if (self.selected_song and self.state == State.SONG_SELECTED):
self.draw_selector() self.draw_selector()
tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute) tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute)
elif self.state == State.DIFF_SORTING:
tex.draw_texture('global', 'difficulty_select', fade=self.text_fade_in.attribute)
else: else:
tex.draw_texture('global', 'song_select', fade=self.text_fade_out.attribute) tex.draw_texture('global', 'song_select', fade=self.text_fade_out.attribute)
tex.draw_texture('global', 'footer')
if self.game_transition is not None: if self.game_transition is not None:
self.game_transition.draw() self.game_transition.draw()
@@ -335,6 +390,11 @@ class SongBox:
6: ray.Color(134, 88, 0, 255), 6: ray.Color(134, 88, 0, 255),
7: ray.Color(79, 40, 134, 255), 7: ray.Color(79, 40, 134, 255),
8: ray.Color(148, 24, 0, 255), 8: ray.Color(148, 24, 0, 255),
9: ray.Color(101, 0, 82, 255),
10: ray.Color(140, 39, 92, 255),
11: ray.Color(151, 57, 30, 255),
12: ray.Color(35, 123, 103, 255),
13: ray.Color(25, 68, 137, 255),
14: ray.Color(157, 13, 31, 255) 14: ray.Color(157, 13, 31, 255)
} }
def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None, def __init__(self, name: str, texture_index: int, is_dir: bool, tja: Optional[TJAParser] = None,
@@ -461,11 +521,13 @@ class SongBox:
def _draw_closed(self, x: int, y: int): def _draw_closed(self, x: int, y: int):
tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x) tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x)
offset = 1 if self.texture_index == 3 or self.texture_index >= 9 else 0 offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0
tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset) tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x, x2=32, y=offset)
tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x)
if self.texture_index == 9: if self.texture_index == 9:
tex.draw_texture('box', 'genre_overlay', x=x, y=y) tex.draw_texture('box', 'genre_overlay', x=x, y=y)
elif self.texture_index == 14:
tex.draw_texture('box', 'diff_overlay', x=x, y=y)
if not self.is_back and self.is_dir: if not self.is_back and self.is_dir:
tex.draw_texture('box', 'folder_clip', frame=self.texture_index, x=x - (1 - offset), y=y) tex.draw_texture('box', 'folder_clip', frame=self.texture_index, x=x - (1 - offset), y=y)
@@ -505,12 +567,14 @@ class SongBox:
self.hori_name.draw(self.hori_name.default_src, dest, ray.Vector2(0, 0), 0, color) self.hori_name.draw(self.hori_name.default_src, dest, ray.Vector2(0, 0), 0, color)
tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x - self.open_anim.attribute) tex.draw_texture('box', 'folder_texture_left', frame=self.texture_index, x=x - self.open_anim.attribute)
offset = 1 if self.texture_index == 3 or self.texture_index >= 9 else 0 offset = 1 if self.texture_index == 3 or self.texture_index >= 9 and self.texture_index not in {10,11,12} else 0
tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x - self.open_anim.attribute, y=offset, x2=324) tex.draw_texture('box', 'folder_texture', frame=self.texture_index, x=x - self.open_anim.attribute, y=offset, x2=324)
tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x + self.open_anim.attribute) tex.draw_texture('box', 'folder_texture_right', frame=self.texture_index, x=x + self.open_anim.attribute)
if self.texture_index == 9: if self.texture_index == 9:
tex.draw_texture('box', 'genre_overlay_large', x=x, y=y, color=color) tex.draw_texture('box', 'genre_overlay_large', x=x, y=y, color=color)
elif self.texture_index == 14:
tex.draw_texture('box', 'diff_overlay_large', x=x, y=y, color=color)
color = ray.WHITE color = ray.WHITE
if fade_override is not None: if fade_override is not None:
color = ray.fade(ray.WHITE, min(0.5, fade_override)) color = ray.fade(ray.WHITE, min(0.5, fade_override))
@@ -780,6 +844,122 @@ class UraSwitchAnimation:
def draw(self): def draw(self):
tex.draw_texture('diff_select', 'ura_switch', frame=self.texture_change.attribute, fade=self.fade_out.attribute) tex.draw_texture('diff_select', 'ura_switch', frame=self.texture_change.attribute, fade=self.fade_out.attribute)
class DiffSortSelect:
def __init__(self):
self.selected_box = -1
self.selected_level = 1
self.in_level_select = False
self.confirmation = False
self.confirm_index = 1
self.num_boxes = 6
self.limits = [5, 7, 8, 10]
def update(self, current_ms):
pass
def get_random_sort(self):
diff = random.randint(0, 4)
if diff == 0:
level = random.randint(1, 5)
elif diff == 1:
level = random.randint(1, 7)
elif diff == 2:
level = random.randint(1, 8)
elif diff == 3:
level = random.randint(1, 10)
else:
level = random.choice([1, 5, 6, 7, 8, 9, 10])
return diff, level
def input_select(self):
if self.confirmation:
if self.confirm_index == 0:
self.confirmation = False
return (-2, -1)
elif self.confirm_index == 1:
return self.selected_box, self.selected_level
elif self.confirm_index == 2:
self.confirmation = False
self.in_level_select = False
return (-2, -1)
elif self.in_level_select:
self.confirmation = True
self.confirm_index = 1
return (-2, -1)
if self.selected_box == -1:
return (-1, -1)
elif self.selected_box == 5:
return (-1, 0)
elif self.selected_box == 4:
return self.get_random_sort()
self.in_level_select = True
self.selected_level = min(self.selected_level, self.limits[self.selected_box])
return (-2, -1)
def input_left(self):
if self.confirmation:
self.confirm_index = max(self.confirm_index - 1, 0)
elif self.in_level_select:
self.selected_level = max(self.selected_level - 1, 1)
else:
self.selected_box = max(self.selected_box - 1, -1)
def input_right(self):
if self.confirmation:
self.confirm_index = min(self.confirm_index + 1, 2)
elif self.in_level_select:
self.selected_level = min(self.selected_level + 1, self.limits[self.selected_box])
else:
self.selected_box = min(self.selected_box + 1, self.num_boxes - 1)
def draw_diff_select(self):
tex.draw_texture('diff_sort', 'background')
tex.draw_texture('diff_sort', 'back')
for i in range(self.num_boxes):
if i == self.selected_box:
tex.draw_texture('diff_sort', 'box_highlight', x=(100*i))
tex.draw_texture('diff_sort', 'box_text_highlight', x=(100*i), frame=i)
else:
tex.draw_texture('diff_sort', 'box', x=(100*i))
tex.draw_texture('diff_sort', 'box_text', x=(100*i), frame=i)
if self.selected_box == -1:
tex.draw_texture('diff_sort', 'back_outline')
else:
tex.draw_texture('diff_sort', 'box_outline', x=(100*self.selected_box))
for i in range(self.num_boxes):
if i < 4:
tex.draw_texture('diff_sort', 'box_diff', x=(100*i), frame=i)
def draw_level_select(self):
tex.draw_texture('diff_sort', 'background')
tex.draw_texture('diff_sort', 'star_select_text')
tex.draw_texture('diff_sort', 'star_limit', frame=self.selected_box)
tex.draw_texture('diff_sort', 'level_box')
tex.draw_texture('diff_sort', 'diff', frame=self.selected_box)
tex.draw_texture('diff_sort', 'star_num', frame=self.selected_level)
for i in range(self.selected_level):
tex.draw_texture('diff_sort', 'star', x=(i*40.5))
if self.confirmation:
for i in range(3):
if i == self.confirm_index:
tex.draw_texture('diff_sort', 'small_box_highlight', x=(i*245))
tex.draw_texture('diff_sort', 'small_box_text_highlight', x=(i*245), frame=i)
else:
tex.draw_texture('diff_sort', 'small_box', x=(i*245))
tex.draw_texture('diff_sort', 'small_box_text', x=(i*245), frame=i)
else:
tex.draw_texture('diff_sort', 'pongos')
def draw(self):
ray.draw_rectangle(0, 0, 1280, 720, ray.fade(ray.BLACK, 0.6))
if self.in_level_select:
self.draw_level_select()
else:
self.draw_diff_select()
class FileSystemItem: class FileSystemItem:
GENRE_MAP = { GENRE_MAP = {
'J-POP': 1, 'J-POP': 1,
@@ -790,11 +970,15 @@ class FileSystemItem:
'クラシック': 6, 'クラシック': 6,
'ゲームミュージック': 7, 'ゲームミュージック': 7,
'ナムコオリジナル': 8, 'ナムコオリジナル': 8,
'RECOMMENDED': 10,
'FAVORITE': 11,
'RECENT': 12,
'DIFFICULTY': 14 'DIFFICULTY': 14
} }
"""Base class for files and directories in the navigation system""" """Base class for files and directories in the navigation system"""
def __init__(self, path: Path, name: str): def __init__(self, path: Path, name: str):
self.path = path self.path = path
self.name = name
class Directory(FileSystemItem): class Directory(FileSystemItem):
"""Represents a directory in the navigation system""" """Represents a directory in the navigation system"""
@@ -856,7 +1040,12 @@ class FileNavigator:
self.current_root_dir = Path() self.current_root_dir = Path()
self.items: list[Directory | SongFile] = [] self.items: list[Directory | SongFile] = []
self.new_items: list[Directory | SongFile] = [] self.new_items: list[Directory | SongFile] = []
self.favorite_folder: Optional[Directory] = None
self.in_favorites = False
self.recent_folder: Optional[Directory] = None
self.selected_index = 0 self.selected_index = 0
self.diff_sort_diff = 4
self.diff_sort_level = 10
self.history = [] self.history = []
self.box_open = False self.box_open = False
self.genre_bg = None self.genre_bg = None
@@ -915,6 +1104,10 @@ class FileNavigator:
box_texture=box_texture, box_texture=box_texture,
collection=collection collection=collection
) )
if directory_obj.collection == Directory.COLLECTIONS[2]:
self.favorite_folder = directory_obj
elif directory_obj.collection == Directory.COLLECTIONS[1]:
self.recent_folder = directory_obj
self.all_directories[dir_key] = directory_obj self.all_directories[dir_key] = directory_obj
# Generate content list for this directory # Generate content list for this directory
@@ -979,7 +1172,7 @@ class FileNavigator:
self.root_items.append(self.all_song_files[song_key]) self.root_items.append(self.all_song_files[song_key])
def _count_tja_files(self, folder_path: Path): def _count_tja_files(self, folder_path: Path):
"""Count TJA files in directory (matching original logic)""" """Count TJA files in directory"""
tja_count = 0 tja_count = 0
# Find all song_list.txt files recursively # Find all song_list.txt files recursively
@@ -988,16 +1181,10 @@ class FileNavigator:
if song_list_files: if song_list_files:
# Process all song_list.txt files found # Process all song_list.txt files found
for song_list_path in song_list_files: for song_list_path in song_list_files:
try: with open(song_list_path, 'r', encoding='utf-8-sig') as song_list_file:
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()])
tja_count += len([line for line in song_list_file.readlines() if line.strip()]) # Fallback: Use recursive counting of .tja files
except (IOError, UnicodeDecodeError) as e: tja_count += sum(1 for _ in folder_path.rglob("*.tja"))
# 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 return tja_count
@@ -1046,41 +1233,35 @@ class FileNavigator:
"""Find TJA files only in the specified directory, not recursively in subdirectories with box.def""" """Find TJA files only in the specified directory, not recursively in subdirectories with box.def"""
tja_files: list[Path] = [] tja_files: list[Path] = []
try: for path in directory.iterdir():
for path in directory.iterdir(): if path.is_file() and path.suffix.lower() == ".tja":
if path.is_file() and path.suffix.lower() == ".tja": tja_files.append(path)
tja_files.append(path) elif path.is_dir():
elif path.is_dir(): # Only recurse into subdirectories that don't have box.def
# Only recurse into subdirectories that don't have box.def sub_dir_has_box_def = (path / "box.def").exists()
sub_dir_has_box_def = (path / "box.def").exists() if not sub_dir_has_box_def:
if not sub_dir_has_box_def: tja_files.extend(self._find_tja_files_in_directory_only(path))
tja_files.extend(self._find_tja_files_in_directory_only(path))
except (PermissionError, OSError):
pass
return tja_files return tja_files
def _find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True): def _find_tja_files_recursive(self, directory: Path, box_def_dirs_only=True):
tja_files: list[Path] = [] tja_files: list[Path] = []
try: has_box_def = (directory / "box.def").exists()
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
# 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
# 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:
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
# This logic should only apply during navigation, not during object generation # During object generation, we want to collect all TJA files
# During object generation, we want to collect all TJA files return []
return []
for path in directory.iterdir(): for path in directory.iterdir():
if path.is_file() and path.suffix.lower() == ".tja": if path.is_file() and path.suffix.lower() == ".tja":
tja_files.append(path) tja_files.append(path)
elif path.is_dir(): elif path.is_dir():
sub_dir_has_box_def = (path / "box.def").exists() sub_dir_has_box_def = (path / "box.def").exists()
if not sub_dir_has_box_def: if not sub_dir_has_box_def:
tja_files.extend(self._find_tja_files_recursive(path, box_def_dirs_only)) tja_files.extend(self._find_tja_files_recursive(path, box_def_dirs_only))
except (PermissionError, OSError):
pass
return tja_files return tja_files
@@ -1137,7 +1318,7 @@ class FileNavigator:
for i in range(len(value)): for i in range(len(value)):
song = value[i] song = value[i]
if (song["title"]["en"] == title and if (song["title"]["en"] == title and
song["subtitle"]["en"][2:] == subtitle and song["subtitle"]["en"] == subtitle and
Path(song["file_path"]).exists()): Path(song["file_path"]).exists()):
hash_val = key hash_val = key
tja_files.append(Path(global_data.song_hashes[hash_val][i]["file_path"])) tja_files.append(Path(global_data.song_hashes[hash_val][i]["file_path"]))
@@ -1151,6 +1332,7 @@ class FileNavigator:
if file_updated: if file_updated:
with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list: with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list:
for line in updated_lines: for line in updated_lines:
print("updated", line)
song_list.write(line + '\n') song_list.write(line + '\n')
return tja_files return tja_files
@@ -1197,6 +1379,7 @@ class FileNavigator:
"""Load pre-generated items for the current directory""" """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()) has_children = any(item.is_dir() and (item / "box.def").exists() for item in self.current_dir.iterdir())
self.genre_bg = None self.genre_bg = None
self.in_favorites = False
if has_children: if has_children:
self.items = [] self.items = []
if not self.box_open: if not self.box_open:
@@ -1222,6 +1405,21 @@ class FileNavigator:
if isinstance(selected_item, Directory): if isinstance(selected_item, Directory):
if selected_item.collection == Directory.COLLECTIONS[0]: if selected_item.collection == Directory.COLLECTIONS[0]:
content_items = self.new_items content_items = self.new_items
elif selected_item.collection == Directory.COLLECTIONS[1]:
if self.recent_folder is None:
raise Exception("tried to enter recent folder without recents")
self._generate_objects_recursive(self.recent_folder.path)
selected_item.box.tja_count_text = None
selected_item.box.tja_count = self._count_tja_files(self.recent_folder.path)
content_items = self.directory_contents[dir_key]
elif selected_item.collection == Directory.COLLECTIONS[2]:
if self.favorite_folder is None:
raise Exception("tried to enter favorite folder without favorites")
self._generate_objects_recursive(self.favorite_folder.path)
selected_item.box.tja_count_text = None
selected_item.box.tja_count = self._count_tja_files(self.favorite_folder.path)
content_items = self.directory_contents[dir_key]
self.in_favorites = True
elif selected_item.collection == Directory.COLLECTIONS[3]: elif selected_item.collection == Directory.COLLECTIONS[3]:
content_items = [] content_items = []
parent_dir = selected_item.path.parent parent_dir = selected_item.path.parent
@@ -1230,11 +1428,20 @@ class FileNavigator:
sibling_key = str(sibling_path) sibling_key = str(sibling_path)
if sibling_key in self.directory_contents: if sibling_key in self.directory_contents:
for item in self.directory_contents[sibling_key]: for item in self.directory_contents[sibling_key]:
if isinstance(item, SongFile): if isinstance(item, SongFile) and item:
if 3 in item.tja.metadata.course_data and item.tja.metadata.course_data[3].level == 10: if self.diff_sort_diff in item.tja.metadata.course_data and item.tja.metadata.course_data[self.diff_sort_diff].level == self.diff_sort_level:
item.box.texture_index = 14 if item not in content_items:
content_items.append(item) content_items.append(item)
elif selected_item.collection == Directory.COLLECTIONS[4]:
parent_dir = selected_item.path.parent
temp_items = []
for sibling_path in parent_dir.iterdir():
if sibling_path.is_dir() and sibling_path != selected_item.path:
sibling_key = str(sibling_path)
if sibling_key in self.directory_contents:
for item in self.directory_contents[sibling_key]:
temp_items.append(item)
content_items = random.sample(temp_items, 10)
i = 1 i = 1
for item in content_items: for item in content_items:
if isinstance(item, SongFile): if isinstance(item, SongFile):
@@ -1242,8 +1449,9 @@ class FileNavigator:
back_dir = Directory(self.current_dir.parent, "", 17, back=True) back_dir = Directory(self.current_dir.parent, "", 17, back=True)
self.items.insert(self.selected_index+i, back_dir) self.items.insert(self.selected_index+i, back_dir)
i += 1 i += 1
if not has_children: if not has_children:
if selected_item is not None:
item.box.texture_index = selected_item.box.texture_index
self.items.insert(self.selected_index+i, item) self.items.insert(self.selected_index+i, item)
else: else:
self.items.append(item) self.items.append(item)
@@ -1341,3 +1549,54 @@ class FileNavigator:
def reset_items(self): def reset_items(self):
for item in self.items: for item in self.items:
item.box.reset() item.box.reset()
def add_recent(self):
song = self.get_current_item()
if isinstance(song, Directory):
return
if self.recent_folder is None:
return
recents_path = self.recent_folder.path / 'song_list.txt'
new_entry = f'{song.hash}|{song.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n'
existing_entries = []
if recents_path.exists():
with open(recents_path, 'r', encoding='utf-8-sig') as song_list:
existing_entries = song_list.readlines()
existing_entries = [entry for entry in existing_entries if not entry.startswith(f'{song.hash}|')]
all_entries = [new_entry] + existing_entries
recent_entries = all_entries[:25]
with open(recents_path, 'w', encoding='utf-8-sig') as song_list:
song_list.writelines(recent_entries)
print("Added recent: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en'])
def add_favorite(self) -> bool:
song = self.get_current_item()
if isinstance(song, Directory):
return False
if self.favorite_folder is None:
return False
favorites_path = self.favorite_folder.path / 'song_list.txt'
lines = []
with open(favorites_path, 'r', encoding='utf-8-sig') as song_list:
for line in song_list:
line = line.strip()
if not line: # Skip empty lines
continue
hash, title, subtitle = line.split('|')
if song.hash == hash or (song.tja.metadata.title['en'] == title and song.tja.metadata.subtitle['en'] == subtitle):
if not self.in_favorites:
return False
else:
lines.append(line)
if self.in_favorites:
with open(favorites_path, 'w', encoding='utf-8-sig') as song_list:
for line in lines:
song_list.write(line + '\n')
print("Removed favorite:", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en'])
else:
with open(favorites_path, 'a', encoding='utf-8-sig') as song_list:
song_list.write(f'{song.hash}|{song.tja.metadata.title['en']}|{song.tja.metadata.subtitle['en']}\n')
print("Added favorite: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en'])
return True