diff --git a/PyTaiko.py b/PyTaiko.py index b9c377b..a3378cf 100644 --- a/PyTaiko.py +++ b/PyTaiko.py @@ -17,7 +17,7 @@ from raylib.defines import ( from libs.audio import audio from libs.config import get_config -from libs.global_data import PlayerNum, ScoreMethod +from libs.global_data import Difficulty, PlayerNum, ScoreMethod from libs.screen import Screen from libs.song_hash import DB_VERSION from libs.parsers.tja import TJAParser @@ -264,7 +264,10 @@ def check_args(): parser.error(f"Invalid difficulty: {args.difficulty}. Available: {list(tja.metadata.course_data.keys())}") selected_difficulty = args.difficulty else: - selected_difficulty = max(tja.metadata.course_data.keys()) + if not tja.metadata.course_data: + selected_difficulty = Difficulty.EASY + else: + selected_difficulty = max(tja.metadata.course_data.keys()) current_screen = Screens.GAME_PRACTICE if args.practice else Screens.GAME global_data.session_data[PlayerNum.P1].selected_song = path global_data.session_data[PlayerNum.P1].selected_difficulty = selected_difficulty diff --git a/Songs/15 Recently Played/song_list.txt b/Songs/15 Recently Played/song_list.txt deleted file mode 100644 index 401d41d..0000000 --- a/Songs/15 Recently Played/song_list.txt +++ /dev/null @@ -1 +0,0 @@ -61506649d1a0f78c7759ffd83b010e58ab0e167bdeb06b11584933d7a7409f35|Dogbite|t+pazolite diff --git a/libs/file_navigator.py b/libs/file_navigator.py index f4a38e0..757b33d 100644 --- a/libs/file_navigator.py +++ b/libs/file_navigator.py @@ -14,6 +14,7 @@ from raylib import SHADER_UNIFORM_VEC3 from libs.animation import Animation, MoveAnimation from libs.audio import audio from libs.global_data import Crown, Difficulty, ScoreMethod +from libs.parsers.osz import OsuParser from libs.texture import tex from libs.parsers.tja import TJAParser, test_encodings from libs.utils import OutlinedText, get_current_ms, global_data @@ -208,13 +209,13 @@ class BackBox(BaseBox): self.yellow_box.draw(self, fade_override, is_ura, self.name) class SongBox(BaseBox): - def __init__(self, name: str, back_color: Optional[tuple[int, int, int]], fore_color: Optional[tuple[int, int, int]], texture_index: TextureIndex, tja: TJAParser): + def __init__(self, name: str, back_color: Optional[tuple[int, int, int]], fore_color: Optional[tuple[int, int, int]], texture_index: TextureIndex, tja: TJAParser | OsuParser): super().__init__(name, back_color, fore_color, texture_index) self.scores = dict() self.hash = dict() self.score_history = None self.history_wait = 0 - self.tja = tja + self.parser = tja self.is_favorite = False self.yellow_box = None @@ -226,8 +227,8 @@ class SongBox(BaseBox): with sqlite3.connect(global_data.score_db) as con: cursor = con.cursor() # Batch database query for all diffs at once - if self.tja.metadata.course_data: - hash_values = [self.hash[diff] for diff in self.tja.metadata.course_data if diff in self.hash] + if self.parser.metadata.course_data: + hash_values = [self.hash[diff] for diff in self.parser.metadata.course_data if diff in self.hash] placeholders = ','.join('?' * len(hash_values)) batch_query = f""" @@ -239,7 +240,7 @@ class SongBox(BaseBox): hash_to_score = {row[0]: row[1:] for row in cursor.fetchall()} - for diff in self.tja.metadata.course_data: + for diff in self.parser.metadata.course_data: if diff not in self.hash: continue diff_hash = self.hash[diff] @@ -261,7 +262,7 @@ class SongBox(BaseBox): self.score_history = ScoreHistory(self.scores, current_time) if not is_open_prev and self.is_open: - self.yellow_box = YellowBox(False, tja=self.tja) + self.yellow_box = YellowBox(False, tja=self.parser) self.yellow_box.create_anim() self.wait = current_time if current_time >= self.history_wait + 3000: @@ -275,7 +276,7 @@ class SongBox(BaseBox): self.name.draw(outline_color=self.fore_color, x=x + tex.skin_config["song_box_name"].x - int(self.name.texture.width / 2), y=y+tex.skin_config["song_box_name"].y, y2=min(self.name.texture.height, tex.skin_config["song_box_name"].height)-self.name.texture.height, fade=outer_fade_override) - if self.tja.ex_data.new: + if self.parser.ex_data.new: tex.draw_texture('yellow_box', 'ex_data_new_song_balloon', x=x, y=y, fade=outer_fade_override) valid_scores = {k: v for k, v in self.scores.items() if v is not None} if valid_scores: @@ -297,6 +298,46 @@ class SongBox(BaseBox): if self.score_history is not None and get_current_ms() >= self.history_wait + 3000: self.score_history.draw() +class SongBoxOsu(SongBox): + def update(self, current_time: float, is_diff_select: bool): + super().update(current_time, is_diff_select) + is_open_prev = self.is_open + self.is_open = self.position == BOX_CENTER + + if self.yellow_box is not None: + self.yellow_box.update(is_diff_select) + + if self.history_wait == 0: + self.history_wait = current_time + + if self.score_history is None and {k: v for k, v in self.scores.items() if v is not None}: + self.score_history = ScoreHistory(self.scores, current_time) + + if not is_open_prev and self.is_open: + self.yellow_box = YellowBox(False) + self.yellow_box.create_anim() + self.wait = current_time + if current_time >= self.history_wait + 3000: + self.history_wait = current_time + + if self.score_history is not None: + self.score_history.update(current_time) + + def _draw_closed(self, x: float, y: float, outer_fade_override: float): + super()._draw_closed(x, y, outer_fade_override) + + self.name.draw(outline_color=self.fore_color, x=x + tex.skin_config["song_box_name"].x - int(self.name.texture.width / 2), y=y+tex.skin_config["song_box_name"].y, y2=min(self.name.texture.height, tex.skin_config["song_box_name"].height)-self.name.texture.height, fade=outer_fade_override) + valid_scores = {k: v for k, v in self.scores.items() if v is not None} + if valid_scores: + highest_key = max(valid_scores.keys()) + score = self.scores[highest_key] + if score and score[5] == Crown.DFC: + tex.draw_texture('yellow_box', 'crown_dfc', x=x, y=y, frame=min(Difficulty.URA, highest_key), fade=outer_fade_override) + elif score and score[5] == Crown.FC: + tex.draw_texture('yellow_box', 'crown_fc', x=x, y=y, frame=min(Difficulty.URA, highest_key), fade=outer_fade_override) + elif score and score[5] >= Crown.CLEAR: + tex.draw_texture('yellow_box', 'crown_clear', x=x, y=y, frame=min(Difficulty.URA, highest_key), fade=outer_fade_override) + class FolderBox(BaseBox): def __init__(self, name: str, back_color: Optional[tuple[int, int, int]], fore_color: Optional[tuple[int, int, int]], texture_index: TextureIndex, genre_index: GenreIndex, tja_count: int = 0, box_texture: Optional[str] = None): super().__init__(name, back_color, fore_color, texture_index) @@ -387,12 +428,20 @@ class FolderBox(BaseBox): tex.draw_texture('yellow_box', 'song_count_songs', color=color) dest_width = min(tex.skin_config["song_tja_count"].width, self.tja_count_text.texture.width) self.tja_count_text.draw(outline_color=ray.BLACK, x=tex.skin_config["song_tja_count"].x - (dest_width//2), y=tex.skin_config["song_tja_count"].y, x2=dest_width-self.tja_count_text.texture.width, color=color) - if self.texture_index != TextureIndex.DEFAULT: + if self.texture_index != TextureIndex.DEFAULT and self.box_texture is None: tex.draw_texture('box', 'folder_graphic', color=color, frame=self.genre_index) tex.draw_texture('box', 'folder_text', color=color, frame=self.genre_index) elif self.box_texture is not None: scaled_width = self.box_texture.width * tex.screen_scale scaled_height = self.box_texture.height * tex.screen_scale + max_width = 344 * tex.screen_scale + max_height = 424 * tex.screen_scale + if scaled_width > max_width or scaled_height > max_height: + width_scale = max_width / scaled_width + height_scale = max_height / scaled_height + scale_factor = min(width_scale, height_scale) + scaled_width *= scale_factor + scaled_height *= scale_factor x = int((x + tex.skin_config["box_texture"].x) - (scaled_width // 2)) y = int((y + tex.skin_config["box_texture"].y) - (scaled_height // 2)) src = ray.Rectangle(0, 0, self.box_texture.width, self.box_texture.height) @@ -401,7 +450,7 @@ class FolderBox(BaseBox): class YellowBox: """A song box when it is opened.""" - def __init__(self, is_back: bool, tja: Optional[TJAParser] = None, is_dan: bool = False): + def __init__(self, is_back: bool, tja: Optional[TJAParser | OsuParser] = None, is_dan: bool = False): self.is_diff_select = False self.is_back = is_back self.tja = tja @@ -1061,12 +1110,23 @@ class SongFile(FileSystemItem): def __init__(self, path: Path, name: str, back_color: Optional[tuple[int, int, int]], fore_color: Optional[tuple[int, int, int]], texture_index: TextureIndex): super().__init__(path, name) self.is_recent = (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)) <= timedelta(days=7) - self.tja = TJAParser(path) + self.parser = 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.parser.ex_data.new = True + title = self.parser.metadata.title.get(global_data.config['general']['language'].lower(), self.parser.metadata.title['en']) self.hash = global_data.song_paths[path] - self.box = SongBox(title, back_color, fore_color, texture_index, self.tja) + self.box = SongBox(title, back_color, fore_color, texture_index, self.parser) + self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"] + self.box.get_scores() + +class SongFileOsu(FileSystemItem): + def __init__(self, path: Path, name: str, back_color: Optional[tuple[int, int, int]], fore_color: Optional[tuple[int, int, int]], texture_index: TextureIndex): + super().__init__(path, name) + self.is_recent = (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)) <= timedelta(days=7) + self.parser = OsuParser(path) + title = self.parser.osu_metadata["Version"] + self.hash = global_data.song_paths[path] + self.box = SongBoxOsu(title, back_color, fore_color, texture_index, self.parser) self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"] self.box.get_scores() @@ -1122,7 +1182,7 @@ class FileNavigator: # Pre-generated objects storage self.all_directories: dict[str, Directory] = {} # path -> Directory - self.all_song_files: dict[str, Union[SongFile, DanCourse]] = {} # path -> SongFile + self.all_song_files: dict[str, Union[SongFile, DanCourse, SongFileOsu]] = {} # path -> SongFile self.directory_contents: dict[str, list[Union[Directory, SongFile]]] = {} # path -> list of items # OPTION 2: Lazy crown calculation with caching @@ -1262,6 +1322,10 @@ class FileNavigator: child_dirs = [] for item_path in dir_path.iterdir(): if item_path.is_dir(): + child_has_osu = any(item_path.glob("*.osu")) + if child_has_osu: + child_dirs.append(item_path) + self.process_osz(item_path) child_has_box_def = (item_path / "box.def").exists() if child_has_box_def: child_dirs.append(item_path) @@ -1289,8 +1353,8 @@ class FileNavigator: elif song_key not in self.all_song_files and tja_path in global_data.song_paths: song_obj = SongFile(tja_path, tja_path.name, back_color, fore_color, texture_index) song_obj.box.get_scores() - for course in song_obj.tja.metadata.course_data: - level = song_obj.tja.metadata.course_data[course].level + for course in song_obj.parser.metadata.course_data: + level = song_obj.parser.metadata.course_data[course].level scores = song_obj.box.scores.get(course) if scores is not None: @@ -1342,6 +1406,61 @@ class FileNavigator: logger.error(f"Error creating SongFile for {tja_path}: {e}") continue + def process_osz(self, dir_path: Path): + dir_key = str(dir_path) + if dir_path.iterdir(): + name = dir_path.name + for file in dir_path.iterdir(): + if file.name.endswith('.osu'): + with open(file, 'r', encoding='utf-8') as f: + content = f.readlines() + for line in content: + if line.startswith('TitleUnicode:'): + title_unicode = line.split(':', 1)[1].strip() + name = title_unicode + break + else: + name = dir_path.name if dir_path.name else str(dir_path) + box_texture = None + collection = None + back_color = None + fore_color = None + texture_index = TextureIndex.DEFAULT + genre_index = GenreIndex.DEFAULT + + for file in dir_path.iterdir(): + if file.name.endswith('.jpg') or file.name.endswith('.png'): + box_texture = str(file) + + # Create Directory object + file_count = len([file for file in dir_path.glob("*.osu")]) + directory_obj = Directory( + dir_path, name, back_color, fore_color, texture_index, genre_index, + tja_count=file_count, + box_texture=box_texture, + collection=collection, + ) + self.all_directories[dir_key] = directory_obj + + content_items = [] + + osu_files = [file for file in dir_path.glob("*.osu")] + + # Create SongFile objects + for osu_path in sorted(osu_files): + song_key = str(osu_path) + if song_key not in self.all_song_files and osu_path in global_data.song_paths: + song_obj = SongFileOsu(osu_path, osu_path.name, back_color, fore_color, texture_index) + song_obj.box.get_scores() + self.song_count += 1 + global_data.song_progress = self.song_count / global_data.total_songs + self.all_song_files[song_key] = song_obj + + if song_key in self.all_song_files: + content_items.append(self.all_song_files[song_key]) + + self.directory_contents[dir_key] = content_items + def is_at_root(self) -> bool: """Check if currently at the virtual root""" return self.current_dir == Path() @@ -1377,7 +1496,7 @@ class FileNavigator: if sibling_key in self.directory_contents: for item in self.directory_contents[sibling_key]: if isinstance(item, SongFile) and item: - 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: + if self.diff_sort_diff in item.parser.metadata.course_data and item.parser.metadata.course_data[self.diff_sort_diff].level == self.diff_sort_level: if item not in content_items: content_items.append(item) return content_items @@ -1425,7 +1544,7 @@ class FileNavigator: if self._levenshtein_distance(song.name[:-4].lower(), search_name.lower()) < 2: items.append(song) if isinstance(song, SongFile): - if self._levenshtein_distance(song.tja.metadata.subtitle["en"].lower(), search_name.lower()) < 2: + if self._levenshtein_distance(song.parser.metadata.subtitle["en"].lower(), search_name.lower()) < 2: items.append(song) return items @@ -1784,7 +1903,7 @@ class FileNavigator: else: box.draw(box.position + int(move_away_attribute), tex.skin_config["boxes"].y, is_ura, inner_fade_override=diff_fade_out_attribute, outer_fade_override=fade) - def mark_crowns_dirty_for_song(self, song_file: SongFile): + def mark_crowns_dirty_for_song(self, song_file: SongFile | SongFileOsu): """Mark directories as needing crown recalculation when a song's score changes""" song_path = song_file.path @@ -1847,7 +1966,7 @@ class FileNavigator: 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' + new_entry = f'{song.hash}|{song.parser.metadata.title["en"]}|{song.parser.metadata.subtitle["en"]}\n' existing_entries = [] if recents_path.exists(): with open(recents_path, 'r', encoding='utf-8-sig') as song_list: @@ -1858,7 +1977,7 @@ class FileNavigator: with open(recents_path, 'w', encoding='utf-8-sig') as song_list: song_list.writelines(recent_entries) - logger.info(f"Added Recent: {song.hash} {song.tja.metadata.title['en']} {song.tja.metadata.subtitle['en']}") + logger.info(f"Added Recent: {song.hash} {song.parser.metadata.title['en']} {song.parser.metadata.subtitle['en']}") def add_favorite(self) -> bool: """Add the current song to the favorites list""" @@ -1877,7 +1996,7 @@ class FileNavigator: 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 song.hash == hash or (song.parser.metadata.title['en'] == title and song.parser.metadata.subtitle['en'] == subtitle): if not self.in_favorites: return False else: @@ -1886,11 +2005,11 @@ class FileNavigator: with open(favorites_path, 'w', encoding='utf-8-sig') as song_list: for line in lines: song_list.write(line + '\n') - logger.info(f"Removed Favorite: {song.hash} {song.tja.metadata.title['en']} {song.tja.metadata.subtitle['en']}") + logger.info(f"Removed Favorite: {song.hash} {song.parser.metadata.title['en']} {song.parser.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') - logger.info(f"Added Favorite: {song.hash} {song.tja.metadata.title['en']} {song.tja.metadata.subtitle['en']}") + song_list.write(f'{song.hash}|{song.parser.metadata.title["en"]}|{song.parser.metadata.subtitle["en"]}\n') + logger.info(f"Added Favorite: {song.hash} {song.parser.metadata.title['en']} {song.parser.metadata.subtitle['en']}") return True navigator = FileNavigator() diff --git a/libs/song_hash.py b/libs/song_hash.py index 103f339..50e343d 100644 --- a/libs/song_hash.py +++ b/libs/song_hash.py @@ -5,9 +5,11 @@ import logging import sqlite3 import time from pathlib import Path +import zipfile from libs.config import get_config from libs.global_data import Crown +from libs.parsers.osz import OsuParser from libs.parsers.tja import NoteList, TJAParser, test_encodings from libs.utils import global_data @@ -105,12 +107,24 @@ def build_song_hashes(output_dir=Path("cache")): for root_dir in tja_paths: root_path = Path(root_dir) found_tja_files = root_path.rglob("*.tja", recurse_symlinks=True) + found_osz_files = root_path.rglob("*.osz", recurse_symlinks=True) + found_osu_files = root_path.rglob("*.osu", recurse_symlinks=True) all_tja_files.extend(found_tja_files) + all_tja_files.extend(found_osz_files) + all_tja_files.extend(found_osu_files) global_data.total_songs = len(all_tja_files) files_to_process = [] for tja_path in all_tja_files: + if tja_path.suffix == '.osz': + with zipfile.ZipFile(tja_path, 'r') as zip_file: + zip_file.extractall(tja_path.with_suffix('')) + zip_path = Path(tja_path.with_suffix('')) + tja_path.unlink() + for file in zip_path.glob('*.osu'): + files_to_process.append(file) + continue tja_path_str = str(tja_path) current_modified = tja_path.stat().st_mtime if current_modified <= saved_timestamp: @@ -133,56 +147,64 @@ def build_song_hashes(output_dir=Path("cache")): global_data.total_songs = total_songs for tja_path in files_to_process: - try: - tja_path_str = str(tja_path) + if tja_path.suffix == '.osu': + parser = OsuParser(tja_path) + path_str = str(tja_path) current_modified = tja_path.stat().st_mtime - tja = TJAParser(tja_path) - all_notes = NoteList() diff_hashes = dict() + all_notes = parser.notes_to_position(0)[0] + diff_hashes[0] = parser.hash_note_data(all_notes) + else: + try: + path_str = str(tja_path) + current_modified = tja_path.stat().st_mtime + parser = TJAParser(tja_path) + all_notes = NoteList() + diff_hashes = dict() - for diff in tja.metadata.course_data: - diff_notes, branch_m, branch_e, branch_n = TJAParser.notes_to_position(TJAParser(tja.file_path), diff) - diff_hashes[diff] = tja.hash_note_data(diff_notes) - all_notes.play_notes.extend(diff_notes.play_notes) - if branch_m: - for branch in branch_m: - all_notes.play_notes.extend(branch.play_notes) - all_notes.bars.extend(branch.bars) - if branch_e: - for branch in branch_e: - all_notes.play_notes.extend(branch.play_notes) - all_notes.bars.extend(branch.bars) - if branch_n: - for branch in branch_n: - all_notes.play_notes.extend(branch.play_notes) - all_notes.bars.extend(branch.bars) - all_notes.bars.extend(diff_notes.bars) - except Exception as e: - logger.error(f"Failed to parse TJA {tja_path}: {e}") - continue + for diff in parser.metadata.course_data: + diff_notes, branch_m, branch_e, branch_n = TJAParser.notes_to_position(TJAParser(parser.file_path), diff) + diff_hashes[diff] = parser.hash_note_data(diff_notes) + all_notes.play_notes.extend(diff_notes.play_notes) + if branch_m: + for branch in branch_m: + all_notes.play_notes.extend(branch.play_notes) + all_notes.bars.extend(branch.bars) + if branch_e: + for branch in branch_e: + all_notes.play_notes.extend(branch.play_notes) + all_notes.bars.extend(branch.bars) + if branch_n: + for branch in branch_n: + all_notes.play_notes.extend(branch.play_notes) + all_notes.bars.extend(branch.bars) + all_notes.bars.extend(diff_notes.bars) + except Exception as e: + logger.error(f"Failed to parse TJA {tja_path}: {e}") + continue if all_notes == NoteList(): continue - hash_val = tja.hash_note_data(all_notes) + hash_val = parser.hash_note_data(all_notes) if hash_val not in song_hashes: song_hashes[hash_val] = [] song_hashes[hash_val].append({ - "file_path": tja_path_str, + "file_path": path_str, "last_modified": current_modified, - "title": tja.metadata.title, - "subtitle": tja.metadata.subtitle, + "title": parser.metadata.title, + "subtitle": parser.metadata.subtitle, "diff_hashes": diff_hashes }) # Update both indexes - path_to_hash[tja_path_str] = hash_val + path_to_hash[path_str] = 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('ja', '') if isinstance(tja.metadata.title, dict) else '' + en_name = parser.metadata.title.get('en', '') if isinstance(parser.metadata.title, dict) else str(parser.metadata.title) + jp_name = parser.metadata.title.get('ja', '') if isinstance(parser.metadata.title, dict) else '' score_ini_path = tja_path.with_suffix('.tja.score.ini') if score_ini_path.exists(): diff --git a/libs/video.py b/libs/video.py index 09f7bd0..2021469 100644 --- a/libs/video.py +++ b/libs/video.py @@ -14,6 +14,12 @@ class VideoPlayer: def __init__(self, path: Path): """Initialize a video player instance""" self.is_finished_list = [False, False] + self.is_static = False + if path.suffix == '.png' or path.suffix == '.jpg': + self.texture = ray.LoadTexture(str(path).encode('utf-8')) + self.is_static = True + return + self.container = av.open(str(path)) self.video_stream = self.container.streams.video[0] @@ -144,6 +150,8 @@ class VideoPlayer: def start(self, current_ms: float) -> None: """Start video playback at call time""" + if self.is_static: + return self.start_ms = current_ms self._init_frame_generator() self._load_frame(0) @@ -154,11 +162,15 @@ class VideoPlayer: def set_volume(self, volume: float) -> None: """Set video volume, takes float value from 0.0 to 1.0""" + if self.is_static: + return if self.audio is not None: audio.set_music_volume(self.audio, volume) def update(self): """Updates video playback, advancing frames and audio""" + if self.is_static: + return self._audio_manager() if self.frame_index >= len(self.frame_timestamps): @@ -197,6 +209,11 @@ class VideoPlayer: def stop(self): """Stops the video, audio, and clears its buffer""" + if self.is_static: + if self.texture is not None: + ray.UnloadTexture(self.texture) + self.texture = None + return if self.container: self.container.close() diff --git a/scenes/game.py b/scenes/game.py index d05d417..489d4a4 100644 --- a/scenes/game.py +++ b/scenes/game.py @@ -23,6 +23,7 @@ from libs.global_data import ( ScoreMethod, ) from libs.global_objects import AllNetIcon, Nameplate +from libs.parsers.osz import OsuParser from libs.screen import Screen from libs.texture import tex from libs.parsers.tja import ( @@ -98,9 +99,9 @@ class GameScreen(Screen): self.load_hitsounds() self.song_info = SongInfo(session_data.song_title, session_data.genre_index) self.result_transition = ResultTransition(global_data.player_num) - subtitle = self.tja.metadata.subtitle.get(global_data.config['general']['language'].lower(), '') - self.bpm = self.tja.metadata.bpm - scene_preset = self.tja.metadata.scene_preset + subtitle = self.parser.metadata.subtitle.get(global_data.config['general']['language'].lower(), '') + self.bpm = self.parser.metadata.bpm + scene_preset = self.parser.metadata.scene_preset if self.movie is None: self.background = Background(global_data.player_num, self.bpm, scene_preset=scene_preset) logger.info("Background initialized") @@ -144,18 +145,22 @@ class GameScreen(Screen): def init_tja(self, song: Path): """Initialize the TJA file""" - self.tja = TJAParser(song, start_delay=self.start_delay) - if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists(): - self.movie = VideoPlayer(self.tja.metadata.bgmovie) + if song.suffix == '.osu': + self.start_delay = 0 + self.parser = OsuParser(song) + else: + self.parser = TJAParser(song, start_delay=self.start_delay) + if self.parser.metadata.bgmovie != Path() and self.parser.metadata.bgmovie.exists(): + self.movie = VideoPlayer(self.parser.metadata.bgmovie) else: self.movie = None - global_data.session_data[global_data.player_num].song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) - if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: - self.song_music = audio.load_music_stream(self.tja.metadata.wave, 'song') + global_data.session_data[global_data.player_num].song_title = self.parser.metadata.title.get(global_data.config['general']['language'].lower(), self.parser.metadata.title['en']) + if self.parser.metadata.wave.exists() and self.parser.metadata.wave.is_file() and self.song_music is None: + self.song_music = audio.load_music_stream(self.parser.metadata.wave, 'song') - self.player_1 = Player(self.tja, global_data.player_num, global_data.session_data[global_data.player_num].selected_difficulty, False, global_data.modifiers[global_data.player_num]) - self.start_ms = get_current_ms() - self.tja.metadata.offset*1000 - self.precise_start = time.time() - self.tja.metadata.offset + self.player_1 = Player(self.parser, global_data.player_num, global_data.session_data[global_data.player_num].selected_difficulty, False, global_data.modifiers[global_data.player_num]) + self.start_ms = get_current_ms() - self.parser.metadata.offset*1000 + self.precise_start = time.time() - self.parser.metadata.offset def write_score(self): """Write the score to the database""" @@ -183,21 +188,21 @@ class GameScreen(Screen): INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); ''' - data = (hash, self.tja.metadata.title['en'], - self.tja.metadata.title.get('ja', ''), self.player_1.difficulty, + data = (hash, self.parser.metadata.title['en'], + self.parser.metadata.title.get('ja', ''), self.player_1.difficulty, session_data.result_data.score, session_data.result_data.good, session_data.result_data.ok, session_data.result_data.bad, session_data.result_data.total_drumroll, session_data.result_data.max_combo, crown) cursor.execute(insert_query, data) session_data.result_data.prev_score = existing_score if existing_score is not None else 0 - logger.info(f"Wrote score {session_data.result_data.score} for {self.tja.metadata.title['en']}") + logger.info(f"Wrote score {session_data.result_data.score} for {self.parser.metadata.title['en']}") con.commit() if result is None or (existing_crown is not None and crown > existing_crown): cursor.execute("UPDATE Scores SET clear = ? WHERE hash = ?", (crown, hash)) con.commit() def start_song(self, ms_from_start): - if (ms_from_start >= self.tja.metadata.offset*1000 + self.start_delay - global_data.config["general"]["audio_offset"]) and not self.song_started: + if (ms_from_start >= self.parser.metadata.offset*1000 + self.start_delay - global_data.config["general"]["audio_offset"]) and not self.song_started: if self.song_music is not None: audio.play_music_stream(self.song_music, 'music') logger.info(f"Song started at {ms_from_start}") @@ -275,7 +280,7 @@ class GameScreen(Screen): if self.transition.is_finished: self.start_song(self.current_ms) else: - self.start_ms = current_time - self.tja.metadata.offset*1000 + self.start_ms = current_time - self.parser.metadata.offset*1000 self.update_background(current_time) self.update_audio(self.current_ms) @@ -330,7 +335,7 @@ class Player: TIMING_OK_EASY = 108.441665649414 TIMING_BAD_EASY = 125.125 - def __init__(self, tja: TJAParser, player_num: PlayerNum, difficulty: int, is_2p: bool, modifiers: Modifiers): + def __init__(self, parser: TJAParser | OsuParser, player_num: PlayerNum, difficulty: int, is_2p: bool, modifiers: Modifiers): self.is_2p = is_2p self.is_dan = False self.player_num = player_num @@ -338,7 +343,7 @@ class Player: self.visual_offset = global_data.config["general"]["visual_offset"] self.score_method = global_data.config["general"]["score_method"] self.modifiers = modifiers - self.tja = tja + self.parser = parser self.reset_chart() @@ -369,7 +374,10 @@ class Player: self.delay_start: Optional[float] = None self.delay_end: Optional[float] = None self.combo_announce = ComboAnnounce(self.combo, 0, player_num, self.is_2p) - self.branch_indicator = BranchIndicator(self.is_2p) if tja and tja.metadata.course_data[self.difficulty].is_branching else None + if not parser.metadata.course_data: + self.branch_indicator = None + else: + self.branch_indicator = BranchIndicator(self.is_2p) if parser and parser.metadata.course_data[self.difficulty].is_branching else None self.ending_anim: Optional[FailAnimation | ClearAnimation | FCAnimation] = None self.is_gogo_time = False plate_info = global_data.config[f'nameplate_{self.is_2p+1}p'] @@ -381,7 +389,10 @@ class Player: self.judge_counter = None self.input_log: dict[float, str] = dict() - stars = tja.metadata.course_data[self.difficulty].level + if not parser.metadata.course_data: + stars = 10 + else: + stars = parser.metadata.course_data[self.difficulty].level self.gauge = Gauge(self.player_num, self.difficulty, stars, self.total_notes, self.is_2p) self.gauge_hit_effect: list[GaugeHitEffect] = [] @@ -414,9 +425,8 @@ class Player: unload_offset = travel_distance / sudden_pixels_per_ms note.unload_ms = note.hit_ms + unload_offset - def reset_chart(self): - notes, self.branch_m, self.branch_e, self.branch_n = self.tja.notes_to_position(self.difficulty) + notes, self.branch_m, self.branch_e, self.branch_n = self.parser.notes_to_position(self.difficulty) self.play_notes, self.draw_note_list, self.draw_bar_list = deque(apply_modifiers(notes, self.modifiers)[0]), deque(apply_modifiers(notes, self.modifiers)[1]), deque(apply_modifiers(notes, self.modifiers)[2]) self.don_notes = deque([note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}]) @@ -434,12 +444,12 @@ class Player: if self.score_method == ScoreMethod.SHINUCHI: self.base_score = calculate_base_score(total_notes) elif self.score_method == ScoreMethod.GEN3: - self.score_diff = self.tja.metadata.course_data[self.difficulty].scorediff + self.score_diff = self.parser.metadata.course_data[self.difficulty].scorediff if self.score_diff <= 0: logger.warning("Error: No scorediff specified or scorediff less than 0 | Using shinuchi scoring method instead") self.score_diff = 0 - score_init_list = self.tja.metadata.course_data[self.difficulty].scoreinit + score_init_list = self.parser.metadata.course_data[self.difficulty].scoreinit if len(score_init_list) <= 0: logger.warning("Error: No scoreinit specified or scoreinit less than 0 | Using shinuchi scoring method instead") self.score_init = calculate_base_score(total_notes) diff --git a/scenes/song_select.py b/scenes/song_select.py index b769136..006c1cf 100644 --- a/scenes/song_select.py +++ b/scenes/song_select.py @@ -16,6 +16,7 @@ from libs.file_navigator import ( GenreIndex, SongBox, SongFile, + SongFileOsu, navigator, ) from libs.global_data import Difficulty, Modifiers, PlayerNum @@ -101,7 +102,7 @@ class SongSelectScreen(Screen): self.navigator.mark_crowns_dirty_for_song(selected_song) curr_item = self.navigator.get_current_item() - if isinstance(curr_item, SongFile): + if not isinstance(curr_item, Directory): curr_item.box.get_scores() self.navigator.add_recent() @@ -116,7 +117,7 @@ class SongSelectScreen(Screen): ray.set_shader_value(self.shader, source_loc, source_color, SHADER_UNIFORM_VEC3) ray.set_shader_value(self.shader, target_loc, target_color, SHADER_UNIFORM_VEC3) - def finalize_song(self, current_item: SongFile): + def finalize_song(self, current_item: SongFile | SongFileOsu): global_data.session_data[global_data.player_num].selected_song = current_item.path global_data.session_data[global_data.player_num].song_hash = global_data.song_hashes[current_item.hash][0]["diff_hashes"][self.player_1.selected_difficulty] global_data.session_data[global_data.player_num].selected_difficulty = self.player_1.selected_difficulty @@ -126,7 +127,7 @@ class SongSelectScreen(Screen): self.screen_init = False self.reset_demo_music() current_item = self.navigator.get_current_item() - if isinstance(current_item, SongFile) and self.player_1.is_ready: + if (isinstance(current_item, SongFile) or isinstance(current_item, SongFileOsu)) and self.player_1.is_ready: self.finalize_song(current_item) self.player_1.nameplate.unload() if isinstance(current_item.box, SongBox) and current_item.box.yellow_box is not None: @@ -183,7 +184,7 @@ class SongSelectScreen(Screen): audio.stop_sound('bgm') return selected_song = self.navigator.select_current_item() - if isinstance(selected_song, SongFile): + if isinstance(selected_song, SongFile) or isinstance(selected_song, SongFileOsu): self.state = State.SONG_SELECTED self.player_1.on_song_selected(selected_song) audio.play_sound('don', 'sound') @@ -284,12 +285,12 @@ class SongSelectScreen(Screen): def check_for_selection(self): if self.player_1.selected_diff_highlight_fade.is_finished and not audio.is_sound_playing(f'voice_start_song_{global_data.player_num}p') and self.game_transition is None: selected_song = self.navigator.get_current_item() - if not isinstance(selected_song, SongFile): + if not isinstance(selected_song, SongFile) and not isinstance(selected_song, SongFileOsu): raise Exception("picked directory") - title = selected_song.tja.metadata.title.get( + title = selected_song.parser.metadata.title.get( global_data.config['general']['language'], '') - subtitle = selected_song.tja.metadata.subtitle.get( + subtitle = selected_song.parser.metadata.subtitle.get( global_data.config['general']['language'], '') self.game_transition = Transition(title, subtitle) self.game_transition.start() @@ -358,12 +359,12 @@ class SongSelectScreen(Screen): if not isinstance(song, Directory) and song.box.is_open: if self.demo_song is None and current_time >= 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, 'demo_song') + if song.parser.metadata.wave.exists() and song.parser.metadata.wave.is_file(): + self.demo_song = audio.load_music_stream(song.parser.metadata.wave, 'demo_song') audio.play_music_stream(self.demo_song, 'music') - audio.seek_music_stream(self.demo_song, song.tja.metadata.demostart) + audio.seek_music_stream(self.demo_song, song.parser.metadata.demostart) audio.stop_sound('bgm') - logger.info(f"Demo song loaded and playing for {song.tja.metadata.title}") + logger.info(f"Demo song loaded and playing for {song.parser.metadata.title}") if song.box.is_open: current_box = song.box if not isinstance(current_box, BackBox) and current_time >= song.box.wait + (83.33*3): @@ -445,7 +446,7 @@ class SongSelectScreen(Screen): if self.state == State.BROWSING and self.navigator.items != []: curr_item = self.navigator.get_current_item() - if isinstance(curr_item, SongFile): + if not isinstance(curr_item, Directory): curr_item.box.draw_score_history() self.draw_overlay() @@ -508,10 +509,10 @@ class SongSelectPlayer: def on_song_selected(self, selected_song): """Called when a song is selected""" - if Difficulty.URA not in selected_song.tja.metadata.course_data: + if Difficulty.URA not in selected_song.parser.metadata.course_data: self.is_ura = False - elif (Difficulty.URA in selected_song.tja.metadata.course_data and - Difficulty.ONI not in selected_song.tja.metadata.course_data): + elif (Difficulty.URA in selected_song.parser.metadata.course_data and + Difficulty.ONI not in selected_song.parser.metadata.course_data): self.is_ura = True def handle_input_browsing(self, last_moved, selected_item): @@ -645,7 +646,7 @@ class SongSelectPlayer: if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num): audio.play_sound('kat', 'sound') selected_song = current_item - diffs = sorted(selected_song.tja.metadata.course_data) + diffs = sorted(selected_song.parser.metadata.course_data) prev_diff = self.selected_difficulty ret_val = None