42 Commits

Author SHA1 Message Date
Anthony Samms
9397f28808 Update audio.py 2026-01-18 14:50:21 -05:00
Anthony Samms
034be723c9 Update PyTaiko.py 2026-01-18 14:50:05 -05:00
Anthony Samms
81d67a7ab3 minor optimizations 2026-01-15 19:46:53 -05:00
Anthony Samms
1ba25d6ce6 Update osz.py 2026-01-15 16:47:41 -05:00
Anthony Samms
0c9645bda7 cache note arc points, untruncate time 2026-01-15 15:15:19 -05:00
Anthony Samms
f62201dbb5 Update game.py 2026-01-15 13:32:42 -05:00
Anthony Samms
027ef5408a Update test_texture.py 2026-01-15 13:20:26 -05:00
Anthony Samms
29d3fdd289 performance improvements 2026-01-15 13:16:18 -05:00
Anthony Samms
0fa765e58b Merge pull request #196 from QBaraki/better_hit_lane_effect
feat: added new 'judgment' for LaneHitEffect and makes lane hit effect more accurate
2026-01-15 06:35:36 -05:00
Anthony Samms
0e5100c3d0 Update PyTaikoGreen 2026-01-15 06:35:29 -05:00
Anthony Samms
7111677e0f Update osz.py 2026-01-15 06:34:30 -05:00
Anthony Samms
5a5f9d9d0d more crash fixes 2026-01-14 17:56:01 -05:00
Anthony Samms
c496ef2188 Update song_select.py 2026-01-14 17:53:09 -05:00
Anthony Samms
0a1327f4b5 crash fixes 2026-01-14 17:49:39 -05:00
Anthony Samms
69808f11e9 Merge branch 'main' of https://github.com/Yonokid/PyTaiko 2026-01-14 17:44:36 -05:00
Anthony Samms
165bd34390 Update osz.py 2026-01-14 17:43:55 -05:00
Anthony Samms
2692d40439 Merge pull request #197 from magickale/osu-parser
Added osu parsing
2026-01-14 17:31:17 -05:00
Anthony Samms
e600f8de0b add proper metadata and bgmovie 2026-01-14 17:30:15 -05:00
Anthony Samms
2704097ee2 fix certain diff names 2026-01-14 15:51:53 -05:00
Anthony Samms
29878623ca add scroll speed multiplier 2026-01-14 14:41:17 -05:00
Anthony Samms
70fcda4670 fix all errors 2026-01-14 13:27:47 -05:00
Anthony Samms
0e8e616576 Update file_navigator.py 2026-01-14 13:08:18 -05:00
Anthony Samms
9c846b8b12 oszs are automatically removed 2026-01-14 12:48:52 -05:00
Anthony Samms
d728bfa9c8 bare minimum to select and display in song select 2026-01-14 12:05:00 -05:00
Valerio
3d89e51030 More clean up 2026-01-13 06:58:05 -05:00
Valerio
e0f2d9e15c Cleaned up project and placed parser in new location 2026-01-12 12:59:57 -05:00
magickale
12d2077e2a Merge branch 'Yonokid:main' into osu-parser 2026-01-12 12:46:04 -05:00
Anthony Samms
ea9d135741 move tja to parsers folder 2026-01-12 09:08:01 -05:00
Valerio
29ffccab57 Fully feature complete except for BPM changes and gimmicks 2026-01-06 15:02:55 -05:00
QBaraki
a461a4efa9 feat: added new 'judgment' for LaneHitEffect
In order to make the hit lane effect to be arcade accurate,
this variable was introduced.

`Player.check_note()` and `LaneHitEffect.draw()` function has been changed
in order to process such accurate hit lane effect.

Please note that in file `Skins/SkinName/Graphics/game/animation.json`,
the field with `"id": 0` needs to have `initial_opacity` set to `0.3`
for better experiences.
2026-01-06 18:01:53 +07:00
Valerio
70ddafae02 Calculates BPM, ONCE 2026-01-06 00:38:38 -05:00
Valerio
2f2272947b Cleaned up implementation, added variables to grab rest of osu data 2026-01-05 23:55:31 -05:00
Valerio
299ac2c47b Cleaned up parser a little, more feature complete implementation soon 2026-01-05 18:38:33 -05:00
magickale
b766a9e170 Merge branch 'Yonokid:main' into osu-parser 2026-01-05 14:32:47 -05:00
Valerio
976f5683b2 Added osu parser, not fully implemented. Very much hardcoded to only play p names 2026-01-05 14:29:55 -05:00
Anthony Samms
183eb75bba minor bug fix 2026-01-04 10:54:30 -05:00
Anthony Samms
8a595aebb7 Update PyTaikoGreen 2026-01-03 19:14:15 -05:00
Anthony Samms
0236c8676c Update PyTaiko.py 2026-01-03 19:14:11 -05:00
Anthony Samms
bcdb485909 Update settings.py 2026-01-03 16:45:57 -05:00
Anthony Samms
f41ffd71d4 Update settings.py 2026-01-03 16:44:29 -05:00
Anthony Samms
1d901b22bf Update settings.py 2026-01-03 16:30:57 -05:00
Anthony Samms
1781960dcc finish settings menu 2026-01-03 16:28:34 -05:00
25 changed files with 1367 additions and 391 deletions

View File

@@ -17,10 +17,10 @@ from raylib.defines import (
from libs.audio import audio from libs.audio import audio
from libs.config import get_config 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.screen import Screen
from libs.song_hash import DB_VERSION from libs.song_hash import DB_VERSION
from libs.tja import TJAParser from libs.parsers.tja import TJAParser
from libs.utils import ( from libs.utils import (
force_dedicated_gpu, force_dedicated_gpu,
get_current_ms, get_current_ms,
@@ -242,7 +242,7 @@ def init_audio():
def check_args(): def check_args():
if len(sys.argv) == 1: if len(sys.argv) == 1:
return Screens.SETTINGS return Screens.LOADING
parser = argparse.ArgumentParser(description='Launch game with specified song file') parser = argparse.ArgumentParser(description='Launch game with specified song file')
parser.add_argument('song_path', type=str, help='Path to the TJA song file') parser.add_argument('song_path', type=str, help='Path to the TJA song file')
@@ -263,6 +263,9 @@ def check_args():
if args.difficulty not in tja.metadata.course_data.keys(): if args.difficulty not in tja.metadata.course_data.keys():
parser.error(f"Invalid difficulty: {args.difficulty}. Available: {list(tja.metadata.course_data.keys())}") parser.error(f"Invalid difficulty: {args.difficulty}. Available: {list(tja.metadata.course_data.keys())}")
selected_difficulty = args.difficulty selected_difficulty = args.difficulty
else:
if not tja.metadata.course_data:
selected_difficulty = Difficulty.EASY
else: else:
selected_difficulty = max(tja.metadata.course_data.keys()) selected_difficulty = max(tja.metadata.course_data.keys())
current_screen = Screens.GAME_PRACTICE if args.practice else Screens.GAME current_screen = Screens.GAME_PRACTICE if args.practice else Screens.GAME
@@ -286,14 +289,15 @@ def check_discord_heartbeat(current_screen):
def draw_fps(last_fps: int): def draw_fps(last_fps: int):
curr_fps = ray.GetFPS() curr_fps = ray.GetFPS()
pos = 20 * global_tex.screen_scale
if curr_fps != 0 and curr_fps != last_fps: if curr_fps != 0 and curr_fps != last_fps:
last_fps = curr_fps last_fps = curr_fps
if last_fps < 30: if last_fps < 30:
ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.RED) pyray.draw_text_ex(global_data.font, f'{last_fps} FPS', (pos, pos), pos, 1, pyray.RED)
elif last_fps < 60: elif last_fps < 60:
ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.YELLOW) pyray.draw_text_ex(global_data.font, f'{last_fps} FPS', (pos, pos), pos, 1, pyray.YELLOW)
else: else:
ray.DrawText(f'{last_fps} FPS'.encode('utf-8'), 20, 20, 20, ray.LIME) pyray.draw_text_ex(pyray.get_font_default(), f'{last_fps} FPS', (pos, pos), pos, 1, pyray.LIME)
def draw_outer_border(screen_width: int, screen_height: int, last_color: pyray.Color): def draw_outer_border(screen_width: int, screen_height: int, last_color: pyray.Color):
pyray.draw_rectangle(-screen_width, 0, screen_width, screen_height, last_color) pyray.draw_rectangle(-screen_width, 0, screen_width, screen_height, last_color)
@@ -442,6 +446,8 @@ def main():
audio.close_audio_device() audio.close_audio_device()
if discord_connected: if discord_connected:
RPC.close() RPC.close()
global_tex.unload_textures()
screen_mapping[current_screen].on_screen_end("LOADING")
logger.info("Window closed and audio device shut down") logger.info("Window closed and audio device shut down")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1 +0,0 @@
61506649d1a0f78c7759ffd83b010e58ab0e167bdeb06b11584933d7a7409f35|Dogbite|t+pazolite

View File

@@ -2,7 +2,7 @@
fps_counter = false fps_counter = false
audio_offset = 0 audio_offset = 0
visual_offset = 0 visual_offset = 0
language = "ja" language = "en"
timer_frozen = true timer_frozen = true
judge_counter = false judge_counter = false
nijiiro_notes = false nijiiro_notes = false
@@ -64,7 +64,7 @@ device_type = 0
sample_rate = 44100 sample_rate = 44100
# buffer_size: Size in samples per audio buffer # buffer_size: Size in samples per audio buffer
# - 0 = let driver choose (may result in very small buffers with ASIO, typically 64) # - 0 = let driver choose (may result in very small buffers with ASIO, typically 64)
buffer_size = 32 buffer_size = 128
[volume] [volume]
sound = 1.0 sound = 1.0

View File

@@ -3,17 +3,8 @@ from typing import Any, Optional
from libs.global_data import global_data from libs.global_data import global_data
def get_current_ms() -> float:
def rounded(num: float) -> int: return time.time() * 1000
sign = 1 if (num >= 0) else -1
num = abs(num)
result = int(num)
if (num - result >= 0.5):
result += 1
return sign * result
def get_current_ms() -> int:
return rounded(time.time() * 1000)
class BaseAnimation(): class BaseAnimation():
@@ -84,6 +75,22 @@ class BaseAnimation():
self.restart() self.restart()
self.pause() self.pause()
def copy(self):
"""Create a copy of the animation with reset state."""
new_anim = self.__class__.__new__(self.__class__)
new_anim.duration = self.duration
new_anim.delay = self.delay_saved
new_anim.delay_saved = self.delay_saved
new_anim.start_ms = get_current_ms()
new_anim.is_finished = False
new_anim.attribute = 0
new_anim.is_started = False
new_anim.is_reversing = False
new_anim.unlocked = False
new_anim.loop = self.loop
new_anim.lock_input = self.lock_input
return new_anim
def _ease_in(self, progress: float, ease_type: str) -> float: def _ease_in(self, progress: float, ease_type: str) -> float:
if ease_type == "quadratic": if ease_type == "quadratic":
return progress * progress return progress * progress
@@ -133,6 +140,20 @@ class FadeAnimation(BaseAnimation):
self.final_opacity = self.final_opacity_saved self.final_opacity = self.final_opacity_saved
self.attribute = self.initial_opacity self.attribute = self.initial_opacity
def copy(self):
"""Create a copy of the fade animation with reset state."""
new_anim = super().copy()
new_anim.initial_opacity = self.initial_opacity_saved
new_anim.initial_opacity_saved = self.initial_opacity_saved
new_anim.final_opacity = self.final_opacity_saved
new_anim.final_opacity_saved = self.final_opacity_saved
new_anim.ease_in = self.ease_in
new_anim.ease_out = self.ease_out
new_anim.reverse_delay = self.reverse_delay_saved
new_anim.reverse_delay_saved = self.reverse_delay_saved
new_anim.attribute = self.initial_opacity_saved
return new_anim
def update(self, current_time_ms: float) -> None: def update(self, current_time_ms: float) -> None:
if not self.is_started: if not self.is_started:
return return
@@ -181,6 +202,20 @@ class MoveAnimation(BaseAnimation):
self.start_position = self.start_position_saved self.start_position = self.start_position_saved
self.attribute = self.start_position self.attribute = self.start_position
def copy(self):
"""Create a copy of the move animation with reset state."""
new_anim = super().copy()
new_anim.reverse_delay = self.reverse_delay_saved
new_anim.reverse_delay_saved = self.reverse_delay_saved
new_anim.total_distance = self.total_distance_saved
new_anim.total_distance_saved = self.total_distance_saved
new_anim.start_position = self.start_position_saved
new_anim.start_position_saved = self.start_position_saved
new_anim.ease_in = self.ease_in
new_anim.ease_out = self.ease_out
new_anim.attribute = self.start_position_saved
return new_anim
def update(self, current_time_ms: float) -> None: def update(self, current_time_ms: float) -> None:
if not self.is_started: if not self.is_started:
return return
@@ -217,6 +252,13 @@ class TextureChangeAnimation(BaseAnimation):
super().reset() super().reset()
self.attribute = self.textures[0][2] self.attribute = self.textures[0][2]
def copy(self):
"""Create a copy of the texture change animation with reset state."""
new_anim = super().copy()
new_anim.textures = self.textures # List of tuples, can be shared
new_anim.attribute = self.textures[0][2]
return new_anim
def update(self, current_time_ms: float) -> None: def update(self, current_time_ms: float) -> None:
if not self.is_started: if not self.is_started:
return return
@@ -234,6 +276,10 @@ class TextureChangeAnimation(BaseAnimation):
self.is_finished = True self.is_finished = True
class TextStretchAnimation(BaseAnimation): class TextStretchAnimation(BaseAnimation):
def copy(self):
"""Create a copy of the text stretch animation with reset state."""
return super().copy()
def update(self, current_time_ms: float) -> None: def update(self, current_time_ms: float) -> None:
if not self.is_started: if not self.is_started:
return return
@@ -275,6 +321,20 @@ class TextureResizeAnimation(BaseAnimation):
self.initial_size = self.initial_size_saved self.initial_size = self.initial_size_saved
self.final_size = self.final_size_saved self.final_size = self.final_size_saved
def copy(self):
"""Create a copy of the texture resize animation with reset state."""
new_anim = super().copy()
new_anim.initial_size = self.initial_size_saved
new_anim.initial_size_saved = self.initial_size_saved
new_anim.final_size = self.final_size_saved
new_anim.final_size_saved = self.final_size_saved
new_anim.reverse_delay = self.reverse_delay_saved
new_anim.reverse_delay_saved = self.reverse_delay_saved
new_anim.ease_in = self.ease_in
new_anim.ease_out = self.ease_out
new_anim.attribute = self.initial_size_saved
return new_anim
def update(self, current_time_ms: float) -> None: def update(self, current_time_ms: float) -> None:
if not self.is_started: if not self.is_started:

View File

@@ -1,5 +1,6 @@
import logging import logging
import platform import platform
import pyray as ray
from pathlib import Path from pathlib import Path
import cffi import cffi
@@ -92,6 +93,7 @@ ffi.cdef("""
void resume_music_stream(music music); void resume_music_stream(music music);
void stop_music_stream(music music); void stop_music_stream(music music);
void seek_music_stream(music music, float position); void seek_music_stream(music music, float position);
bool music_stream_needs_update(music music);
void update_music_stream(music music); void update_music_stream(music music);
bool is_music_stream_playing(music music); bool is_music_stream_playing(music music);
void set_music_volume(music music, float volume); void set_music_volume(music music, float volume);
@@ -131,6 +133,7 @@ class AudioEngine:
self.music_streams = {} self.music_streams = {}
self.audio_device_ready = False self.audio_device_ready = False
self.volume_presets = volume_presets self.volume_presets = volume_presets
self.lib = lib
if sounds_path is None: if sounds_path is None:
self.sounds_path = Path(f"Skins/{get_config()['paths']['skin']}/Sounds") self.sounds_path = Path(f"Skins/{get_config()['paths']['skin']}/Sounds")
@@ -138,15 +141,15 @@ class AudioEngine:
self.sounds_path = sounds_path self.sounds_path = sounds_path
def set_log_level(self, level: int): def set_log_level(self, level: int):
lib.set_log_level(level) # type: ignore self.lib.set_log_level(level) # type: ignore
def list_host_apis(self): def list_host_apis(self):
"""Prints a list of available host APIs to the console""" """Prints a list of available host APIs to the console"""
lib.list_host_apis() # type: ignore self.lib.list_host_apis() # type: ignore
def get_host_api_name(self, api_id: int) -> str: def get_host_api_name(self, api_id: int) -> str:
"""Returns the name of the host API with the given ID""" """Returns the name of the host API with the given ID"""
result = lib.get_host_api_name(api_id) # type: ignore result = self.lib.get_host_api_name(api_id) # type: ignore
if result == ffi.NULL: if result == ffi.NULL:
return "" return ""
result = ffi.string(result) result = ffi.string(result)
@@ -157,12 +160,12 @@ class AudioEngine:
def init_audio_device(self) -> bool: def init_audio_device(self) -> bool:
"""Initialize the audio device""" """Initialize the audio device"""
try: try:
lib.init_audio_device(self.device_type, self.target_sample_rate, self.buffer_size) # type: ignore self.lib.init_audio_device(self.device_type, self.target_sample_rate, self.buffer_size) # type: ignore
self.audio_device_ready = lib.is_audio_device_ready() # type: ignore self.audio_device_ready = self.lib.is_audio_device_ready() # type: ignore
file_path_str = str(self.sounds_path / 'don.wav').encode('utf-8') file_path_str = str(self.sounds_path / 'don.wav').encode('utf-8')
self.don = lib.load_sound(file_path_str) # type: ignore self.don = self.lib.load_sound(file_path_str) # type: ignore
file_path_str = str(self.sounds_path / 'ka.wav').encode('utf-8') file_path_str = str(self.sounds_path / 'ka.wav').encode('utf-8')
self.kat = lib.load_sound(file_path_str) # type: ignore self.kat = self.lib.load_sound(file_path_str) # type: ignore
if self.audio_device_ready: if self.audio_device_ready:
logger.info("Audio device initialized successfully") logger.info("Audio device initialized successfully")
return self.audio_device_ready return self.audio_device_ready
@@ -179,9 +182,9 @@ class AudioEngine:
for music_id in list(self.music_streams.keys()): for music_id in list(self.music_streams.keys()):
self.unload_music_stream(music_id) self.unload_music_stream(music_id)
lib.unload_sound(self.don) # type: ignore self.lib.unload_sound(self.don) # type: ignore
lib.unload_sound(self.kat) # type: ignore self.lib.unload_sound(self.kat) # type: ignore
lib.close_audio_device() # type: ignore self.lib.close_audio_device() # type: ignore
self.audio_device_ready = False self.audio_device_ready = False
logger.info("Audio device closed") logger.info("Audio device closed")
except Exception as e: except Exception as e:
@@ -189,15 +192,15 @@ class AudioEngine:
def is_audio_device_ready(self) -> bool: def is_audio_device_ready(self) -> bool:
"""Check if audio device is ready""" """Check if audio device is ready"""
return lib.is_audio_device_ready() # type: ignore return self.lib.is_audio_device_ready() # type: ignore
def set_master_volume(self, volume: float) -> None: def set_master_volume(self, volume: float) -> None:
"""Set master volume (0.0 to 1.0)""" """Set master volume (0.0 to 1.0)"""
lib.set_master_volume(max(0.0, min(1.0, volume))) # type: ignore self.lib.set_master_volume(max(0.0, min(1.0, volume))) # type: ignore
def get_master_volume(self) -> float: def get_master_volume(self) -> float:
"""Get master volume""" """Get master volume"""
return lib.get_master_volume() # type: ignore return self.lib.get_master_volume() # type: ignore
# Sound management # Sound management
def load_sound(self, file_path: Path, name: str) -> str: def load_sound(self, file_path: Path, name: str) -> str:
@@ -208,13 +211,13 @@ class AudioEngine:
file_path_str = str(file_path).encode('cp932', errors='replace') file_path_str = str(file_path).encode('cp932', errors='replace')
else: else:
file_path_str = str(file_path).encode('utf-8') file_path_str = str(file_path).encode('utf-8')
sound = lib.load_sound(file_path_str) # type: ignore sound = self.lib.load_sound(file_path_str) # type: ignore
if not lib.is_sound_valid(sound): # type: ignore if not self.lib.is_sound_valid(sound): # type: ignore
file_path_str = str(file_path).encode('utf-8') file_path_str = str(file_path).encode('utf-8')
sound = lib.load_sound(file_path_str) # type: ignore sound = self.lib.load_sound(file_path_str) # type: ignore
if lib.is_sound_valid(sound): # type: ignore if self.lib.is_sound_valid(sound): # type: ignore
self.sounds[name] = sound self.sounds[name] = sound
return name return name
else: else:
@@ -227,7 +230,7 @@ class AudioEngine:
def unload_sound(self, name: str) -> None: def unload_sound(self, name: str) -> None:
"""Unload a sound by name""" """Unload a sound by name"""
if name in self.sounds: if name in self.sounds:
lib.unload_sound(self.sounds[name]) # type: ignore self.lib.unload_sound(self.sounds[name]) # type: ignore
del self.sounds[name] del self.sounds[name]
else: else:
logger.warning(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
@@ -262,41 +265,41 @@ class AudioEngine:
"""Play a sound""" """Play a sound"""
if name == 'don': if name == 'don':
if volume_preset: if volume_preset:
lib.set_sound_volume(self.don, self.volume_presets[volume_preset]) # type: ignore self.lib.set_sound_volume(self.don, self.volume_presets[volume_preset]) # type: ignore
lib.play_sound(self.don) # type: ignore self.lib.play_sound(self.don) # type: ignore
elif name == 'kat': elif name == 'kat':
if volume_preset: if volume_preset:
lib.set_sound_volume(self.kat, self.volume_presets[volume_preset]) # type: ignore self.lib.set_sound_volume(self.kat, self.volume_presets[volume_preset]) # type: ignore
lib.play_sound(self.kat) # type: ignore self.lib.play_sound(self.kat) # type: ignore
elif name in self.sounds: elif name in self.sounds:
sound = self.sounds[name] sound = self.sounds[name]
if volume_preset: if volume_preset:
lib.set_sound_volume(sound, self.volume_presets[volume_preset]) # type: ignore self.lib.set_sound_volume(sound, self.volume_presets[volume_preset]) # type: ignore
lib.play_sound(sound) # type: ignore self.lib.play_sound(sound) # type: ignore
else: else:
logger.warning(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
def stop_sound(self, name: str) -> None: def stop_sound(self, name: str) -> None:
"""Stop a sound""" """Stop a sound"""
if name == 'don': if name == 'don':
lib.stop_sound(self.don) # type: ignore self.lib.stop_sound(self.don) # type: ignore
elif name == 'kat': elif name == 'kat':
lib.stop_sound(self.kat) # type: ignore self.lib.stop_sound(self.kat) # type: ignore
if name in self.sounds: if name in self.sounds:
sound = self.sounds[name] sound = self.sounds[name]
lib.stop_sound(sound) # type: ignore self.lib.stop_sound(sound) # type: ignore
else: else:
logger.warning(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
def is_sound_playing(self, name: str) -> bool: def is_sound_playing(self, name: str) -> bool:
"""Check if a sound is playing""" """Check if a sound is playing"""
if name == 'don': if name == 'don':
return lib.is_sound_playing(self.don) # type: ignore return self.lib.is_sound_playing(self.don) # type: ignore
elif name == 'kat': elif name == 'kat':
return lib.is_sound_playing(self.kat) # type: ignore return self.lib.is_sound_playing(self.kat) # type: ignore
if name in self.sounds: if name in self.sounds:
sound = self.sounds[name] sound = self.sounds[name]
return lib.is_sound_playing(sound) # type: ignore return self.lib.is_sound_playing(sound) # type: ignore
else: else:
logger.warning(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
return False return False
@@ -304,24 +307,24 @@ class AudioEngine:
def set_sound_volume(self, name: str, volume: float) -> None: def set_sound_volume(self, name: str, volume: float) -> None:
"""Set the volume of a specific sound""" """Set the volume of a specific sound"""
if name == 'don': if name == 'don':
lib.set_sound_volume(self.don, volume) # type: ignore self.lib.set_sound_volume(self.don, volume) # type: ignore
elif name == 'kat': elif name == 'kat':
lib.set_sound_volume(self.kat, volume) # type: ignore self.lib.set_sound_volume(self.kat, volume) # type: ignore
elif name in self.sounds: elif name in self.sounds:
sound = self.sounds[name] sound = self.sounds[name]
lib.set_sound_volume(sound, volume) # type: ignore self.lib.set_sound_volume(sound, volume) # type: ignore
else: else:
logger.warning(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
def set_sound_pan(self, name: str, pan: float) -> None: def set_sound_pan(self, name: str, pan: float) -> None:
"""Set the pan of a specific sound""" """Set the pan of a specific sound"""
if name == 'don': if name == 'don':
lib.set_sound_pan(self.don, pan) # type: ignore self.lib.set_sound_pan(self.don, pan) # type: ignore
elif name == 'kat': elif name == 'kat':
lib.set_sound_pan(self.kat, pan) # type: ignore self.lib.set_sound_pan(self.kat, pan) # type: ignore
elif name in self.sounds: elif name in self.sounds:
sound = self.sounds[name] sound = self.sounds[name]
lib.set_sound_pan(sound, pan) # type: ignore self.lib.set_sound_pan(sound, pan) # type: ignore
else: else:
logger.warning(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
@@ -334,13 +337,13 @@ class AudioEngine:
else: else:
file_path_str = str(file_path).encode('utf-8') file_path_str = str(file_path).encode('utf-8')
music = lib.load_music_stream(file_path_str) # type: ignore music = self.lib.load_music_stream(file_path_str) # type: ignore
if not lib.is_music_valid(music): # type: ignore if not self.lib.is_music_valid(music): # type: ignore
file_path_str = str(file_path).encode('utf-8') file_path_str = str(file_path).encode('utf-8')
music = lib.load_music_stream(file_path_str) # type: ignore music = self.lib.load_music_stream(file_path_str) # type: ignore
if lib.is_music_valid(music): # type: ignore if self.lib.is_music_valid(music): # type: ignore
self.music_streams[name] = music self.music_streams[name] = music
logger.info(f"Loaded music stream from {file_path} as {name}") logger.info(f"Loaded music stream from {file_path} as {name}")
return name return name
@@ -352,18 +355,26 @@ class AudioEngine:
"""Play a music stream""" """Play a music stream"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
lib.seek_music_stream(music, 0) # type: ignore self.lib.seek_music_stream(music, 0) # type: ignore
if volume_preset: if volume_preset:
lib.set_music_volume(music, self.volume_presets[volume_preset]) # type: ignore self.lib.set_music_volume(music, self.volume_presets[volume_preset]) # type: ignore
lib.play_music_stream(music) # type: ignore self.lib.play_music_stream(music) # type: ignore
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
def update_music_stream(self, name: str) -> None: def music_stream_needs_update(self, name: str) -> bool:
"""Update a music stream""" """Check if a music stream needs updating (buffers need refilling)"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
lib.update_music_stream(music) # type: ignore return self.lib.music_stream_needs_update(music) # type: ignore
return False
def update_music_stream(self, name: str) -> None:
"""Update a music stream (only if buffers need refilling)"""
if name in self.music_streams:
music = self.music_streams[name]
if self.lib.music_stream_needs_update(music): # type: ignore
self.lib.update_music_stream(music) # type: ignore
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
@@ -371,7 +382,7 @@ class AudioEngine:
"""Get the time length of a music stream""" """Get the time length of a music stream"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
return lib.get_music_time_length(music) # type: ignore return self.lib.get_music_time_length(music) # type: ignore
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
return 0.0 return 0.0
@@ -380,7 +391,7 @@ class AudioEngine:
"""Get the time played of a music stream""" """Get the time played of a music stream"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
return lib.get_music_time_played(music) # type: ignore return self.lib.get_music_time_played(music) # type: ignore
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
return 0.0 return 0.0
@@ -389,7 +400,7 @@ class AudioEngine:
"""Set the volume of a music stream""" """Set the volume of a music stream"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
lib.set_music_volume(music, volume) # type: ignore self.lib.set_music_volume(music, volume) # type: ignore
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
@@ -397,7 +408,7 @@ class AudioEngine:
"""Check if a music stream is playing""" """Check if a music stream is playing"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
return lib.is_music_stream_playing(music) # type: ignore return self.lib.is_music_stream_playing(music) # type: ignore
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
return False return False
@@ -406,7 +417,7 @@ class AudioEngine:
"""Stop a music stream""" """Stop a music stream"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
lib.stop_music_stream(music) # type: ignore self.lib.stop_music_stream(music) # type: ignore
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
@@ -414,7 +425,7 @@ class AudioEngine:
"""Unload a music stream""" """Unload a music stream"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
lib.unload_music_stream(music) # type: ignore self.lib.unload_music_stream(music) # type: ignore
del self.music_streams[name] del self.music_streams[name]
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
@@ -428,10 +439,76 @@ class AudioEngine:
"""Seek a music stream to a specific position""" """Seek a music stream to a specific position"""
if name in self.music_streams: if name in self.music_streams:
music = self.music_streams[name] music = self.music_streams[name]
lib.seek_music_stream(music, position) # type: ignore self.lib.seek_music_stream(music, position) # type: ignore
else:
logger.warning(f"Music stream {name} not found")
class AudioEngineLegacy(AudioEngine):
def __init__(self, device_type: int, sample_rate: float, buffer_size: int,
volume_presets: VolumeConfig, sounds_path: Path | None = None):
super().__init__(device_type, sample_rate, buffer_size, volume_presets, sounds_path)
self.lib = ray
def set_log_level(self, level: int):
pass
def list_host_apis(self):
"""Prints a list of available host APIs to the console"""
pass
def get_host_api_name(self, api_id: int) -> str:
"""Returns the name of the host API with the given ID"""
return ''
def init_audio_device(self) -> bool:
"""Initialize the audio device"""
try:
self.lib.init_audio_device()
self.audio_device_ready = self.lib.is_audio_device_ready()
file_path_str = str(self.sounds_path / 'don.wav')
self.don = self.lib.load_sound(file_path_str)
file_path_str = str(self.sounds_path / 'ka.wav')
self.kat = self.lib.load_sound(file_path_str)
if self.audio_device_ready:
logger.info("Audio device initialized successfully")
return self.audio_device_ready
except Exception as e:
logger.error(f"Failed to initialize audio device: {e}")
return False
def load_sound(self, file_path: Path, name: str) -> str:
"""Load a sound file and return sound ID"""
try:
sound = self.lib.load_sound(str(file_path))
if self.lib.is_sound_valid(sound):
self.sounds[name] = sound
return name
else:
logger.error(f"Failed to load sound: {file_path}")
return ""
except Exception as e:
logger.error(f"Error loading sound {file_path}: {e}")
return ""
def load_music_stream(self, file_path: Path, name: str) -> str:
"""Load a music stream and return music ID"""
music = self.lib.load_music_stream(str(file_path))
if self.lib.is_music_valid(music):
self.music_streams[name] = music
logger.info(f"Loaded music stream from {file_path} as {name}")
return name
else:
logger.error(f"Failed to load music: {file_path}")
return ""
def update_music_stream(self, name: str) -> None:
"""Update a music stream (only if buffers need refilling)"""
if name in self.music_streams:
music = self.music_streams[name]
self.lib.update_music_stream(music)
else: else:
logger.warning(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
# Create the global audio instance # Create the global audio instance
audio = AudioEngine(get_config()["audio"]["device_type"], get_config()["audio"]["sample_rate"], get_config()["audio"]["buffer_size"], get_config()["volume"]) audio = AudioEngineLegacy(get_config()["audio"]["device_type"], get_config()["audio"]["sample_rate"], get_config()["audio"]["buffer_size"], get_config()["volume"])
audio.set_master_volume(0.75) audio.set_master_volume(0.75)

View File

@@ -175,6 +175,7 @@ void pause_music_stream(music music);
void resume_music_stream(music music); void resume_music_stream(music music);
void stop_music_stream(music music); void stop_music_stream(music music);
void seek_music_stream(music music, float position); void seek_music_stream(music music, float position);
bool music_stream_needs_update(music music);
void update_music_stream(music music); void update_music_stream(music music);
bool is_music_stream_playing(music music); bool is_music_stream_playing(music music);
void set_music_volume(music music, float volume); void set_music_volume(music music, float volume);
@@ -1064,6 +1065,17 @@ void seek_music_stream(music music, float position) {
pthread_mutex_unlock(&AUDIO.System.lock); pthread_mutex_unlock(&AUDIO.System.lock);
} }
bool music_stream_needs_update(music music) {
if (music.stream.buffer == NULL || music.ctxData == NULL) return false;
pthread_mutex_lock(&AUDIO.System.lock);
bool needs_update = music.stream.buffer->isSubBufferProcessed[0] ||
music.stream.buffer->isSubBufferProcessed[1];
pthread_mutex_unlock(&AUDIO.System.lock);
return needs_update;
}
void update_music_stream(music music) { void update_music_stream(music music) {
if (music.stream.buffer == NULL || music.ctxData == NULL) return; if (music.stream.buffer == NULL || music.ctxData == NULL) return;
@@ -1071,33 +1083,41 @@ void update_music_stream(music music) {
SNDFILE *sndFile = ctx->snd_file; SNDFILE *sndFile = ctx->snd_file;
if (sndFile == NULL) return; if (sndFile == NULL) return;
for (int i = 0; i < 2; i++) { bool needs_refill[2];
pthread_mutex_lock(&AUDIO.System.lock); pthread_mutex_lock(&AUDIO.System.lock);
bool needs_refill = music.stream.buffer->isSubBufferProcessed[i]; needs_refill[0] = music.stream.buffer->isSubBufferProcessed[0];
needs_refill[1] = music.stream.buffer->isSubBufferProcessed[1];
pthread_mutex_unlock(&AUDIO.System.lock); pthread_mutex_unlock(&AUDIO.System.lock);
if (needs_refill) { if (!needs_refill[0] && !needs_refill[1]) return;
unsigned int subBufferSizeFrames = music.stream.buffer->sizeInFrames / 2; unsigned int subBufferSizeFrames = music.stream.buffer->sizeInFrames / 2;
float *buffer_data = (float *)music.stream.buffer->data;
bool needs_resampling = (ctx->resampler != NULL);
bool needs_mono_to_stereo = (music.stream.channels == 1 && AUDIO_DEVICE_CHANNELS == 2);
unsigned int frames_to_read = subBufferSizeFrames; unsigned int frames_to_read = subBufferSizeFrames;
if (ctx->resampler) { if (needs_resampling) {
frames_to_read = (unsigned int)(subBufferSizeFrames / ctx->src_ratio) + 1; frames_to_read = (unsigned int)(subBufferSizeFrames / ctx->src_ratio) + 1;
} }
if (AUDIO.System.pcmBufferSize < frames_to_read * music.stream.channels * sizeof(float)) { size_t required_size = frames_to_read * music.stream.channels * sizeof(float);
if (AUDIO.System.pcmBufferSize < required_size) {
FREE(AUDIO.System.pcmBuffer); FREE(AUDIO.System.pcmBuffer);
AUDIO.System.pcmBuffer = calloc(1, frames_to_read * music.stream.channels * sizeof(float)); AUDIO.System.pcmBuffer = calloc(1, required_size);
AUDIO.System.pcmBufferSize = frames_to_read * music.stream.channels * sizeof(float); AUDIO.System.pcmBufferSize = required_size;
} }
for (int i = 0; i < 2; i++) {
if (!needs_refill[i]) continue;
sf_count_t frames_read = sf_readf_float(sndFile, (float*)AUDIO.System.pcmBuffer, frames_to_read); sf_count_t frames_read = sf_readf_float(sndFile, (float*)AUDIO.System.pcmBuffer, frames_to_read);
unsigned int subBufferOffset = i * subBufferSizeFrames * AUDIO_DEVICE_CHANNELS; unsigned int subBufferOffset = i * subBufferSizeFrames * AUDIO_DEVICE_CHANNELS;
float *buffer_data = (float *)music.stream.buffer->data;
float *input_ptr = (float *)AUDIO.System.pcmBuffer; float *input_ptr = (float *)AUDIO.System.pcmBuffer;
sf_count_t frames_written = 0; sf_count_t frames_written = 0;
if (ctx->resampler) { if (needs_resampling) {
spx_uint32_t in_len = frames_read; spx_uint32_t in_len = frames_read;
spx_uint32_t out_len = subBufferSizeFrames; spx_uint32_t out_len = subBufferSizeFrames;
@@ -1115,7 +1135,7 @@ void update_music_stream(music music) {
frames_written = out_len; frames_written = out_len;
} else { } else {
if (music.stream.channels == 1 && AUDIO_DEVICE_CHANNELS == 2) { if (needs_mono_to_stereo) {
for (int j = 0; j < frames_read; j++) { for (int j = 0; j < frames_read; j++) {
buffer_data[subBufferOffset + j*2] = input_ptr[j]; buffer_data[subBufferOffset + j*2] = input_ptr[j];
buffer_data[subBufferOffset + j*2 + 1] = input_ptr[j]; buffer_data[subBufferOffset + j*2 + 1] = input_ptr[j];
@@ -1131,13 +1151,13 @@ void update_music_stream(music music) {
unsigned int size = (subBufferSizeFrames - frames_written) * AUDIO_DEVICE_CHANNELS * sizeof(float); unsigned int size = (subBufferSizeFrames - frames_written) * AUDIO_DEVICE_CHANNELS * sizeof(float);
memset(buffer_data + offset, 0, size); memset(buffer_data + offset, 0, size);
} }
}
pthread_mutex_lock(&AUDIO.System.lock); pthread_mutex_lock(&AUDIO.System.lock);
music.stream.buffer->isSubBufferProcessed[i] = false; if (needs_refill[0]) music.stream.buffer->isSubBufferProcessed[0] = false;
if (needs_refill[1]) music.stream.buffer->isSubBufferProcessed[1] = false;
pthread_mutex_unlock(&AUDIO.System.lock); pthread_mutex_unlock(&AUDIO.System.lock);
} }
}
}
bool is_music_stream_playing(music music) { bool is_music_stream_playing(music music) {
return is_audio_stream_playing(music.stream); return is_audio_stream_playing(music.stream);

View File

@@ -14,8 +14,9 @@ from raylib import SHADER_UNIFORM_VEC3
from libs.animation import Animation, MoveAnimation from libs.animation import Animation, MoveAnimation
from libs.audio import audio from libs.audio import audio
from libs.global_data import Crown, Difficulty, ScoreMethod from libs.global_data import Crown, Difficulty, ScoreMethod
from libs.parsers.osz import OsuParser
from libs.texture import tex from libs.texture import tex
from libs.tja import TJAParser, test_encodings from libs.parsers.tja import TJAParser, test_encodings
from libs.utils import OutlinedText, get_current_ms, global_data from libs.utils import OutlinedText, get_current_ms, global_data
BOX_CENTER = 594 * tex.screen_scale BOX_CENTER = 594 * tex.screen_scale
@@ -208,13 +209,13 @@ class BackBox(BaseBox):
self.yellow_box.draw(self, fade_override, is_ura, self.name) self.yellow_box.draw(self, fade_override, is_ura, self.name)
class SongBox(BaseBox): 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) super().__init__(name, back_color, fore_color, texture_index)
self.scores = dict() self.scores = dict()
self.hash = dict() self.hash = dict()
self.score_history = None self.score_history = None
self.history_wait = 0 self.history_wait = 0
self.tja = tja self.parser = tja
self.is_favorite = False self.is_favorite = False
self.yellow_box = None self.yellow_box = None
@@ -226,8 +227,8 @@ class SongBox(BaseBox):
with sqlite3.connect(global_data.score_db) as con: with sqlite3.connect(global_data.score_db) as con:
cursor = con.cursor() cursor = con.cursor()
# Batch database query for all diffs at once # Batch database query for all diffs at once
if self.tja.metadata.course_data: if self.parser.metadata.course_data:
hash_values = [self.hash[diff] for diff in self.tja.metadata.course_data if diff in self.hash] hash_values = [self.hash[diff] for diff in self.parser.metadata.course_data if diff in self.hash]
placeholders = ','.join('?' * len(hash_values)) placeholders = ','.join('?' * len(hash_values))
batch_query = f""" batch_query = f"""
@@ -239,7 +240,7 @@ class SongBox(BaseBox):
hash_to_score = {row[0]: row[1:] for row in cursor.fetchall()} 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: if diff not in self.hash:
continue continue
diff_hash = self.hash[diff] diff_hash = self.hash[diff]
@@ -261,7 +262,7 @@ class SongBox(BaseBox):
self.score_history = ScoreHistory(self.scores, current_time) self.score_history = ScoreHistory(self.scores, current_time)
if not is_open_prev and self.is_open: 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.yellow_box.create_anim()
self.wait = current_time self.wait = current_time
if current_time >= self.history_wait + 3000: 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) 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) 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} valid_scores = {k: v for k, v in self.scores.items() if v is not None}
if valid_scores: 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: if self.score_history is not None and get_current_ms() >= self.history_wait + 3000:
self.score_history.draw() 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): 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): 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) 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) 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) 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) 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_graphic', color=color, frame=self.genre_index)
tex.draw_texture('box', 'folder_text', 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: elif self.box_texture is not None:
scaled_width = self.box_texture.width * tex.screen_scale scaled_width = self.box_texture.width * tex.screen_scale
scaled_height = self.box_texture.height * 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)) x = int((x + tex.skin_config["box_texture"].x) - (scaled_width // 2))
y = int((y + tex.skin_config["box_texture"].y) - (scaled_height // 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) src = ray.Rectangle(0, 0, self.box_texture.width, self.box_texture.height)
@@ -401,7 +450,7 @@ class FolderBox(BaseBox):
class YellowBox: class YellowBox:
"""A song box when it is opened.""" """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_diff_select = False
self.is_back = is_back self.is_back = is_back
self.tja = tja 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): 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) super().__init__(path, name)
self.is_recent = (datetime.now() - datetime.fromtimestamp(path.stat().st_mtime)) <= timedelta(days=7) 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: if self.is_recent:
self.tja.ex_data.new = True self.parser.ex_data.new = True
title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) 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.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.hash = global_data.song_hashes[self.hash][0]["diff_hashes"]
self.box.get_scores() self.box.get_scores()
@@ -1122,7 +1182,7 @@ class FileNavigator:
# Pre-generated objects storage # Pre-generated objects storage
self.all_directories: dict[str, Directory] = {} # path -> Directory 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 self.directory_contents: dict[str, list[Union[Directory, SongFile]]] = {} # path -> list of items
# OPTION 2: Lazy crown calculation with caching # OPTION 2: Lazy crown calculation with caching
@@ -1262,6 +1322,10 @@ class FileNavigator:
child_dirs = [] child_dirs = []
for item_path in dir_path.iterdir(): for item_path in dir_path.iterdir():
if item_path.is_dir(): 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() child_has_box_def = (item_path / "box.def").exists()
if child_has_box_def: if child_has_box_def:
child_dirs.append(item_path) 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: 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 = SongFile(tja_path, tja_path.name, back_color, fore_color, texture_index)
song_obj.box.get_scores() song_obj.box.get_scores()
for course in song_obj.tja.metadata.course_data: for course in song_obj.parser.metadata.course_data:
level = song_obj.tja.metadata.course_data[course].level level = song_obj.parser.metadata.course_data[course].level
scores = song_obj.box.scores.get(course) scores = song_obj.box.scores.get(course)
if scores is not None: if scores is not None:
@@ -1342,6 +1406,61 @@ class FileNavigator:
logger.error(f"Error creating SongFile for {tja_path}: {e}") logger.error(f"Error creating SongFile for {tja_path}: {e}")
continue 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: def is_at_root(self) -> bool:
"""Check if currently at the virtual root""" """Check if currently at the virtual root"""
return self.current_dir == Path() return self.current_dir == Path()
@@ -1377,7 +1496,7 @@ class FileNavigator:
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) and item: 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: if item not in content_items:
content_items.append(item) content_items.append(item)
return content_items return content_items
@@ -1425,7 +1544,7 @@ class FileNavigator:
if self._levenshtein_distance(song.name[:-4].lower(), search_name.lower()) < 2: if self._levenshtein_distance(song.name[:-4].lower(), search_name.lower()) < 2:
items.append(song) items.append(song)
if isinstance(song, SongFile): 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) items.append(song)
return items return items
@@ -1784,7 +1903,7 @@ class FileNavigator:
else: 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) 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""" """Mark directories as needing crown recalculation when a song's score changes"""
song_path = song_file.path song_path = song_file.path
@@ -1847,7 +1966,7 @@ class FileNavigator:
return return
recents_path = self.recent_folder.path / 'song_list.txt' 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 = [] existing_entries = []
if recents_path.exists(): if recents_path.exists():
with open(recents_path, 'r', encoding='utf-8-sig') as song_list: 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: with open(recents_path, 'w', encoding='utf-8-sig') as song_list:
song_list.writelines(recent_entries) 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: def add_favorite(self) -> bool:
"""Add the current song to the favorites list""" """Add the current song to the favorites list"""
@@ -1877,7 +1996,7 @@ class FileNavigator:
if not line: # Skip empty lines if not line: # Skip empty lines
continue continue
hash, title, subtitle = line.split('|') 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: if not self.in_favorites:
return False return False
else: else:
@@ -1886,11 +2005,11 @@ class FileNavigator:
with open(favorites_path, 'w', encoding='utf-8-sig') as song_list: with open(favorites_path, 'w', encoding='utf-8-sig') as song_list:
for line in lines: for line in lines:
song_list.write(line + '\n') 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: else:
with open(favorites_path, 'a', encoding='utf-8-sig') as song_list: 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') 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.tja.metadata.title['en']} {song.tja.metadata.subtitle['en']}") logger.info(f"Added Favorite: {song.hash} {song.parser.metadata.title['en']} {song.parser.metadata.subtitle['en']}")
return True return True
navigator = FileNavigator() navigator = FileNavigator()

311
libs/parsers/osz.py Normal file
View File

@@ -0,0 +1,311 @@
import hashlib
import math
from pathlib import Path
from libs.parsers.tja import CourseData, Note, NoteType, Drumroll, Balloon, NoteList, TJAEXData, TJAMetadata, TimelineObject
import re
class OsuParser:
general: dict[str, str]
editor: dict[str, str]
osu_metadata: dict[str, str]
difficulty: dict[str, str]
events: list[list[float]]
timing_points: list[list[float]]
hit_objects: list[list[float]]
bpm: list[float]
def __init__(self, osu_file: Path):
self.general = self.read_osu_data_dict(osu_file, target_header="General")
self.editor = self.read_osu_data_dict(osu_file, target_header="Editor")
self.osu_metadata = self.read_osu_data_dict(osu_file, target_header="Metadata")
self.difficulty = self.read_osu_data_dict(osu_file, target_header="Difficulty")
self.events = self.read_osu_data_list(osu_file, target_header="Events")
self.timing_points = self.read_osu_data_list(osu_file, target_header="TimingPoints")
#self.general = self.read_osu_data(osu_file, target_header="Colours", is_dict=True)
self.hit_objects = self.read_osu_data_list(osu_file, target_header="HitObjects")
self.slider_multiplier = float(self.difficulty["SliderMultiplier"])
self.metadata = TJAMetadata()
self.metadata.wave = osu_file.parent / self.general["AudioFilename"]
self.metadata.demostart = float(self.general["PreviewTime"]) / 1000
self.metadata.offset = -30/1000
self.metadata.title["en"] = self.osu_metadata["Version"]
self.metadata.subtitle["en"] = self.osu_metadata["Creator"]
match = re.search(r'\[Events\][\s\S]*?^[ \t]*(\d+),(\d+),"([^"]+)"', osu_file.read_text(encoding='utf-8'), re.MULTILINE)
if match:
self.metadata.bgmovie = osu_file.parent / Path(match.group(3))
self.metadata.course_data[0] = CourseData()
self.ex_data = TJAEXData()
self.bpm = []
for points in self.timing_points:
self.bpm.append(math.floor(1 / points[1] * 1000 * 60))
self.osu_NoteList = self.note_data_to_NoteList(self.hit_objects)
for points in self.timing_points:
if 0 < points[1] < 60000:
obj = TimelineObject()
obj.hit_ms = points[0]
obj.bpm = math.floor(1 / points[1] * 1000 * 60)
self.osu_NoteList[0].timeline.append(obj)
def read_osu_data_list(self, file_path: Path, target_header="HitObjects") -> list[list[float]]:
data = []
current_header = None
with file_path.open(mode='r', encoding='utf-8') as f:
for line in f:
line = line.rstrip("\n")
if re.match(r"\[\w*\]", line): # header pattern
current_header = line[1:-1]
if current_header == target_header:
if re.match(r"[-+]?\d*\.?\d+" , line): # Events, TimingPoints, HitObjects
string_array = re.findall(r"[-+]?\d*\.?\d+" , line) # search for floats
int_array = [float(num_str) for num_str in string_array]
data.append(int_array)
else:
continue
return data
def read_osu_data_dict(self, file_path: Path, target_header="HitObjects") -> dict[str, str]:
data = dict()
current_header = None
with file_path.open(mode='r', encoding='utf-8') as f:
for line in f:
line = line.rstrip("\n")
if re.match(r"\[\w*\]", line): # header pattern
current_header = line[1:-1]
if current_header == target_header:
if ':' in line and not line.startswith('['):
key, value = line.split(':', 1)
data[key.strip()] = value.strip()
else:
continue
return data
def get_scroll_multiplier(self, ms: float) -> float:
base_scroll = (1.0 if 1.37 <= self.slider_multiplier <= 1.47
else self.slider_multiplier / 1.40)
current_scroll = 1.0
for tp in self.timing_points:
time = tp[0]
beat_length = tp[1] # positive for BPM, negative for scroll
if time > ms:
break
if beat_length < 0: # This is an inherited (green) timing point
current_scroll = -100.0 / beat_length
return current_scroll * base_scroll
def note_data_to_NoteList(self, note_data) -> tuple[NoteList, list[NoteList], list[NoteList], list[NoteList]]:
osu_NoteList = NoteList()
counter = 0
for line in note_data:
note_time = line[2]
scroll = self.get_scroll_multiplier(note_time)
if (line[3] == 1 or line[3] == 4 or line[3] == 5 or line[3] == 6) and line[4] == 0: # DON
don = Note()
don.type = NoteType(1)
don.hit_ms = line[2]
don.bpm = self.bpm[0]
don.scroll_x = scroll
don.scroll_y = 0
don.display = True
don.index = counter
counter = counter + 1
don.moji = 1
osu_NoteList.play_notes.append(don)
if (line[3] == 1 or line[3] == 4 or line[3] == 5 or line[3] == 6) and (line[4] == 2 or line[4] == 8): # KAT
kat = Note()
kat.type = NoteType(2)
kat.hit_ms = line[2]
kat.bpm = self.bpm[0]
kat.scroll_x = scroll
kat.scroll_y = 0
kat.display = True
kat.index = counter
counter = counter + 1
kat.moji = 4
osu_NoteList.play_notes.append(kat)
if (line[3] == 1 or line[3] == 4 or line[3] == 5 or line[3] == 6) and line[4] == 4: # L-DON
don = Note()
don.type = NoteType(3)
don.hit_ms = line[2]
don.bpm = self.bpm[0]
don.scroll_x = scroll
don.scroll_y = 0
don.display = True
don.index = counter
counter = counter + 1
don.moji = 5
osu_NoteList.play_notes.append(don)
if (line[3] == 1 or line[3] == 4 or line[3] == 5 or line[3] == 6) and (line[4] == 6 or line[4] == 12): # L-KAT
kat = Note()
kat.type = NoteType(4)
kat.hit_ms = line[2]
kat.bpm = self.bpm[0]
kat.scroll_x = scroll
kat.scroll_y = 0
kat.display = True
kat.index = counter
counter = counter + 1
kat.moji = 6
osu_NoteList.play_notes.append(kat)
if (line[3] == 2) and (line[4] == 0): # Drum Roll
if len(line) >= 9:
slider_time = line[8] / (float(self.difficulty["SliderMultiplier"]) * 100) * self.timing_points[0][1]
else:
slider_time = line[6] / (float(self.difficulty["SliderMultiplier"]) * 100) * self.timing_points[0][1]
source = Note()
source.type = NoteType(8)
source.hit_ms = line[2] + slider_time
source.bpm = self.bpm[0]
source.scroll_x = scroll
source.scroll_y = 0
source.display = True
# this is where the index would be if it wasn't a tail note
source.moji = 7
slider = Drumroll(source)
slider.color = 255
slider.type = NoteType(5)
slider.hit_ms = line[2]
slider.bpm = self.bpm[0]
slider.scroll_x = scroll
slider.scroll_y = 0
slider.display = True
slider.index = counter
slider.moji = 10
counter = counter + 1
source.index = counter
counter = counter + 1
osu_NoteList.play_notes.append(slider)
osu_NoteList.play_notes.append(source)
if (line[3] == 2) and (line[4] == 4): # L-Drum Roll
if len(line) >= 9:
slider_time = line[8] / (float(self.difficulty["SliderMultiplier"]) * 100) * self.timing_points[0][1]
else:
slider_time = line[6] / (float(self.difficulty["SliderMultiplier"]) * 100) * self.timing_points[0][1]
source = Note()
source.type = NoteType(8)
source.hit_ms = line[2] + slider_time
source.bpm = self.bpm[0]
source.scroll_x = scroll
source.scroll_y = 0
source.display = True
# this is where the index would be if it wasn't a tail note
source.moji = 8
slider = Drumroll(source)
slider.color = 255
slider.type = NoteType(6)
slider.hit_ms = line[2]
slider.bpm = self.bpm[0]
slider.scroll_x = scroll
slider.scroll_y = 0
slider.display = True
slider.index = counter
counter = counter + 1
source.index = counter
counter = counter + 1
osu_NoteList.play_notes.append(slider)
osu_NoteList.play_notes.append(source)
if (line[3] == 8): # Balloon
source = Note()
source.type = NoteType(8)
source.hit_ms = line[5]
source.bpm = self.bpm[0]
source.scroll_x = scroll
source.scroll_y = 0
source.display = True
#source.index = counter
#counter = counter + 1
source.moji = 9
balloon = Balloon(source)
balloon.type = NoteType(7)
balloon.hit_ms = line[2]
balloon.bpm = self.bpm[0]
balloon.scroll_x = scroll
balloon.scroll_y = 0
balloon.display = True
balloon.index = counter
counter = counter + 1
balloon.moji = 10
'''
od = int(self.difficulty["OverallDifficulty"])
# thank you https://github.com/IepIweidieng/osu2tja/blob/dev-iid/osu2tja/osu2tja.py
hit_multiplier = (5 - 2 * (5 - od) / 5 if od < 5
else 5 + 2.5 * (od - 5) / 5 if od > 5
else 5) * 1.65
'''
balloon.count = 20#int(max(1, (ret[-1][1] - ret[-2][1]) / 1000 * hit_multiplier))
# end of 'stolen' code
source.index = counter
counter = counter + 1
osu_NoteList.play_notes.append(balloon)
osu_NoteList.play_notes.append(source)
osu_NoteList.draw_notes = osu_NoteList.play_notes.copy()
return osu_NoteList, [], [], []
def notes_to_position(self, difficulty):
return self.osu_NoteList
def hash_note_data(self, notes: NoteList):
"""Hashes the note data for the given NoteList."""
n = hashlib.sha256()
list1 = notes.play_notes
list2 = notes.bars
merged: list[Note | Drumroll | Balloon] = []
i = 0
j = 0
while i < len(list1) and j < len(list2):
if list1[i] <= list2[j]:
merged.append(list1[i])
i += 1
else:
merged.append(list2[j])
j += 1
merged.extend(list1[i:])
merged.extend(list2[j:])
for item in merged:
n.update(item.get_hash().encode('utf-8'))
return n.hexdigest()

View File

@@ -2,7 +2,6 @@ import hashlib
import logging import logging
import math import math
import random import random
from collections import deque
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from enum import IntEnum from enum import IntEnum
from functools import lru_cache from functools import lru_cache

View File

@@ -5,10 +5,12 @@ import logging
import sqlite3 import sqlite3
import time import time
from pathlib import Path from pathlib import Path
import zipfile
from libs.config import get_config from libs.config import get_config
from libs.global_data import Crown from libs.global_data import Crown
from libs.tja import NoteList, TJAParser, test_encodings from libs.parsers.osz import OsuParser
from libs.parsers.tja import NoteList, TJAParser, test_encodings
from libs.utils import global_data from libs.utils import global_data
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -105,12 +107,24 @@ def build_song_hashes(output_dir=Path("cache")):
for root_dir in tja_paths: for root_dir in tja_paths:
root_path = Path(root_dir) root_path = Path(root_dir)
found_tja_files = root_path.rglob("*.tja", recurse_symlinks=True) 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_tja_files)
all_tja_files.extend(found_osz_files)
all_tja_files.extend(found_osu_files)
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:
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) 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:
@@ -133,16 +147,24 @@ def build_song_hashes(output_dir=Path("cache")):
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:
try: if tja_path.suffix == '.osu':
tja_path_str = str(tja_path) parser = OsuParser(tja_path)
path_str = str(tja_path)
current_modified = tja_path.stat().st_mtime current_modified = tja_path.stat().st_mtime
tja = TJAParser(tja_path) 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() all_notes = NoteList()
diff_hashes = dict() diff_hashes = dict()
for diff in tja.metadata.course_data: for diff in parser.metadata.course_data:
diff_notes, branch_m, branch_e, branch_n = TJAParser.notes_to_position(TJAParser(tja.file_path), diff) diff_notes, branch_m, branch_e, branch_n = TJAParser.notes_to_position(TJAParser(parser.file_path), diff)
diff_hashes[diff] = tja.hash_note_data(diff_notes) diff_hashes[diff] = parser.hash_note_data(diff_notes)
all_notes.play_notes.extend(diff_notes.play_notes) all_notes.play_notes.extend(diff_notes.play_notes)
if branch_m: if branch_m:
for branch in branch_m: for branch in branch_m:
@@ -164,25 +186,25 @@ def build_song_hashes(output_dir=Path("cache")):
if all_notes == NoteList(): if all_notes == NoteList():
continue continue
hash_val = tja.hash_note_data(all_notes) hash_val = parser.hash_note_data(all_notes)
if hash_val not in song_hashes: if hash_val not in song_hashes:
song_hashes[hash_val] = [] song_hashes[hash_val] = []
song_hashes[hash_val].append({ song_hashes[hash_val].append({
"file_path": tja_path_str, "file_path": path_str,
"last_modified": current_modified, "last_modified": current_modified,
"title": tja.metadata.title, "title": parser.metadata.title,
"subtitle": tja.metadata.subtitle, "subtitle": parser.metadata.subtitle,
"diff_hashes": diff_hashes "diff_hashes": diff_hashes
}) })
# Update both indexes # 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 global_data.song_paths[tja_path] = hash_val
# Prepare database updates for each difficulty # Prepare database updates for each difficulty
en_name = tja.metadata.title.get('en', '') if isinstance(tja.metadata.title, dict) else str(tja.metadata.title) en_name = parser.metadata.title.get('en', '') if isinstance(parser.metadata.title, dict) else str(parser.metadata.title)
jp_name = tja.metadata.title.get('ja', '') if isinstance(tja.metadata.title, dict) else '' jp_name = parser.metadata.title.get('ja', '') if isinstance(parser.metadata.title, dict) else ''
score_ini_path = tja_path.with_suffix('.tja.score.ini') score_ini_path = tja_path.with_suffix('.tja.score.ini')
if score_ini_path.exists(): if score_ini_path.exists():

View File

@@ -1,4 +1,3 @@
import copy
import json import json
import logging import logging
import sys import sys
@@ -128,7 +127,7 @@ class TextureWrapper:
if index not in self.animations: if index not in self.animations:
raise Exception(f"Unable to find id {index} in loaded animations") raise Exception(f"Unable to find id {index} in loaded animations")
if is_copy: if is_copy:
new_anim = copy.deepcopy(self.animations[index]) new_anim = self.animations[index].copy()
if self.animations[index].loop: if self.animations[index].loop:
new_anim.start() new_anim.start()
return new_anim return new_anim

View File

@@ -40,9 +40,9 @@ def rounded(num: float) -> int:
result += 1 result += 1
return sign * result return sign * result
def get_current_ms() -> int: def get_current_ms() -> float:
"""Get the current time in milliseconds""" """Get the current time in milliseconds"""
return rounded(time.time() * 1000) return time.time() * 1000
def strip_comments(code: str) -> str: def strip_comments(code: str) -> str:
"""Strip comments from a string of code""" """Strip comments from a string of code"""
@@ -175,6 +175,12 @@ class OutlinedText:
self.default_src = ray.Rectangle(0, 0, self.texture.width, self.texture.height) self.default_src = ray.Rectangle(0, 0, self.texture.width, self.texture.height)
self._last_outline_color = None
self._last_color = None
self._last_fade = None
self._outline_color_alloc = None
self._alpha_value = None
def _hash_text(self, text: str, font_size: int, color: ray.Color, vertical: bool): def _hash_text(self, text: str, font_size: int, color: ray.Color, vertical: bool):
n = hashlib.sha256() n = hashlib.sha256()
n.update(text.encode('utf-8')) n.update(text.encode('utf-8'))
@@ -406,39 +412,46 @@ class OutlinedText:
rotation (float): The rotation angle of the text. rotation (float): The rotation angle of the text.
fade (float): The fade factor to apply to the text. fade (float): The fade factor to apply to the text.
""" """
if self._last_outline_color != outline_color:
if isinstance(outline_color, tuple): if isinstance(outline_color, tuple):
outline_color_alloc = ray.ffi.new("float[4]", [ self._outline_color_alloc = ray.ffi.new("float[4]", [
outline_color[0] / 255.0, outline_color[0] / 255.0,
outline_color[1] / 255.0, outline_color[1] / 255.0,
outline_color[2] / 255.0, outline_color[2] / 255.0,
outline_color[3] / 255.0 outline_color[3] / 255.0
]) ])
else: else:
outline_color_alloc = ray.ffi.new("float[4]", [ self._outline_color_alloc = ray.ffi.new("float[4]", [
outline_color.r / 255.0, outline_color.r / 255.0,
outline_color.g / 255.0, outline_color.g / 255.0,
outline_color.b / 255.0, outline_color.b / 255.0,
outline_color.a / 255.0 outline_color.a / 255.0
]) ])
ray.set_shader_value(self.shader, self.outline_color_loc, outline_color_alloc, SHADER_UNIFORM_VEC4) ray.set_shader_value(self.shader, self.outline_color_loc, self._outline_color_alloc, SHADER_UNIFORM_VEC4)
self._last_outline_color = outline_color
if self._last_color != color or self._last_fade != fade:
if isinstance(color, tuple): if isinstance(color, tuple):
alpha_value = ray.ffi.new('float*', min(fade * 255, color[3]) / 255.0) self._alpha_value = ray.ffi.new('float*', min(fade * 255, color[3]) / 255.0)
else: else:
alpha_value = ray.ffi.new('float*', min(fade * 255, color.a) / 255.0) self._alpha_value = ray.ffi.new('float*', min(fade * 255, color.a) / 255.0)
ray.set_shader_value(self.shader, self.alpha_loc, self._alpha_value, SHADER_UNIFORM_FLOAT)
self._last_color = color
self._last_fade = fade
if fade != 1.1: if fade != 1.1:
final_color = ray.fade(color, fade) final_color = ray.fade(color, fade)
else: else:
final_color = color final_color = color
ray.set_shader_value(self.shader, self.alpha_loc, alpha_value, SHADER_UNIFORM_FLOAT)
if not self.vertical: if not self.vertical:
offset = (10 * global_tex.screen_scale)-10 offset = (10 * global_tex.screen_scale)-10
else: else:
offset = 0 offset = 0
dest_rect = ray.Rectangle(x, y+offset, self.texture.width+x2, self.texture.height+y2) dest_rect = ray.Rectangle(x, y+offset, self.texture.width+x2, self.texture.height+y2)
if self.outline_thickness > 0: if self.outline_thickness > 0 and self._last_color != ray.BLANK:
ray.begin_shader_mode(self.shader) ray.begin_shader_mode(self.shader)
ray.draw_texture_pro(self.texture, self.default_src, dest_rect, origin, rotation, final_color) ray.draw_texture_pro(self.texture, self.default_src, dest_rect, origin, rotation, final_color)
if self.outline_thickness > 0: if self.outline_thickness > 0 and self._last_color != ray.BLANK:
ray.end_shader_mode() ray.end_shader_mode()
def unload(self): def unload(self):

View File

@@ -14,6 +14,12 @@ class VideoPlayer:
def __init__(self, path: Path): def __init__(self, path: Path):
"""Initialize a video player instance""" """Initialize a video player instance"""
self.is_finished_list = [False, False] 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.container = av.open(str(path))
self.video_stream = self.container.streams.video[0] self.video_stream = self.container.streams.video[0]
@@ -144,6 +150,8 @@ class VideoPlayer:
def start(self, current_ms: float) -> None: def start(self, current_ms: float) -> None:
"""Start video playback at call time""" """Start video playback at call time"""
if self.is_static:
return
self.start_ms = current_ms self.start_ms = current_ms
self._init_frame_generator() self._init_frame_generator()
self._load_frame(0) self._load_frame(0)
@@ -154,11 +162,15 @@ class VideoPlayer:
def set_volume(self, volume: float) -> None: def set_volume(self, volume: float) -> None:
"""Set video volume, takes float value from 0.0 to 1.0""" """Set video volume, takes float value from 0.0 to 1.0"""
if self.is_static:
return
if self.audio is not None: if self.audio is not None:
audio.set_music_volume(self.audio, volume) audio.set_music_volume(self.audio, volume)
def update(self): def update(self):
"""Updates video playback, advancing frames and audio""" """Updates video playback, advancing frames and audio"""
if self.is_static:
return
self._audio_manager() self._audio_manager()
if self.frame_index >= len(self.frame_timestamps): if self.frame_index >= len(self.frame_timestamps):
@@ -186,10 +198,26 @@ class VideoPlayer:
def draw(self): def draw(self):
"""Draw video frames to the raylib canvas""" """Draw video frames to the raylib canvas"""
if self.texture is not None: if self.texture is not None:
source = (0, 0, self.texture.width, self.texture.height)
texture_aspect = self.texture.width / self.texture.height
screen_aspect = tex.screen_width / tex.screen_height
if texture_aspect > screen_aspect:
dest_width = tex.screen_width
dest_height = tex.screen_width / texture_aspect
dest_x = 0
dest_y = (tex.screen_height - dest_height) / 2
else:
dest_height = tex.screen_height
dest_width = tex.screen_height * texture_aspect
dest_x = (tex.screen_width - dest_width) / 2
dest_y = 0
destination = (dest_x, dest_y, dest_width, dest_height)
ray.ClearBackground(ray.BLACK)
ray.DrawTexturePro( ray.DrawTexturePro(
self.texture, self.texture,
(0, 0, self.texture.width, self.texture.height), source,
(0, 0, tex.screen_width, tex.screen_height), destination,
(0, 0), (0, 0),
0, 0,
ray.WHITE ray.WHITE
@@ -197,6 +225,11 @@ class VideoPlayer:
def stop(self): def stop(self):
"""Stops the video, audio, and clears its buffer""" """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: if self.container:
self.container.close() self.container.close()

View File

@@ -15,7 +15,7 @@ from libs.chara_2d import Chara2D
from libs.global_data import Difficulty, Modifiers, PlayerNum, global_data from libs.global_data import Difficulty, Modifiers, PlayerNum, global_data
from libs.global_objects import Nameplate from libs.global_objects import Nameplate
from libs.texture import tex from libs.texture import tex
from libs.tja import TJAParser from libs.parsers.tja import TJAParser
from libs.utils import get_current_ms, global_tex from libs.utils import get_current_ms, global_tex
from scenes.game import ( from scenes.game import (
DrumType, DrumType,
@@ -100,21 +100,21 @@ class AIBattleGameScreen(GameScreen):
def init_tja(self, song: Path): def init_tja(self, song: Path):
"""Initialize the TJA file""" """Initialize the TJA file"""
self.tja = TJAParser(song, start_delay=self.start_delay) self.parser = TJAParser(song, start_delay=self.start_delay)
self.movie = None self.movie = None
session_data = global_data.session_data[global_data.player_num] session_data = global_data.session_data[global_data.player_num]
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.parser.metadata.title.get(global_data.config['general']['language'].lower(), self.parser.metadata.title['en'])
if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: 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.tja.metadata.wave, 'song') self.song_music = audio.load_music_stream(self.parser.metadata.wave, 'song')
tja_copy = copy.deepcopy(self.tja) tja_copy = copy.deepcopy(self.parser)
self.player_1 = PlayerNoChara(self.tja, global_data.player_num, session_data.selected_difficulty, False, global_data.modifiers[global_data.player_num]) self.player_1 = PlayerNoChara(self.parser, global_data.player_num, session_data.selected_difficulty, False, global_data.modifiers[global_data.player_num])
self.player_1.gauge = AIGauge(self.player_1.player_num, self.player_1.difficulty, self.tja.metadata.course_data[self.player_1.difficulty].level, self.player_1.total_notes, self.player_1.is_2p) self.player_1.gauge = AIGauge(self.player_1.player_num, self.player_1.difficulty, self.parser.metadata.course_data[self.player_1.difficulty].level, self.player_1.total_notes, self.player_1.is_2p)
ai_modifiers = copy.deepcopy(global_data.modifiers[global_data.player_num]) ai_modifiers = copy.deepcopy(global_data.modifiers[global_data.player_num])
ai_modifiers.auto = True ai_modifiers.auto = True
self.player_2 = AIPlayer(tja_copy, PlayerNum.AI, session_data.selected_difficulty, True, ai_modifiers, AIDifficulty.LVL_2) self.player_2 = AIPlayer(tja_copy, PlayerNum.AI, session_data.selected_difficulty, True, ai_modifiers, AIDifficulty.LVL_2)
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) self.start_ms = (get_current_ms() - self.parser.metadata.offset*1000)
self.precise_start = time.time() - self.tja.metadata.offset self.precise_start = time.time() - self.parser.metadata.offset
self.total_notes = len(self.player_1.don_notes) + len(self.player_1.kat_notes) self.total_notes = len(self.player_1.don_notes) + len(self.player_1.kat_notes)
logger.info(f"TJA initialized for two-player song: {song}") logger.info(f"TJA initialized for two-player song: {song}")
@@ -140,7 +140,7 @@ class AIBattleGameScreen(GameScreen):
if self.transition.is_finished: if self.transition.is_finished:
self.start_song(self.current_ms) self.start_song(self.current_ms)
else: 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_background(current_time)
self.update_audio(self.current_ms) self.update_audio(self.current_ms)

View File

@@ -98,7 +98,7 @@ class AISongSelectPlayer(SongSelectPlayer):
def on_song_selected(self, selected_song: SongFile): def on_song_selected(self, selected_song: SongFile):
"""Called when a song is selected""" """Called when a song is selected"""
super().on_song_selected(selected_song) super().on_song_selected(selected_song)
self.subdiff_selector = SubdiffSelector(self.player_num, min(selected_song.tja.metadata.course_data)) self.subdiff_selector = SubdiffSelector(self.player_num, min(selected_song.parser.metadata.course_data))
def handle_input_selected(self, current_item): def handle_input_selected(self, current_item):
"""Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None""" """Handle input for selecting difficulty. Returns 'cancel', 'confirm', or None"""
@@ -158,7 +158,7 @@ class AISongSelectPlayer(SongSelectPlayer):
if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num): if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num):
audio.play_sound('kat', 'sound') audio.play_sound('kat', 'sound')
selected_song = current_item 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 prev_diff = self.selected_difficulty
ret_val = None ret_val = None

View File

@@ -16,7 +16,7 @@ from libs.global_data import (
) )
from libs.global_objects import AllNetIcon from libs.global_objects import AllNetIcon
from libs.texture import tex from libs.texture import tex
from libs.tja import TJAParser from libs.parsers.tja import TJAParser
from libs.transition import Transition from libs.transition import Transition
from libs.utils import OutlinedText, get_current_ms from libs.utils import OutlinedText, get_current_ms
from scenes.game import ( from scenes.game import (
@@ -90,7 +90,7 @@ class DanGameScreen(GameScreen):
self.player_1.is_dan = True self.player_1.is_dan = True
self.player_1.gauge = DanGauge(global_data.player_num, self.total_notes) self.player_1.gauge = DanGauge(global_data.player_num, self.total_notes)
self.song_info = SongInfo(song.metadata.title.get(global_data.config["general"]["language"], "en"), genre_index) self.song_info = SongInfo(song.metadata.title.get(global_data.config["general"]["language"], "en"), genre_index)
self.bpm = self.tja.metadata.bpm self.bpm = self.parser.metadata.bpm
logger.info(f"TJA initialized for song: {song.file_path}") logger.info(f"TJA initialized for song: {song.file_path}")
@@ -103,19 +103,19 @@ class DanGameScreen(GameScreen):
song, genre_index, difficulty, level = songs[self.song_index] song, genre_index, difficulty, level = songs[self.song_index]
session_data.selected_difficulty = difficulty session_data.selected_difficulty = difficulty
self.player_1.difficulty = difficulty self.player_1.difficulty = difficulty
self.tja = TJAParser(song.file_path, start_delay=self.start_delay) self.parser = TJAParser(song.file_path, start_delay=self.start_delay)
if self.song_music is not None: if self.song_music is not None:
audio.unload_music_stream(self.song_music) audio.unload_music_stream(self.song_music)
self.song_music = None self.song_music = None
self.song_started = False self.song_started = False
if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: 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.tja.metadata.wave, 'song') self.song_music = audio.load_music_stream(self.parser.metadata.wave, 'song')
self.player_1.tja = self.tja self.player_1.parser = self.parser
self.player_1.reset_chart() self.player_1.reset_chart()
self.dan_transition.start() self.dan_transition.start()
self.song_info = SongInfo(self.tja.metadata.title.get(global_data.config["general"]["language"], "en"), genre_index) self.song_info = SongInfo(self.parser.metadata.title.get(global_data.config["general"]["language"], "en"), genre_index)
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) self.start_ms = (get_current_ms() - self.parser.metadata.offset*1000)
def _calculate_dan_info(self): def _calculate_dan_info(self):
"""Calculate all dan info data for drawing""" """Calculate all dan info data for drawing"""
@@ -205,7 +205,7 @@ class DanGameScreen(GameScreen):
if self.transition.is_finished and self.dan_transition.is_finished: if self.transition.is_finished and self.dan_transition.is_finished:
self.start_song(self.current_ms) self.start_song(self.current_ms)
else: 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_background(current_time)
if self.song_music is not None: if self.song_music is not None:

View File

@@ -23,9 +23,10 @@ from libs.global_data import (
ScoreMethod, ScoreMethod,
) )
from libs.global_objects import AllNetIcon, Nameplate from libs.global_objects import AllNetIcon, Nameplate
from libs.parsers.osz import OsuParser
from libs.screen import Screen from libs.screen import Screen
from libs.texture import tex from libs.texture import tex
from libs.tja import ( from libs.parsers.tja import (
Balloon, Balloon,
Drumroll, Drumroll,
Note, Note,
@@ -98,9 +99,9 @@ class GameScreen(Screen):
self.load_hitsounds() self.load_hitsounds()
self.song_info = SongInfo(session_data.song_title, session_data.genre_index) self.song_info = SongInfo(session_data.song_title, session_data.genre_index)
self.result_transition = ResultTransition(global_data.player_num) self.result_transition = ResultTransition(global_data.player_num)
subtitle = self.tja.metadata.subtitle.get(global_data.config['general']['language'].lower(), '') subtitle = self.parser.metadata.subtitle.get(global_data.config['general']['language'].lower(), '')
self.bpm = self.tja.metadata.bpm self.bpm = self.parser.metadata.bpm
scene_preset = self.tja.metadata.scene_preset scene_preset = self.parser.metadata.scene_preset
if self.movie is None: if self.movie is None:
self.background = Background(global_data.player_num, self.bpm, scene_preset=scene_preset) self.background = Background(global_data.player_num, self.bpm, scene_preset=scene_preset)
logger.info("Background initialized") logger.info("Background initialized")
@@ -144,18 +145,22 @@ class GameScreen(Screen):
def init_tja(self, song: Path): def init_tja(self, song: Path):
"""Initialize the TJA file""" """Initialize the TJA file"""
self.tja = TJAParser(song, start_delay=self.start_delay) if song.suffix == '.osu':
if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists(): self.start_delay = 0
self.movie = VideoPlayer(self.tja.metadata.bgmovie) 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: else:
self.movie = None 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']) 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.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: 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.tja.metadata.wave, 'song') 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.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.tja.metadata.offset*1000 self.start_ms = get_current_ms() - self.parser.metadata.offset*1000
self.precise_start = time.time() - self.tja.metadata.offset self.precise_start = time.time() - self.parser.metadata.offset
def write_score(self): def write_score(self):
"""Write the score to the database""" """Write the score to the database"""
@@ -171,7 +176,7 @@ class GameScreen(Screen):
existing_score = result[0] if result is not None else None existing_score = result[0] if result is not None else None
existing_crown = result[1] if result is not None and len(result) > 1 and result[1] is not None else 0 existing_crown = result[1] if result is not None and len(result) > 1 and result[1] is not None else 0
crown = Crown.NONE crown = Crown.NONE
if session_data.result_data.bad and session_data.result_data.ok == 0: if session_data.result_data.bad == 0 and session_data.result_data.ok == 0:
crown = Crown.DFC crown = Crown.DFC
elif session_data.result_data.bad == 0: elif session_data.result_data.bad == 0:
crown = Crown.FC crown = Crown.FC
@@ -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) INSERT OR REPLACE INTO Scores (hash, en_name, jp_name, diff, score, good, ok, bad, drumroll, combo, clear)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
''' '''
data = (hash, self.tja.metadata.title['en'], data = (hash, self.parser.metadata.title['en'],
self.tja.metadata.title.get('ja', ''), self.player_1.difficulty, self.parser.metadata.title.get('ja', ''), self.player_1.difficulty,
session_data.result_data.score, session_data.result_data.good, session_data.result_data.score, session_data.result_data.good,
session_data.result_data.ok, session_data.result_data.bad, session_data.result_data.ok, session_data.result_data.bad,
session_data.result_data.total_drumroll, session_data.result_data.max_combo, crown) session_data.result_data.total_drumroll, session_data.result_data.max_combo, crown)
cursor.execute(insert_query, data) cursor.execute(insert_query, data)
session_data.result_data.prev_score = existing_score if existing_score is not None else 0 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() con.commit()
if result is None or (existing_crown is not None and crown > existing_crown): if result is None or (existing_crown is not None and crown > existing_crown):
cursor.execute("UPDATE Scores SET clear = ? WHERE hash = ?", (crown, hash)) cursor.execute("UPDATE Scores SET clear = ? WHERE hash = ?", (crown, hash))
con.commit() con.commit()
def start_song(self, ms_from_start): 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: if self.song_music is not None:
audio.play_music_stream(self.song_music, 'music') audio.play_music_stream(self.song_music, 'music')
logger.info(f"Song started at {ms_from_start}") logger.info(f"Song started at {ms_from_start}")
@@ -275,7 +280,7 @@ class GameScreen(Screen):
if self.transition.is_finished: if self.transition.is_finished:
self.start_song(self.current_ms) self.start_song(self.current_ms)
else: 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_background(current_time)
self.update_audio(self.current_ms) self.update_audio(self.current_ms)
@@ -309,7 +314,9 @@ class GameScreen(Screen):
def draw_overlay(self): def draw_overlay(self):
self.song_info.draw() self.song_info.draw()
if not self.transition.is_finished:
self.transition.draw() self.transition.draw()
if self.result_transition.is_started:
self.result_transition.draw() self.result_transition.draw()
self.allnet_indicator.draw() self.allnet_indicator.draw()
@@ -330,7 +337,7 @@ class Player:
TIMING_OK_EASY = 108.441665649414 TIMING_OK_EASY = 108.441665649414
TIMING_BAD_EASY = 125.125 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_2p = is_2p
self.is_dan = False self.is_dan = False
self.player_num = player_num self.player_num = player_num
@@ -338,7 +345,7 @@ class Player:
self.visual_offset = global_data.config["general"]["visual_offset"] self.visual_offset = global_data.config["general"]["visual_offset"]
self.score_method = global_data.config["general"]["score_method"] self.score_method = global_data.config["general"]["score_method"]
self.modifiers = modifiers self.modifiers = modifiers
self.tja = tja self.parser = parser
self.reset_chart() self.reset_chart()
@@ -369,7 +376,10 @@ class Player:
self.delay_start: Optional[float] = None self.delay_start: Optional[float] = None
self.delay_end: Optional[float] = None self.delay_end: Optional[float] = None
self.combo_announce = ComboAnnounce(self.combo, 0, player_num, self.is_2p) 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.ending_anim: Optional[FailAnimation | ClearAnimation | FCAnimation] = None
self.is_gogo_time = False self.is_gogo_time = False
plate_info = global_data.config[f'nameplate_{self.is_2p+1}p'] plate_info = global_data.config[f'nameplate_{self.is_2p+1}p']
@@ -381,7 +391,10 @@ class Player:
self.judge_counter = None self.judge_counter = None
self.input_log: dict[float, str] = dict() 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 = Gauge(self.player_num, self.difficulty, stars, self.total_notes, self.is_2p)
self.gauge_hit_effect: list[GaugeHitEffect] = [] self.gauge_hit_effect: list[GaugeHitEffect] = []
@@ -414,9 +427,8 @@ class Player:
unload_offset = travel_distance / sudden_pixels_per_ms unload_offset = travel_distance / sudden_pixels_per_ms
note.unload_ms = note.hit_ms + unload_offset note.unload_ms = note.hit_ms + unload_offset
def reset_chart(self): 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.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}]) self.don_notes = deque([note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}])
@@ -434,12 +446,12 @@ class Player:
if self.score_method == ScoreMethod.SHINUCHI: if self.score_method == ScoreMethod.SHINUCHI:
self.base_score = calculate_base_score(total_notes) self.base_score = calculate_base_score(total_notes)
elif self.score_method == ScoreMethod.GEN3: 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: if self.score_diff <= 0:
logger.warning("Error: No scorediff specified or scorediff less than 0 | Using shinuchi scoring method instead") logger.warning("Error: No scorediff specified or scorediff less than 0 | Using shinuchi scoring method instead")
self.score_diff = 0 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: if len(score_init_list) <= 0:
logger.warning("Error: No scoreinit specified or scoreinit less than 0 | Using shinuchi scoring method instead") logger.warning("Error: No scoreinit specified or scoreinit less than 0 | Using shinuchi scoring method instead")
self.score_init = calculate_base_score(total_notes) self.score_init = calculate_base_score(total_notes)
@@ -524,8 +536,8 @@ class Player:
self.draw_note_list.extend(branch_section.draw_notes) self.draw_note_list.extend(branch_section.draw_notes)
self.draw_bar_list.extend(branch_section.bars) self.draw_bar_list.extend(branch_section.bars)
self.play_notes = deque(sorted(self.play_notes)) self.play_notes = deque(sorted(self.play_notes))
self.draw_note_list = deque(sorted(self.draw_note_list, key=lambda x: x.hit_ms)) self.draw_note_list = deque(sorted(self.draw_note_list, key=lambda x: x.load_ms))
self.draw_bar_list = deque(sorted(self.draw_bar_list, key=lambda x: x.hit_ms)) self.draw_bar_list = deque(sorted(self.draw_bar_list, key=lambda x: x.load_ms))
total_don = [note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}] total_don = [note for note in self.play_notes if note.type in {NoteType.DON, NoteType.DON_L}]
total_kat = [note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_L}] total_kat = [note for note in self.play_notes if note.type in {NoteType.KAT, NoteType.KAT_L}]
total_other = [note for note in self.play_notes if note.type not in {NoteType.DON, NoteType.DON_L, NoteType.KAT, NoteType.KAT_L}] total_other = [note for note in self.play_notes if note.type not in {NoteType.DON, NoteType.DON_L, NoteType.KAT, NoteType.KAT_L}]
@@ -851,6 +863,7 @@ class Player:
if background is not None: if background is not None:
background.add_renda() background.add_renda()
self.score += 100 self.score += 100
if len(self.base_score_list) < 5:
self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p)) self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p))
if not self.current_notes_draw: if not self.current_notes_draw:
return return
@@ -870,6 +883,7 @@ class Player:
self.curr_balloon_count += 1 self.curr_balloon_count += 1
self.total_drumroll += 1 self.total_drumroll += 1
self.score += 100 self.score += 100
if len(self.base_score_list) < 5:
self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p)) self.base_score_list.append(ScoreCounterAnimation(self.player_num, 100, self.is_2p))
if self.curr_balloon_count == note.count: if self.curr_balloon_count == note.count:
self.is_balloon = False self.is_balloon = False
@@ -944,10 +958,12 @@ class Player:
big = curr_note.type == NoteType.DON_L or curr_note.type == NoteType.KAT_L big = curr_note.type == NoteType.DON_L or curr_note.type == NoteType.KAT_L
if (curr_note.hit_ms - good_window_ms) <= ms_from_start <= (curr_note.hit_ms + good_window_ms): if (curr_note.hit_ms - good_window_ms) <= ms_from_start <= (curr_note.hit_ms + good_window_ms):
if len(self.draw_judge_list) < 7:
self.draw_judge_list.append(Judgment(Judgments.GOOD, big, self.is_2p)) self.draw_judge_list.append(Judgment(Judgments.GOOD, big, self.is_2p))
self.lane_hit_effect = LaneHitEffect(Judgments.GOOD, self.is_2p) self.lane_hit_effect = LaneHitEffect(drum_type, Judgments.GOOD, self.is_2p)
self.good_count += 1 self.good_count += 1
self.score += self.base_score self.score += self.base_score
if len(self.base_score_list) < 5:
self.base_score_list.append(ScoreCounterAnimation(self.player_num, self.base_score, self.is_2p)) self.base_score_list.append(ScoreCounterAnimation(self.player_num, self.base_score, self.is_2p))
self.input_log[curr_note.index] = 'GOOD' self.input_log[curr_note.index] = 'GOOD'
self.note_correct(curr_note, current_time) self.note_correct(curr_note, current_time)
@@ -963,8 +979,10 @@ class Player:
elif (curr_note.hit_ms - ok_window_ms) <= ms_from_start <= (curr_note.hit_ms + ok_window_ms): elif (curr_note.hit_ms - ok_window_ms) <= ms_from_start <= (curr_note.hit_ms + ok_window_ms):
self.draw_judge_list.append(Judgment(Judgments.OK, big, self.is_2p)) self.draw_judge_list.append(Judgment(Judgments.OK, big, self.is_2p))
self.lane_hit_effect = LaneHitEffect(drum_type, Judgments.OK, self.is_2p)
self.ok_count += 1 self.ok_count += 1
self.score += 10 * math.floor(self.base_score / 2 / 10) self.score += 10 * math.floor(self.base_score / 2 / 10)
if len(self.base_score_list) < 5:
self.base_score_list.append(ScoreCounterAnimation(self.player_num, 10 * math.floor(self.base_score / 2 / 10), self.is_2p)) self.base_score_list.append(ScoreCounterAnimation(self.player_num, 10 * math.floor(self.base_score / 2 / 10), self.is_2p))
self.input_log[curr_note.index] = 'OK' self.input_log[curr_note.index] = 'OK'
self.note_correct(curr_note, current_time) self.note_correct(curr_note, current_time)
@@ -1026,7 +1044,8 @@ class Player:
self.kusudama_anim = None self.kusudama_anim = None
def spawn_hit_effects(self, drum_type: DrumType, side: Side): def spawn_hit_effects(self, drum_type: DrumType, side: Side):
self.lane_hit_effect = LaneHitEffect(drum_type, self.is_2p) self.lane_hit_effect = LaneHitEffect(drum_type, Judgments.BAD, self.is_2p) # Bad code detected...
if len(self.draw_drum_hit_list) < 4:
self.draw_drum_hit_list.append(DrumHitEffect(drum_type, side, self.is_2p)) self.draw_drum_hit_list.append(DrumHitEffect(drum_type, side, self.is_2p))
def handle_input(self, ms_from_start: float, current_time: float, background: Optional[Background]): def handle_input(self, ms_from_start: float, current_time: float, background: Optional[Background]):
@@ -1142,7 +1161,7 @@ class Player:
finished_arcs = [] finished_arcs = []
for i, anim in enumerate(self.draw_arc_list): for i, anim in enumerate(self.draw_arc_list):
anim.update(current_time) anim.update(current_time)
if anim.is_finished: if anim.is_finished and len(self.gauge_hit_effect) < 7:
self.gauge_hit_effect.append(GaugeHitEffect(anim.note_type, anim.is_big, self.is_2p)) self.gauge_hit_effect.append(GaugeHitEffect(anim.note_type, anim.is_big, self.is_2p))
finished_arcs.append(i) finished_arcs.append(i)
for i in reversed(finished_arcs): for i in reversed(finished_arcs):
@@ -1451,9 +1470,10 @@ class Judgment:
class LaneHitEffect: class LaneHitEffect:
"""Display a gradient overlay when the player hits the drum""" """Display a gradient overlay when the player hits the drum"""
def __init__(self, type: Judgments | DrumType, is_2p: bool): def __init__(self, type: DrumType, judgment: Judgments, is_2p: bool):
self.is_2p = is_2p self.is_2p = is_2p
self.type = type self.type = type
self.judgment = judgment
self.fade = tex.get_animation(0, is_copy=True) self.fade = tex.get_animation(0, is_copy=True)
self.fade.start() self.fade.start()
self.is_finished = False self.is_finished = False
@@ -1464,12 +1484,12 @@ class LaneHitEffect:
self.is_finished = True self.is_finished = True
def draw(self): def draw(self):
if self.type == Judgments.GOOD: if self.type == DrumType.DON:
tex.draw_texture('lane', 'lane_hit_effect', frame=2, index=self.is_2p, fade=self.fade.attribute)
elif self.type == DrumType.DON:
tex.draw_texture('lane', 'lane_hit_effect', frame=0, index=self.is_2p, fade=self.fade.attribute) tex.draw_texture('lane', 'lane_hit_effect', frame=0, index=self.is_2p, fade=self.fade.attribute)
elif self.type == DrumType.KAT: elif self.type == DrumType.KAT:
tex.draw_texture('lane', 'lane_hit_effect', frame=1, index=self.is_2p, fade=self.fade.attribute) tex.draw_texture('lane', 'lane_hit_effect', frame=1, index=self.is_2p, fade=self.fade.attribute)
if self.judgment == Judgments.GOOD or self.judgment == Judgments.OK:
tex.draw_texture('lane', 'lane_hit_effect', frame=2, index=self.is_2p, fade=self.fade.attribute)
class DrumHitEffect: class DrumHitEffect:
"""Display the side of the drum hit""" """Display the side of the drum hit"""
@@ -1609,6 +1629,8 @@ class GaugeHitEffect:
class NoteArc: class NoteArc:
"""Note arcing from the player to the gauge""" """Note arcing from the player to the gauge"""
_arc_points_cache = {}
def __init__(self, note_type: int, current_ms: float, player_num: PlayerNum, big: bool, is_balloon: bool, start_x: float = 0, start_y: float = 0): def __init__(self, note_type: int, current_ms: float, player_num: PlayerNum, big: bool, is_balloon: bool, start_x: float = 0, start_y: float = 0):
self.note_type = note_type self.note_type = note_type
self.is_big = big self.is_big = big
@@ -1642,13 +1664,20 @@ class NoteArc:
self.x_i = self.start_x self.x_i = self.start_x
self.y_i = self.start_y self.y_i = self.start_y
self.is_finished = False self.is_finished = False
self.arc_points_cache = []
cache_key = (self.start_x, self.start_y, self.end_x, self.end_y, self.control_x, self.control_y, self.arc_points)
if cache_key not in NoteArc._arc_points_cache:
arc_points_list = []
for i in range(self.arc_points + 1): for i in range(self.arc_points + 1):
t = i / self.arc_points t = i / self.arc_points
t_inv = 1.0 - t t_inv = 1.0 - t
x = int(t_inv * t_inv * self.start_x + 2 * t_inv * t * self.control_x + t * t * self.end_x) x = int(t_inv * t_inv * self.start_x + 2 * t_inv * t * self.control_x + t * t * self.end_x)
y = int(t_inv * t_inv * self.start_y + 2 * t_inv * t * self.control_y + t * t * self.end_y) y = int(t_inv * t_inv * self.start_y + 2 * t_inv * t * self.control_y + t * t * self.end_y)
self.arc_points_cache.append((x, y)) arc_points_list.append((x, y))
NoteArc._arc_points_cache[cache_key] = arc_points_list
self.arc_points_cache = NoteArc._arc_points_cache[cache_key]
self.explosion_x, self.explosion_y = self.arc_points_cache[0] self.explosion_x, self.explosion_y = self.arc_points_cache[0]
self.explosion_anim = tex.get_animation(22) self.explosion_anim = tex.get_animation(22)

View File

@@ -12,7 +12,7 @@ from libs.audio import audio
from libs.background import Background from libs.background import Background
from libs.global_data import Modifiers, PlayerNum, global_data from libs.global_data import Modifiers, PlayerNum, global_data
from libs.texture import tex from libs.texture import tex
from libs.tja import ( from libs.parsers.tja import (
Balloon, Balloon,
Drumroll, Drumroll,
NoteType, NoteType,
@@ -32,6 +32,7 @@ from scenes.game import (
DrumType, DrumType,
GameScreen, GameScreen,
JudgeCounter, JudgeCounter,
Judgments,
LaneHitEffect, LaneHitEffect,
Player, Player,
Side, Side,
@@ -46,16 +47,16 @@ class PracticeGameScreen(GameScreen):
def init_tja(self, song: Path): def init_tja(self, song: Path):
"""Initialize the TJA file""" """Initialize the TJA file"""
self.tja = TJAParser(song, start_delay=self.start_delay) self.parser = TJAParser(song, start_delay=self.start_delay)
self.scrobbling_tja = TJAParser(song, start_delay=self.start_delay) self.scrobbling_tja = TJAParser(song, start_delay=self.start_delay)
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']) 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.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: 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.tja.metadata.wave, 'song') self.song_music = audio.load_music_stream(self.parser.metadata.wave, 'song')
self.player_1 = PracticePlayer(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.player_1 = PracticePlayer(self.parser, global_data.player_num, global_data.session_data[global_data.player_num].selected_difficulty, False, global_data.modifiers[global_data.player_num])
notes, branch_m, branch_e, branch_n = self.tja.notes_to_position(self.player_1.difficulty) notes, branch_m, branch_e, branch_n = self.parser.notes_to_position(self.player_1.difficulty)
self.scrobble_timeline = notes.timeline self.scrobble_timeline = notes.timeline
_, self.scrobble_note_list, self.bars = apply_modifiers(notes, self.player_1.modifiers) _, self.scrobble_note_list, self.bars = apply_modifiers(notes, self.player_1.modifiers)
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) self.start_ms = (get_current_ms() - self.parser.metadata.offset*1000)
self.scrobble_index = 0 self.scrobble_index = 0
self.scrobble_time = self.bars[self.scrobble_index].hit_ms self.scrobble_time = self.bars[self.scrobble_index].hit_ms
self.scrobble_move = Animation.create_move(200, total_distance=0) self.scrobble_move = Animation.create_move(200, total_distance=0)
@@ -98,7 +99,7 @@ class PracticeGameScreen(GameScreen):
start_time = self.bars[previous_bar_index].hit_ms - first_bar_time + self.start_delay start_time = self.bars[previous_bar_index].hit_ms - first_bar_time + self.start_delay
tja_copy = copy.deepcopy(self.scrobbling_tja) tja_copy = copy.deepcopy(self.scrobbling_tja)
self.player_1.tja = tja_copy self.player_1.parser = tja_copy
self.player_1.reset_chart() self.player_1.reset_chart()
self.player_1.don_notes = deque([note for note in self.player_1.don_notes if note.hit_ms > resume_time]) self.player_1.don_notes = deque([note for note in self.player_1.don_notes if note.hit_ms > resume_time])
@@ -110,7 +111,7 @@ class PracticeGameScreen(GameScreen):
self.pause_time = start_time self.pause_time = start_time
audio.play_music_stream(self.song_music, 'music') audio.play_music_stream(self.song_music, 'music')
audio.seek_music_stream(self.song_music, (self.pause_time - self.start_delay)/1000 - self.tja.metadata.offset) audio.seek_music_stream(self.song_music, (self.pause_time - self.start_delay)/1000 - self.parser.metadata.offset)
self.song_started = True self.song_started = True
self.start_ms = get_current_ms() - self.pause_time self.start_ms = get_current_ms() - self.pause_time
@@ -156,7 +157,7 @@ class PracticeGameScreen(GameScreen):
if self.transition.is_finished: if self.transition.is_finished:
self.start_song(self.current_ms) self.start_song(self.current_ms)
else: 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_background(current_time)
if self.song_music is not None: if self.song_music is not None:
@@ -311,7 +312,7 @@ class PracticePlayer(Player):
self.check_note(ms_from_start, drum_type, current_time, background) self.check_note(ms_from_start, drum_type, current_time, background)
def spawn_hit_effects(self, drum_type: DrumType, side: Side): def spawn_hit_effects(self, drum_type: DrumType, side: Side):
self.lane_hit_effect = LaneHitEffect(drum_type, self.is_2p) self.lane_hit_effect = LaneHitEffect(drum_type, Judgments.BAD, self.is_2p)
self.draw_drum_hit_list.append(PracticeDrumHitEffect(drum_type, side, self.is_2p, player_num=self.player_num)) self.draw_drum_hit_list.append(PracticeDrumHitEffect(drum_type, side, self.is_2p, player_num=self.player_num))
def draw_overlays(self, mask_shader: ray.Shader): def draw_overlays(self, mask_shader: ray.Shader):

View File

@@ -3,9 +3,10 @@ import logging
import pyray as ray import pyray as ray
from libs.animation import Animation
from libs.audio import audio from libs.audio import audio
from libs.config import save_config from libs.config import get_key_string, save_config
from libs.global_objects import Indicator from libs.global_objects import AllNetIcon, CoinOverlay, Indicator
from libs.screen import Screen from libs.screen import Screen
from libs.texture import tex from libs.texture import tex
from libs.utils import ( from libs.utils import (
@@ -21,34 +22,281 @@ from libs.utils import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseOptionBox: class BaseOptionBox:
def __init__(self, name: str, description: str): def __init__(self, name: str, description: str, path: str, values: dict):
self.name = OutlinedText(name, 30, ray.WHITE) self.name = OutlinedText(name, 30, ray.WHITE)
self.setting_header, self.setting_name = path.split('/')
self.description = description self.description = description
self.is_highlighted = False self.is_highlighted = False
self.value = global_data.config[self.setting_header][self.setting_name]
def update(self, current_time):
pass
def move_left(self):
pass
def move_right(self):
pass
def confirm(self):
global_data.config[self.setting_header][self.setting_name] = self.value
def __repr__(self):
return str(self.__dict__)
def draw(self): def draw(self):
tex.draw_texture('background', 'overlay', scale=0.70)
if self.is_highlighted: if self.is_highlighted:
tex.draw_texture('background', 'title_highlight') tex.draw_texture('background', 'title_highlight')
else: else:
tex.draw_texture('background', 'title') tex.draw_texture('background', 'title')
text_x = tex.textures['background']['title'].x[0] + (tex.textures['background']['title'].width//2) - (self.name.texture.width//2) text_x = tex.textures['background']['title'].x[0] + (tex.textures['background']['title'].width//2) - (self.name.texture.width//2)
text_y = tex.textures['background']['title'].y[0] text_y = tex.textures['background']['title'].y[0] + self.name.texture.height//4
self.name.draw(outline_color=ray.BLACK, x=text_x, y=text_y) self.name.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
ray.draw_text_ex(global_data.font, self.description, (450 * tex.screen_scale, 270 * tex.screen_scale), 25 * tex.screen_scale, 1, ray.BLACK)
class BoolOptionBox(BaseOptionBox):
def __init__(self, name: str, description: str, path: str, values: dict):
super().__init__(name, description, path, values)
language = global_data.config["general"]["language"]
self.on_value = OutlinedText(values["true"].get(language, values["true"]["en"]), int(30 * tex.screen_scale), ray.WHITE)
self.off_value = OutlinedText(values["false"].get(language, values["false"]["en"]), int(30 * tex.screen_scale), ray.WHITE)
def move_left(self):
self.value = False
def move_right(self):
self.value = True
def draw(self):
super().draw()
if not self.value:
tex.draw_texture('option', 'button_on', index=0)
else:
tex.draw_texture('option', 'button_off', index=0)
text_x = tex.textures["option"]["button_on"].x[0] + (tex.textures["option"]["button_on"].width//2) - (self.off_value.texture.width//2)
text_y = tex.textures["option"]["button_on"].y[0] + (tex.textures["option"]["button_on"].height//2) - (self.off_value.texture.height//2)
self.off_value.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
if self.value:
tex.draw_texture('option', 'button_on', index=1)
else:
tex.draw_texture('option', 'button_off', index=1)
text_x = tex.textures["option"]["button_on"].x[1] + (tex.textures["option"]["button_on"].width//2) - (self.on_value.texture.width//2)
text_y = tex.textures["option"]["button_on"].y[1] + (tex.textures["option"]["button_on"].height//2) - (self.on_value.texture.height//2)
self.on_value.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
class IntOptionBox(BaseOptionBox):
def __init__(self, name: str, description: str, path: str, values: dict):
super().__init__(name, description, path, values)
self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE)
self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True)
self.flicker_fade.start()
language = global_data.config["general"]["language"]
self.value_list = []
if values != dict():
self.value_list = list(values.keys())
self.value_index = 0
self.values = values
self.value_text = OutlinedText(self.values[str(self.value)].get(language, self.values[str(self.value)]["en"]), int(30 * tex.screen_scale), ray.WHITE)
def update(self, current_time):
self.flicker_fade.update(current_time)
def move_left(self):
if self.value_list:
self.value_index = max(self.value_index - 1, 0)
self.value = int(self.value_list[self.value_index])
self.value_text = OutlinedText(self.values[str(self.value)].get(global_data.config["general"]["language"], self.values[str(self.value)]["en"]), int(30 * tex.screen_scale), ray.WHITE)
else:
self.value -= 1
self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE)
def move_right(self):
if self.value_list:
self.value_index = min(self.value_index + 1, len(self.value_list) - 1)
self.value = int(self.value_list[self.value_index])
self.value_text = OutlinedText(self.values[str(self.value)].get(global_data.config["general"]["language"], self.values[str(self.value)]["en"]), int(30 * tex.screen_scale), ray.WHITE)
else:
self.value += 1
self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE)
def draw(self):
super().draw()
tex.draw_texture('option', 'button_off', index=2)
if self.is_highlighted:
tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute)
text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2)
text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2)
self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
class StrOptionBox(BaseOptionBox):
def __init__(self, name: str, description: str, path: str, values: dict):
super().__init__(name, description, path, values)
self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True)
self.flicker_fade.start()
self.value_list = []
if values != dict():
self.value_list = list(values.keys())
self.value_index = 0
self.values = values
language = global_data.config["general"]["language"]
self.value_text = OutlinedText(self.values[self.value].get(language, self.values[self.value]["en"]), int(30 * tex.screen_scale), ray.WHITE)
else:
self.string = self.value
self.value_text = OutlinedText(self.value, int(30 * tex.screen_scale), ray.WHITE)
def update(self, current_time):
self.flicker_fade.update(current_time)
if self.is_highlighted and self.value_list == []:
if ray.is_key_pressed(ray.KeyboardKey.KEY_BACKSPACE):
self.string = self.string[:-1]
self.value_text = OutlinedText(self.string, int(30 * tex.screen_scale), ray.WHITE)
elif ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER):
self.value = self.string
self.is_highlighted = False
key = ray.get_char_pressed()
if key > 0:
self.string += chr(key)
self.value_text = OutlinedText(self.string, int(30 * tex.screen_scale), ray.WHITE)
def move_left(self):
if self.value_list:
self.value_index = max(self.value_index - 1, 0)
self.value = self.value_list[self.value_index]
self.value_text = OutlinedText(self.values[self.value].get(global_data.config["general"]["language"], self.values[self.value]["en"]), int(30 * tex.screen_scale), ray.WHITE)
def move_right(self):
if self.value_list:
self.value_index = min(self.value_index + 1, len(self.value_list) - 1)
self.value = self.value_list[self.value_index]
self.value_text = OutlinedText(self.values[self.value].get(global_data.config["general"]["language"], self.values[self.value]["en"]), int(30 * tex.screen_scale), ray.WHITE)
def draw(self):
super().draw()
tex.draw_texture('option', 'button_off', index=2)
if self.is_highlighted:
tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute)
text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2)
text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2)
self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
class KeybindOptionBox(BaseOptionBox):
def __init__(self, name: str, description: str, path: str, values: dict):
super().__init__(name, description, path, values)
if isinstance(self.value, list):
text = ', '.join([get_key_string(key) for key in self.value])
else:
text = get_key_string(self.value)
self.value_text = OutlinedText(text, int(30 * tex.screen_scale), ray.WHITE)
self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True)
self.flicker_fade.start()
def update(self, current_time):
self.flicker_fade.update(current_time)
if self.is_highlighted:
key = ray.get_key_pressed()
if key > 0:
self.value = key
audio.play_sound('don', 'sound')
self.value_text = OutlinedText(get_key_string(self.value), int(30 * tex.screen_scale), ray.WHITE)
self.is_highlighted = False
def draw(self):
super().draw()
tex.draw_texture('option', 'button_off', index=2)
if self.is_highlighted:
tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute)
text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2)
text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2)
self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
class KeyBindControllerOptionBox(BaseOptionBox):
def __init__(self, name: str, description: str, path: str, values: dict):
super().__init__(name, description, path, values)
if isinstance(self.value, list):
text = ', '.join([str(key) for key in self.value])
else:
text = str(self.value)
self.value_text = OutlinedText(text, int(30 * tex.screen_scale), ray.WHITE)
self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True)
self.flicker_fade.start()
def update(self, current_time):
self.flicker_fade.update(current_time)
if self.is_highlighted:
key = ray.get_gamepad_button_pressed()
if key > 0:
self.value = key
audio.play_sound('don', 'sound')
self.value_text = OutlinedText(str(self.value), int(30 * tex.screen_scale), ray.WHITE)
self.is_highlighted = False
def draw(self):
super().draw()
tex.draw_texture('option', 'button_off', index=2)
if self.is_highlighted:
tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute)
text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2)
text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2)
self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
class FloatOptionBox(BaseOptionBox):
def __init__(self, name: str, description: str, path: str, values: dict):
super().__init__(name, description, path, values)
self.value_text = OutlinedText(str(int(self.value*100)) + "%", int(30 * tex.screen_scale), ray.WHITE)
self.flicker_fade = Animation.create_fade(400, initial_opacity=0.0, final_opacity=1.0, reverse_delay=0, loop=True)
self.flicker_fade.start()
def update(self, current_time):
self.flicker_fade.update(current_time)
def move_left(self):
self.value = ((self.value*100) - 1) / 100
self.value_text = OutlinedText(str(int(self.value*100))+"%", int(30 * tex.screen_scale), ray.WHITE)
def move_right(self):
self.value = ((self.value*100) + 1) / 100
self.value_text = OutlinedText(str(int(self.value*100))+"%", int(30 * tex.screen_scale), ray.WHITE)
def draw(self):
super().draw()
tex.draw_texture('option', 'button_off', index=2)
if self.is_highlighted:
tex.draw_texture('option', 'button_on', index=2, fade=self.flicker_fade.attribute)
text_x = tex.textures["option"]["button_on"].x[2] + (tex.textures["option"]["button_on"].width//2) - (self.value_text.texture.width//2)
text_y = tex.textures["option"]["button_on"].y[2] + (tex.textures["option"]["button_on"].height//2) - (self.value_text.texture.height//2)
self.value_text.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
class Box: class Box:
"""Box class for the entry screen""" """Box class for the entry screen"""
def __init__(self, text: OutlinedText, box_options: dict): OPTION_BOX_MAP = {
self.text = text "int": IntOptionBox,
"bool": BoolOptionBox,
"string": StrOptionBox,
"keybind": KeybindOptionBox,
"keybind_controller": KeyBindControllerOptionBox,
"float": FloatOptionBox
}
def __init__(self, name: str, text: str, box_options: dict):
self.name = name
self.text = OutlinedText(text, tex.skin_config["entry_box_text"].font_size - int(5*tex.screen_scale), ray.WHITE, outline_thickness=5)
self.x = 10 * tex.screen_scale self.x = 10 * tex.screen_scale
self.y = -50 * tex.screen_scale self.y = -50 * tex.screen_scale
self.move = tex.get_animation(0) self.move = tex.get_animation(0)
self.blue_arrow_fade = tex.get_animation(1)
self.blue_arrow_move = tex.get_animation(2)
self.is_selected = False self.is_selected = False
self.in_box = False
self.outline_color = ray.Color(109, 68, 24, 255) self.outline_color = ray.Color(109, 68, 24, 255)
self.direction = 1 self.direction = 1
self.target_position = float('inf') self.target_position = float('inf')
self.start_position = self.y self.start_position = self.y
self.option_index = 0
language = global_data.config["general"]["language"] language = global_data.config["general"]["language"]
self.options = [BaseOptionBox(box_options[option]["name"][language], box_options[option]["description"][language]) for option in box_options] self.options = [Box.OPTION_BOX_MAP[
box_options[option]["type"]](box_options[option]["name"].get(language, box_options[option]["name"]["en"]),
box_options[option]["description"].get(language, box_options[option]["description"]["en"]), box_options[option]["path"],
box_options[option]["values"]) for option in box_options]
def __repr__(self): def __repr__(self):
return str(self.__dict__) return str(self.__dict__)
@@ -81,13 +329,41 @@ class Box:
return True return True
def move_option_left(self):
if self.options[self.option_index].is_highlighted:
self.options[self.option_index].move_left()
return True
else:
if self.option_index == 0:
self.in_box = False
return False
self.option_index -= 1
return True
def move_option_right(self):
if self.options[self.option_index].is_highlighted:
self.options[self.option_index].move_right()
else:
self.option_index = min(self.option_index + 1, len(self.options) - 1)
def select_option(self):
self.options[self.option_index].is_highlighted = not self.options[self.option_index].is_highlighted
self.options[self.option_index].confirm()
def select(self):
self.in_box = True
def update(self, current_time_ms: float, is_selected: bool): def update(self, current_time_ms: float, is_selected: bool):
self.move.update(current_time_ms) self.move.update(current_time_ms)
self.blue_arrow_fade.update(current_time_ms)
self.blue_arrow_move.update(current_time_ms)
self.is_selected = is_selected self.is_selected = is_selected
if self.move.is_finished: if self.move.is_finished:
self.y = self.target_position self.y = self.target_position
else: else:
self.y = self.start_position + (self.move.attribute * self.direction) self.y = self.start_position + (self.move.attribute * self.direction)
for option in self.options:
option.update(current_time_ms)
def _draw_highlighted(self): def _draw_highlighted(self):
tex.draw_texture('box', 'box_highlight', x=self.x, y=self.y) tex.draw_texture('box', 'box_highlight', x=self.x, y=self.y)
@@ -97,6 +373,9 @@ class Box:
text_y = self.y + (tex.textures['box']['box'].height//2) - (self.text.texture.height//2) text_y = self.y + (tex.textures['box']['box'].height//2) - (self.text.texture.height//2)
if self.is_selected: if self.is_selected:
self.text.draw(outline_color=ray.BLACK, x=text_x, y=text_y) self.text.draw(outline_color=ray.BLACK, x=text_x, y=text_y)
else:
if self.name == 'exit':
self.text.draw(outline_color=ray.RED, x=text_x, y=text_y)
else: else:
self.text.draw(outline_color=self.outline_color, x=text_x, y=text_y) self.text.draw(outline_color=self.outline_color, x=text_x, y=text_y)
@@ -104,16 +383,22 @@ class Box:
tex.draw_texture('box', 'box', x=self.x, y=self.y) tex.draw_texture('box', 'box', x=self.x, y=self.y)
if self.is_selected: if self.is_selected:
self._draw_highlighted() self._draw_highlighted()
if self.in_box:
self.options[self.option_index].draw()
if not self.options[self.option_index].is_highlighted:
tex.draw_texture('background', 'blue_arrow', index=0, x=-self.blue_arrow_move.attribute, fade=self.blue_arrow_fade.attribute)
if self.option_index != len(self.options) - 1:
tex.draw_texture('background', 'blue_arrow', index=1, x=self.blue_arrow_move.attribute, fade=self.blue_arrow_fade.attribute, mirror='horizontal')
self._draw_text() self._draw_text()
class BoxManager: class BoxManager:
"""BoxManager class for the entry screen""" """BoxManager class for the entry screen"""
def __init__(self, settings_template: dict): def __init__(self, settings_template: dict):
language = global_data.config["general"]["language"] language = global_data.config["general"]["language"]
self.boxes = [Box(OutlinedText(settings_template[config_name]["name"][language], tex.skin_config["entry_box_text"].font_size - int(5*tex.screen_scale), ray.WHITE, outline_thickness=5), settings_template[config_name]["options"]) for config_name in settings_template] self.boxes = [Box(config_name, settings_template[config_name]["name"].get(language, settings_template[config_name]["name"]["en"]), settings_template[config_name]["options"]) for config_name in settings_template]
self.num_boxes = len(self.boxes) self.num_boxes = len(self.boxes)
self.selected_box_index = 3 self.selected_box_index = 3
self.is_2p = False self.box_selected = False
for i, box in enumerate(self.boxes): for i, box in enumerate(self.boxes):
box.y += 100*i box.y += 100*i
@@ -121,6 +406,10 @@ class BoxManager:
def move_left(self): def move_left(self):
"""Move the cursor to the left""" """Move the cursor to the left"""
if self.box_selected:
box = self.boxes[self.selected_box_index]
self.box_selected = box.move_option_left()
else:
moved = True moved = True
for box in self.boxes: for box in self.boxes:
if not box.move_left(): if not box.move_left():
@@ -131,6 +420,10 @@ class BoxManager:
def move_right(self): def move_right(self):
"""Move the cursor to the right""" """Move the cursor to the right"""
if self.box_selected:
box = self.boxes[self.selected_box_index]
box.move_option_right()
else:
moved = True moved = True
for box in self.boxes: for box in self.boxes:
if not box.move_right(): if not box.move_right():
@@ -139,9 +432,19 @@ class BoxManager:
if moved: if moved:
self.selected_box_index = (self.selected_box_index + 1) % self.num_boxes self.selected_box_index = (self.selected_box_index + 1) % self.num_boxes
def select_box(self):
if self.boxes[self.selected_box_index].name == "exit":
return "exit"
if self.box_selected:
box = self.boxes[self.selected_box_index]
box.select_option()
else:
self.box_selected = True
self.boxes[self.selected_box_index].in_box = True
def update(self, current_time_ms: float): def update(self, current_time_ms: float):
for i, box in enumerate(self.boxes): for i, box in enumerate(self.boxes):
is_selected = i == self.selected_box_index is_selected = i == self.selected_box_index and not self.box_selected
box.update(current_time_ms, is_selected) box.update(current_time_ms, is_selected)
def draw(self): def draw(self):
@@ -151,17 +454,23 @@ class BoxManager:
class SettingsScreen(Screen): class SettingsScreen(Screen):
def on_screen_start(self): def on_screen_start(self):
super().on_screen_start() super().on_screen_start()
self.config = global_data.config
self.indicator = Indicator(Indicator.State.SELECT) self.indicator = Indicator(Indicator.State.SELECT)
self.template = json.loads((tex.graphics_path / "settings_template.json").read_text(encoding='utf-8')) self.template = json.loads((tex.graphics_path / "settings_template.json").read_text(encoding='utf-8'))
self.box_manager = BoxManager(self.template) self.box_manager = BoxManager(self.template)
self.coin_overlay = CoinOverlay()
self.allnet_indicator = AllNetIcon()
audio.play_sound('bgm', 'music')
def on_screen_end(self, next_screen: str): def on_screen_end(self, next_screen: str):
save_config(self.config) save_config(global_data.config)
global_data.config = self.config audio.close_audio_device()
audio.device_type = global_data.config["audio"]["device_type"]
audio.target_sample_rate = global_data.config["audio"]["sample_rate"]
audio.buffer_size = global_data.config["audio"]["buffer_size"]
audio.volume_presets = global_data.config["volume"]
audio.init_audio_device() audio.init_audio_device()
logger.info("Settings saved and audio device re-initialized") logger.info("Settings saved and audio device re-initialized")
return next_screen return super().on_screen_end(next_screen)
def handle_input(self): def handle_input(self):
if is_l_kat_pressed(): if is_l_kat_pressed():
@@ -170,19 +479,24 @@ class SettingsScreen(Screen):
elif is_r_kat_pressed(): elif is_r_kat_pressed():
audio.play_sound('kat', 'sound') audio.play_sound('kat', 'sound')
self.box_manager.move_right() self.box_manager.move_right()
elif is_l_don_pressed() or is_r_don_pressed():
audio.play_sound('don', 'sound')
box_name = self.box_manager.select_box()
if box_name == 'exit':
return self.on_screen_end("ENTRY")
def update(self): def update(self):
super().update() super().update()
self.handle_input()
current_time = get_current_ms() current_time = get_current_ms()
self.indicator.update(current_time) self.indicator.update(current_time)
self.box_manager.update(current_time) self.box_manager.update(current_time)
return self.handle_input()
def draw(self): def draw(self):
tex.draw_texture('background', 'background') tex.draw_texture('background', 'background')
self.box_manager.draw() self.box_manager.draw()
tex.draw_texture('background', 'footer') tex.draw_texture('background', 'footer')
self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y) self.indicator.draw(tex.skin_config['song_select_indicator'].x, tex.skin_config['song_select_indicator'].y)
tex.draw_texture('background', 'overlay', scale=0.70) self.coin_overlay.draw()
self.allnet_indicator.draw()

View File

@@ -16,6 +16,7 @@ from libs.file_navigator import (
GenreIndex, GenreIndex,
SongBox, SongBox,
SongFile, SongFile,
SongFileOsu,
navigator, navigator,
) )
from libs.global_data import Difficulty, Modifiers, PlayerNum from libs.global_data import Difficulty, Modifiers, PlayerNum
@@ -101,7 +102,7 @@ class SongSelectScreen(Screen):
self.navigator.mark_crowns_dirty_for_song(selected_song) self.navigator.mark_crowns_dirty_for_song(selected_song)
curr_item = self.navigator.get_current_item() curr_item = self.navigator.get_current_item()
if isinstance(curr_item, SongFile): if not isinstance(curr_item, Directory):
curr_item.box.get_scores() curr_item.box.get_scores()
self.navigator.add_recent() 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, source_loc, source_color, SHADER_UNIFORM_VEC3)
ray.set_shader_value(self.shader, target_loc, target_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].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].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 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.screen_init = False
self.reset_demo_music() self.reset_demo_music()
current_item = self.navigator.get_current_item() 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.finalize_song(current_item)
self.player_1.nameplate.unload() self.player_1.nameplate.unload()
if isinstance(current_item.box, SongBox) and current_item.box.yellow_box is not None: 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') audio.stop_sound('bgm')
return return
selected_song = self.navigator.select_current_item() 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.state = State.SONG_SELECTED
self.player_1.on_song_selected(selected_song) self.player_1.on_song_selected(selected_song)
audio.play_sound('don', 'sound') audio.play_sound('don', 'sound')
@@ -284,12 +285,12 @@ class SongSelectScreen(Screen):
def check_for_selection(self): 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: 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() 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") raise Exception("picked directory")
title = selected_song.tja.metadata.title.get( title = selected_song.parser.metadata.title.get(
global_data.config['general']['language'], '') global_data.config['general']['language'], '')
subtitle = selected_song.tja.metadata.subtitle.get( subtitle = selected_song.parser.metadata.subtitle.get(
global_data.config['general']['language'], '') global_data.config['general']['language'], '')
self.game_transition = Transition(title, subtitle) self.game_transition = Transition(title, subtitle)
self.game_transition.start() self.game_transition.start()
@@ -358,12 +359,12 @@ class SongSelectScreen(Screen):
if not isinstance(song, Directory) and song.box.is_open: 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): if self.demo_song is None and current_time >= song.box.wait + (83.33*3):
song.box.get_scores() song.box.get_scores()
if song.tja.metadata.wave.exists() and song.tja.metadata.wave.is_file(): if song.parser.metadata.wave.exists() and song.parser.metadata.wave.is_file():
self.demo_song = audio.load_music_stream(song.tja.metadata.wave, 'demo_song') self.demo_song = audio.load_music_stream(song.parser.metadata.wave, 'demo_song')
audio.play_music_stream(self.demo_song, 'music') 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') 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: if song.box.is_open:
current_box = song.box current_box = song.box
if not isinstance(current_box, BackBox) and current_time >= song.box.wait + (83.33*3): 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 != []: if self.state == State.BROWSING and self.navigator.items != []:
curr_item = self.navigator.get_current_item() curr_item = self.navigator.get_current_item()
if isinstance(curr_item, SongFile): if not isinstance(curr_item, Directory):
curr_item.box.draw_score_history() curr_item.box.draw_score_history()
self.draw_overlay() self.draw_overlay()
@@ -508,10 +509,10 @@ class SongSelectPlayer:
def on_song_selected(self, selected_song): def on_song_selected(self, selected_song):
"""Called when a song is selected""" """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 self.is_ura = False
elif (Difficulty.URA in selected_song.tja.metadata.course_data and elif (Difficulty.URA in selected_song.parser.metadata.course_data and
Difficulty.ONI not in selected_song.tja.metadata.course_data): Difficulty.ONI not in selected_song.parser.metadata.course_data):
self.is_ura = True self.is_ura = True
def handle_input_browsing(self, last_moved, selected_item): 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): if is_l_kat_pressed(self.player_num) or is_r_kat_pressed(self.player_num):
audio.play_sound('kat', 'sound') audio.play_sound('kat', 'sound')
selected_song = current_item 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 prev_diff = self.selected_difficulty
ret_val = None ret_val = None

View File

@@ -6,7 +6,7 @@ import pyray as ray
from libs.audio import audio from libs.audio import audio
from libs.global_data import PlayerNum from libs.global_data import PlayerNum
from libs.tja import TJAParser from libs.parsers.tja import TJAParser
from libs.utils import get_current_ms, global_data from libs.utils import get_current_ms, global_data
from libs.video import VideoPlayer from libs.video import VideoPlayer
from scenes.game import ( from scenes.game import (
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
class TwoPlayerGameScreen(GameScreen): class TwoPlayerGameScreen(GameScreen):
def on_screen_start(self): def on_screen_start(self):
super().on_screen_start() super().on_screen_start()
scene_preset = self.tja.metadata.scene_preset scene_preset = self.parser.metadata.scene_preset
if self.background is not None: if self.background is not None:
self.background.unload() self.background.unload()
self.background = Background(PlayerNum.TWO_PLAYER, self.bpm, scene_preset=scene_preset) self.background = Background(PlayerNum.TWO_PLAYER, self.bpm, scene_preset=scene_preset)
@@ -83,20 +83,20 @@ class TwoPlayerGameScreen(GameScreen):
def init_tja(self, song: Path): def init_tja(self, song: Path):
"""Initialize the TJA file""" """Initialize the TJA file"""
self.tja = TJAParser(song, start_delay=self.start_delay) self.parser = TJAParser(song, start_delay=self.start_delay)
if self.tja.metadata.bgmovie != Path() and self.tja.metadata.bgmovie.exists(): if self.parser.metadata.bgmovie != Path() and self.parser.metadata.bgmovie.exists():
self.movie = VideoPlayer(self.tja.metadata.bgmovie) self.movie = VideoPlayer(self.parser.metadata.bgmovie)
self.movie.set_volume(0.0) self.movie.set_volume(0.0)
else: else:
self.movie = None self.movie = None
global_data.session_data[PlayerNum.P1].song_title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en']) global_data.session_data[PlayerNum.P1].song_title = self.parser.metadata.title.get(global_data.config['general']['language'].lower(), self.parser.metadata.title['en'])
if self.tja.metadata.wave.exists() and self.tja.metadata.wave.is_file() and self.song_music is None: 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.tja.metadata.wave, 'song') self.song_music = audio.load_music_stream(self.parser.metadata.wave, 'song')
tja_copy = copy.deepcopy(self.tja) tja_copy = copy.deepcopy(self.parser)
self.player_1 = Player(self.tja, PlayerNum.P1, global_data.session_data[PlayerNum.P1].selected_difficulty, False, global_data.modifiers[PlayerNum.P1]) self.player_1 = Player(self.parser, PlayerNum.P1, global_data.session_data[PlayerNum.P1].selected_difficulty, False, global_data.modifiers[PlayerNum.P1])
self.player_2 = Player(tja_copy, PlayerNum.P2, global_data.session_data[PlayerNum.P2].selected_difficulty, True, global_data.modifiers[PlayerNum.P2]) self.player_2 = Player(tja_copy, PlayerNum.P2, global_data.session_data[PlayerNum.P2].selected_difficulty, True, global_data.modifiers[PlayerNum.P2])
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) self.start_ms = (get_current_ms() - self.parser.metadata.offset*1000)
logger.info(f"TJA initialized for two-player song: {song}") logger.info(f"TJA initialized for two-player song: {song}")
def spawn_ending_anims(self): def spawn_ending_anims(self):
@@ -122,7 +122,7 @@ class TwoPlayerGameScreen(GameScreen):
if self.transition.is_finished: if self.transition.is_finished:
self.start_song(self.current_ms) self.start_song(self.current_ms)
else: 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_background(current_time)
if self.song_music is not None: if self.song_music is not None:

View File

@@ -167,9 +167,9 @@ class TwoPlayerSongSelectScreen(SongSelectScreen):
if not isinstance(selected_song, SongFile): if not isinstance(selected_song, SongFile):
raise Exception("picked directory") raise Exception("picked directory")
title = selected_song.tja.metadata.title.get( title = selected_song.parser.metadata.title.get(
global_data.config['general']['language'], '') global_data.config['general']['language'], '')
subtitle = selected_song.tja.metadata.subtitle.get( subtitle = selected_song.parser.metadata.subtitle.get(
global_data.config['general']['language'], '') global_data.config['general']['language'], '')
self.game_transition = Transition(title, subtitle) self.game_transition = Transition(title, subtitle)
self.game_transition.start() self.game_transition.start()

View File

@@ -17,7 +17,7 @@ void main()
float outline = 0.0; float outline = 0.0;
int ringSamples = 16; int ringSamples = 16;
int rings = 2; int rings = 1;
for(int ring = 1; ring <= rings; ring++) { for(int ring = 1; ring <= rings; ring++) {
float ringRadius = float(ring) / float(rings); float ringRadius = float(ring) / float(rings);
for(int i = 0; i < ringSamples; i++) { for(int i = 0; i < ringSamples; i++) {

View File

@@ -197,33 +197,6 @@ class TestTextureWrapper(unittest.TestCase):
self.assertEqual(result, mock_animation) self.assertEqual(result, mock_animation)
@patch('libs.texture.get_config')
@patch('libs.texture.Path')
@patch('libs.texture.copy.deepcopy')
def test_get_animation_copy(self, mock_deepcopy, mock_path_cls, mock_get_config):
"""Test getting animation copy."""
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
# Mock the skin_config.json file
mock_path_instance = Mock()
mock_config_path = Mock()
mock_config_path.exists.return_value = True
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
mock_path_cls.return_value = mock_path_instance
mock_animation = Mock()
mock_copy = Mock()
mock_deepcopy.return_value = mock_copy
wrapper = TextureWrapper()
wrapper.animations = {0: mock_animation}
result = wrapper.get_animation(0, is_copy=True)
mock_deepcopy.assert_called_once_with(mock_animation)
self.assertEqual(result, mock_copy)
@patch('libs.texture.get_config') @patch('libs.texture.get_config')
@patch('libs.texture.Path') @patch('libs.texture.Path')
@patch('libs.texture.ray') @patch('libs.texture.ray')