fix double free bug, add logging, update to python 3.14

This commit is contained in:
Anthony Samms
2025-10-27 18:41:28 -04:00
parent 104ec726b0
commit 3b0a6bed97
29 changed files with 739 additions and 640 deletions

3
.gitignore vendored
View File

@@ -3,6 +3,5 @@ __pycache__
scores.db scores.db
cache cache
dev-config.toml dev-config.toml
.env
libaudio.so libaudio.so
win-libs latest.log

View File

@@ -1 +1 @@
3.12 3.14

View File

@@ -1,7 +1,9 @@
import logging
import os
import sqlite3 import sqlite3
import pyray as ray import pyray as ray
from raylib import CAMERA_ORTHOGRAPHIC
from raylib.defines import ( from raylib.defines import (
RL_FUNC_ADD, RL_FUNC_ADD,
RL_ONE, RL_ONE,
@@ -29,6 +31,8 @@ from scenes.title import TitleScreen
from scenes.two_player.song_select import TwoPlayerSongSelectScreen from scenes.two_player.song_select import TwoPlayerSongSelectScreen
logger = logging.getLogger(__name__)
class Screens: class Screens:
TITLE = "TITLE" TITLE = "TITLE"
ENTRY = "ENTRY" ENTRY = "ENTRY"
@@ -42,6 +46,22 @@ class Screens:
DEV_MENU = "DEV_MENU" DEV_MENU = "DEV_MENU"
LOADING = "LOADING" LOADING = "LOADING"
class ColoredFormatter(logging.Formatter):
COLORS = {
'DEBUG': '\033[36m', # Cyan
'INFO': '\033[32m', # Green
'WARNING': '\033[33m', # Yellow
'ERROR': '\033[31m', # Red
'CRITICAL': '\033[35m', # Magenta
}
RESET = '\033[0m'
def format(self, record):
log_color = self.COLORS.get(record.levelname, self.RESET)
record = logging.makeLogRecord(record.__dict__)
record.levelname = f"{log_color}{record.levelname}{self.RESET}"
return super().format(record)
def create_song_db(): def create_song_db():
"""Create the scores database if it doesn't exist """Create the scores database if it doesn't exist
The migration will eventually be removed""" The migration will eventually be removed"""
@@ -64,62 +84,78 @@ def create_song_db():
''' '''
cursor.execute(create_table_query) cursor.execute(create_table_query)
con.commit() con.commit()
# Migrate existing records: set clear=2 for full combos (bad=0) logger.info("Scores database created successfully")
cursor.execute("""
UPDATE Scores
SET clear = 2
WHERE bad = 0 AND (clear IS NULL OR clear <> 2)
""")
con.commit()
print("Scores database created successfully")
def main(): def main():
force_dedicated_gpu() force_dedicated_gpu()
global_data.config = get_config() global_data.config = get_config()
log_level = global_data.config["general"]["log_level"]
colored_formatter = ColoredFormatter('[%(levelname)s] %(name)s: %(message)s')
plain_formatter = logging.Formatter('[%(levelname)s] %(name)s: %(message)s')
console_handler = logging.StreamHandler()
console_handler.setFormatter(colored_formatter)
file_handler = logging.FileHandler("latest.log")
file_handler.setFormatter(plain_formatter)
logging.basicConfig(
level=log_level,
handlers=[console_handler, file_handler]
)
logger.info("Starting PyTaiko")
logger.debug(f"Loaded config: {global_data.config}")
screen_width: int = global_data.config["video"]["screen_width"] screen_width: int = global_data.config["video"]["screen_width"]
screen_height: int = global_data.config["video"]["screen_height"] screen_height: int = global_data.config["video"]["screen_height"]
if global_data.config["video"]["vsync"]: if global_data.config["video"]["vsync"]:
ray.set_config_flags(ray.ConfigFlags.FLAG_VSYNC_HINT) ray.set_config_flags(ray.ConfigFlags.FLAG_VSYNC_HINT)
logger.info("VSync enabled")
if global_data.config["video"]["target_fps"] != -1: if global_data.config["video"]["target_fps"] != -1:
ray.set_target_fps(global_data.config["video"]["target_fps"]) ray.set_target_fps(global_data.config["video"]["target_fps"])
logger.info(f"Target FPS set to {global_data.config['video']['target_fps']}")
ray.set_config_flags(ray.ConfigFlags.FLAG_MSAA_4X_HINT) ray.set_config_flags(ray.ConfigFlags.FLAG_MSAA_4X_HINT)
ray.set_trace_log_level(ray.TraceLogLevel.LOG_WARNING) ray.set_trace_log_level(ray.TraceLogLevel.LOG_WARNING)
camera = ray.Camera3D()
camera.position = ray.Vector3(0.0, 0.0, 10.0) # Camera position
camera.target = ray.Vector3(0.0, 0.0, 0.0) # Camera looking at point
camera.up = ray.Vector3(0.0, 1.0, 0.0) # Camera up vector
camera.fovy = screen_height # For orthographic, this acts as the view height
camera.projection = CAMERA_ORTHOGRAPHIC
ray.init_window(screen_width, screen_height, "PyTaiko") ray.init_window(screen_width, screen_height, "PyTaiko")
logger.info(f"Window initialized: {screen_width}x{screen_height}")
global_tex.load_screen_textures('global') global_tex.load_screen_textures('global')
logger.info("Global screen textures loaded")
global_tex.load_zip('chara', 'chara_0') global_tex.load_zip('chara', 'chara_0')
global_tex.load_zip('chara', 'chara_1') global_tex.load_zip('chara', 'chara_1')
logger.info("Chara textures loaded")
if global_data.config["video"]["borderless"]: if global_data.config["video"]["borderless"]:
ray.toggle_borderless_windowed() ray.toggle_borderless_windowed()
logger.info("Borderless window enabled")
if global_data.config["video"]["fullscreen"]: if global_data.config["video"]["fullscreen"]:
ray.toggle_fullscreen() ray.toggle_fullscreen()
logger.info("Fullscreen enabled")
current_screen = Screens.LOADING current_screen = Screens.LOADING
logger.info(f"Initial screen: {current_screen}")
audio.set_log_level(1) audio.set_log_level(1)
old_stderr = os.dup(2)
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, 2)
os.close(devnull)
audio.init_audio_device() audio.init_audio_device()
os.dup2(old_stderr, 2)
os.close(old_stderr)
logger.info("Audio device initialized")
create_song_db() create_song_db()
title_screen = TitleScreen() title_screen = TitleScreen('title')
entry_screen = EntryScreen() entry_screen = EntryScreen('entry')
song_select_screen = SongSelectScreen() song_select_screen = SongSelectScreen('song_select')
song_select_screen_2p = TwoPlayerSongSelectScreen() song_select_screen_2p = TwoPlayerSongSelectScreen('song_select')
load_screen = LoadScreen() load_screen = LoadScreen('loading')
game_screen = GameScreen() game_screen = GameScreen('game')
game_screen_2p = TwoPlayerGameScreen() game_screen_2p = TwoPlayerGameScreen('game')
result_screen = ResultScreen() result_screen = ResultScreen('result')
result_screen_2p = TwoPlayerResultScreen() result_screen_2p = TwoPlayerResultScreen('result')
settings_screen = SettingsScreen() settings_screen = SettingsScreen('settings')
dev_screen = DevScreen() dev_screen = DevScreen('dev')
screen_mapping = { screen_mapping = {
Screens.ENTRY: entry_screen, Screens.ENTRY: entry_screen,
@@ -141,12 +177,15 @@ def main():
ray.set_exit_key(ord(global_data.config["keys_1p"]["exit_key"])) ray.set_exit_key(ord(global_data.config["keys_1p"]["exit_key"]))
ray.hide_cursor() ray.hide_cursor()
logger.info("Cursor hidden")
while not ray.window_should_close(): while not ray.window_should_close():
if ray.is_key_pressed(ray.KeyboardKey.KEY_F11): if ray.is_key_pressed(ray.KeyboardKey.KEY_F11):
ray.toggle_fullscreen() ray.toggle_fullscreen()
logger.info("Toggled fullscreen")
elif ray.is_key_pressed(ray.KeyboardKey.KEY_F10): elif ray.is_key_pressed(ray.KeyboardKey.KEY_F10):
ray.toggle_borderless_windowed() ray.toggle_borderless_windowed()
logger.info("Toggled borderless windowed mode")
ray.begin_texture_mode(target) ray.begin_texture_mode(target)
ray.begin_blend_mode(ray.BlendMode.BLEND_CUSTOM_SEPARATE) ray.begin_blend_mode(ray.BlendMode.BLEND_CUSTOM_SEPARATE)
@@ -154,13 +193,12 @@ def main():
screen = screen_mapping[current_screen] screen = screen_mapping[current_screen]
next_screen = screen.update() next_screen = screen.update()
ray.clear_background(ray.BLACK) if screen.screen_init:
screen.draw() ray.clear_background(ray.BLACK)
#ray.begin_mode_3d(camera) screen.draw()
#screen.draw_3d()
# ray.end_mode_3d()
if next_screen is not None: if next_screen is not None:
logger.info(f"Screen changed from {current_screen} to {next_screen}")
current_screen = next_screen current_screen = next_screen
global_data.input_locked = 0 global_data.input_locked = 0
@@ -181,6 +219,7 @@ def main():
ray.end_drawing() ray.end_drawing()
ray.close_window() ray.close_window()
audio.close_audio_device() audio.close_audio_device()
logger.info("Window closed and audio device shut down")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -8,6 +8,7 @@ touch_enabled = false
timer_frozen = true timer_frozen = true
judge_counter = false judge_counter = false
nijiiro_notes = false nijiiro_notes = false
log_level = 30
[nameplate_1p] [nameplate_1p]
name = 'どんちゃん' name = 'どんちゃん'
@@ -47,7 +48,7 @@ right_kat = [12]
[audio] [audio]
# device_type: 0 = default, check console output from list_host_apis() for other options # device_type: 0 = default, check console output from list_host_apis() for other options
# Windows users should generally pick 3 (WASAPI) and Linux users should pick 0 (ALSA) # Windows users should generally pick 3 (WASAPI) and Linux users should pick 0 (ALSA)
device_type = 3 device_type = 0
# sample_rate: -1 = use device default (usually 44100 or 48000) # sample_rate: -1 = use device default (usually 44100 or 48000)
sample_rate = -1 sample_rate = -1
# buffer_size: Size in samples per audio buffer # buffer_size: Size in samples per audio buffer
@@ -67,4 +68,4 @@ screen_height = 720
fullscreen = false fullscreen = false
borderless = false borderless = false
target_fps = -1 target_fps = -1
vsync = true vsync = false

View File

@@ -1,6 +1,6 @@
import cffi import cffi
import ctypes
import platform import platform
import logging
from pathlib import Path from pathlib import Path
from libs.utils import get_config from libs.utils import get_config
@@ -43,6 +43,7 @@ ffi.cdef("""
// Device management // Device management
void list_host_apis(void); void list_host_apis(void);
const char* get_host_api_name(PaHostApiIndex hostApi);
void init_audio_device(PaHostApiIndex host_api, double sample_rate, unsigned long buffer_size); void init_audio_device(PaHostApiIndex host_api, double sample_rate, unsigned long buffer_size);
void close_audio_device(void); void close_audio_device(void);
bool is_audio_device_ready(void); bool is_audio_device_ready(void);
@@ -102,6 +103,8 @@ ffi.cdef("""
void free(void *ptr); void free(void *ptr);
""") """)
logger = logging.getLogger(__name__)
try: try:
if platform.system() == "Windows": if platform.system() == "Windows":
lib = ffi.dlopen("libaudio.dll") lib = ffi.dlopen("libaudio.dll")
@@ -110,23 +113,9 @@ try:
else: # Assume Linux/Unix else: # Assume Linux/Unix
lib = ffi.dlopen("./libaudio.so") lib = ffi.dlopen("./libaudio.so")
except OSError as e: except OSError as e:
print(f"Failed to load shared library: {e}") logger.error(f"Failed to load shared library: {e}")
raise raise
def get_short_path_name(long_path: str) -> str:
"""Convert long path to Windows short path (8.3 format)"""
if platform.system() != 'Windows':
return long_path
# Get short path name
buffer = ctypes.create_unicode_buffer(512)
get_short_path = ctypes.windll.kernel32.GetShortPathNameW
ret = get_short_path(long_path, buffer, 512)
if ret:
return buffer.value
return long_path
class AudioEngine: class AudioEngine:
"""Initialize an audio engine for playing sounds and music.""" """Initialize an audio engine for playing sounds and music."""
def __init__(self, device_type: int, sample_rate: float, buffer_size: int, volume_presets: dict[str, float]): def __init__(self, device_type: int, sample_rate: float, buffer_size: int, volume_presets: dict[str, float]):
@@ -150,6 +139,11 @@ class AudioEngine:
"""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 lib.list_host_apis() # type: ignore
def get_host_api_name(self, api_id: int) -> str:
"""Returns the name of the host API with the given ID"""
result = lib.get_host_api_name(api_id) # type: ignore
return ffi.string(result).decode('utf-8')
def init_audio_device(self) -> bool: def init_audio_device(self) -> bool:
"""Initialize the audio device""" """Initialize the audio device"""
try: try:
@@ -160,10 +154,10 @@ class AudioEngine:
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 = lib.load_sound(file_path_str) # type: ignore
if self.audio_device_ready: if self.audio_device_ready:
print("Audio device initialized successfully") logger.info("Audio device initialized successfully")
return self.audio_device_ready return self.audio_device_ready
except Exception as e: except Exception as e:
print(f"Failed to initialize audio device: {e}") logger.error(f"Failed to initialize audio device: {e}")
return False return False
def close_audio_device(self) -> None: def close_audio_device(self) -> None:
@@ -179,9 +173,9 @@ class AudioEngine:
lib.unload_sound(self.kat) # type: ignore lib.unload_sound(self.kat) # type: ignore
lib.close_audio_device() # type: ignore lib.close_audio_device() # type: ignore
self.audio_device_ready = False self.audio_device_ready = False
print("Audio device closed") logger.info("Audio device closed")
except Exception as e: except Exception as e:
print(f"Error closing audio device: {e}") logger.error(f"Error closing audio device: {e}")
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"""
@@ -210,10 +204,10 @@ class AudioEngine:
self.sounds[name] = sound self.sounds[name] = sound
return name return name
else: else:
print(f"Failed to load sound: {file_path}") logger.error(f"Failed to load sound: {file_path}")
return "" return ""
except Exception as e: except Exception as e:
print(f"Error loading sound {file_path}: {e}") logger.error(f"Error loading sound {file_path}: {e}")
return "" return ""
def unload_sound(self, name: str) -> None: def unload_sound(self, name: str) -> None:
@@ -222,11 +216,14 @@ class AudioEngine:
lib.unload_sound(self.sounds[name]) # type: ignore lib.unload_sound(self.sounds[name]) # type: ignore
del self.sounds[name] del self.sounds[name]
else: else:
print(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
def load_screen_sounds(self, screen_name: str) -> None: def load_screen_sounds(self, screen_name: str) -> None:
"""Load sounds for a given screen""" """Load sounds for a given screen"""
path = self.sounds_path / screen_name path = self.sounds_path / screen_name
if not path.exists():
logger.warning(f"Sounds for {screen_name} not found")
return
for sound in path.iterdir(): for sound in path.iterdir():
if sound.is_dir(): if sound.is_dir():
for file in sound.iterdir(): for file in sound.iterdir():
@@ -263,7 +260,7 @@ class AudioEngine:
lib.set_sound_volume(sound, self.volume_presets[volume_preset]) # type: ignore lib.set_sound_volume(sound, self.volume_presets[volume_preset]) # type: ignore
lib.play_sound(sound) # type: ignore lib.play_sound(sound) # type: ignore
else: else:
print(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"""
@@ -275,7 +272,7 @@ class AudioEngine:
sound = self.sounds[name] sound = self.sounds[name]
lib.stop_sound(sound) # type: ignore lib.stop_sound(sound) # type: ignore
else: else:
print(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"""
@@ -287,7 +284,7 @@ class AudioEngine:
sound = self.sounds[name] sound = self.sounds[name]
return lib.is_sound_playing(sound) # type: ignore return lib.is_sound_playing(sound) # type: ignore
else: else:
print(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
return False return False
def set_sound_volume(self, name: str, volume: float) -> None: def set_sound_volume(self, name: str, volume: float) -> None:
@@ -300,7 +297,7 @@ class AudioEngine:
sound = self.sounds[name] sound = self.sounds[name]
lib.set_sound_volume(sound, volume) # type: ignore lib.set_sound_volume(sound, volume) # type: ignore
else: else:
print(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"""
@@ -312,7 +309,7 @@ class AudioEngine:
sound = self.sounds[name] sound = self.sounds[name]
lib.set_sound_pan(sound, pan) # type: ignore lib.set_sound_pan(sound, pan) # type: ignore
else: else:
print(f"Sound {name} not found") logger.warning(f"Sound {name} not found")
# Music management # Music management
def load_music_stream(self, file_path: Path, name: str) -> str: def load_music_stream(self, file_path: Path, name: str) -> str:
@@ -326,10 +323,10 @@ class AudioEngine:
if lib.is_music_valid(music): # type: ignore if lib.is_music_valid(music): # type: ignore
self.music_streams[name] = music self.music_streams[name] = music
print(f"Loaded music stream from {file_path} as {name}") logger.info(f"Loaded music stream from {file_path} as {name}")
return name return name
else: else:
print(f"Failed to load music: {file_path}") logger.error(f"Failed to load music: {file_path}")
return "" return ""
def play_music_stream(self, name: str, volume_preset: str) -> None: def play_music_stream(self, name: str, volume_preset: str) -> None:
@@ -341,7 +338,7 @@ class AudioEngine:
lib.set_music_volume(music, self.volume_presets[volume_preset]) # type: ignore lib.set_music_volume(music, self.volume_presets[volume_preset]) # type: ignore
lib.play_music_stream(music) # type: ignore lib.play_music_stream(music) # type: ignore
else: else:
print(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
def update_music_stream(self, name: str) -> None: def update_music_stream(self, name: str) -> None:
"""Update a music stream""" """Update a music stream"""
@@ -349,7 +346,7 @@ class AudioEngine:
music = self.music_streams[name] music = self.music_streams[name]
lib.update_music_stream(music) # type: ignore lib.update_music_stream(music) # type: ignore
else: else:
print(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
def get_music_time_length(self, name: str) -> float: def get_music_time_length(self, name: str) -> float:
"""Get the time length of a music stream""" """Get the time length of a music stream"""
@@ -357,7 +354,7 @@ class AudioEngine:
music = self.music_streams[name] music = self.music_streams[name]
return lib.get_music_time_length(music) # type: ignore return lib.get_music_time_length(music) # type: ignore
else: else:
print(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
return 0.0 return 0.0
def get_music_time_played(self, name: str) -> float: def get_music_time_played(self, name: str) -> float:
@@ -366,7 +363,7 @@ class AudioEngine:
music = self.music_streams[name] music = self.music_streams[name]
return lib.get_music_time_played(music) # type: ignore return lib.get_music_time_played(music) # type: ignore
else: else:
print(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
return 0.0 return 0.0
def set_music_volume(self, name: str, volume: float) -> None: def set_music_volume(self, name: str, volume: float) -> None:
@@ -375,7 +372,7 @@ class AudioEngine:
music = self.music_streams[name] music = self.music_streams[name]
lib.set_music_volume(music, volume) # type: ignore lib.set_music_volume(music, volume) # type: ignore
else: else:
print(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
def is_music_stream_playing(self, name: str) -> bool: def is_music_stream_playing(self, name: str) -> bool:
"""Check if a music stream is playing""" """Check if a music stream is playing"""
@@ -383,7 +380,7 @@ class AudioEngine:
music = self.music_streams[name] music = self.music_streams[name]
return lib.is_music_stream_playing(music) # type: ignore return lib.is_music_stream_playing(music) # type: ignore
else: else:
print(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
return False return False
def stop_music_stream(self, name: str) -> None: def stop_music_stream(self, name: str) -> None:
@@ -392,7 +389,7 @@ class AudioEngine:
music = self.music_streams[name] music = self.music_streams[name]
lib.stop_music_stream(music) # type: ignore lib.stop_music_stream(music) # type: ignore
else: else:
print(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
def unload_music_stream(self, name: str) -> None: def unload_music_stream(self, name: str) -> None:
"""Unload a music stream""" """Unload a music stream"""
@@ -401,7 +398,7 @@ class AudioEngine:
lib.unload_music_stream(music) # type: ignore lib.unload_music_stream(music) # type: ignore
del self.music_streams[name] del self.music_streams[name]
else: else:
print(f"Music stream {name} not found") logger.warning(f"Music stream {name} not found")
def unload_all_music(self) -> None: def unload_all_music(self) -> None:
"""Unload all music streams""" """Unload all music streams"""
@@ -414,7 +411,7 @@ class AudioEngine:
music = self.music_streams[name] music = self.music_streams[name]
lib.seek_music_stream(music, position) # type: ignore lib.seek_music_stream(music, position) # type: ignore
else: else:
print(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 = AudioEngine(get_config()["audio"]["device_type"], get_config()["audio"]["sample_rate"], get_config()["audio"]["buffer_size"], get_config()["volume"])

View File

@@ -116,6 +116,7 @@ typedef struct AudioData {
} AudioData; } AudioData;
void list_host_apis(void); void list_host_apis(void);
const char* get_host_api_name(PaHostApiIndex hostApi);
void init_audio_device(PaHostApiIndex host_api, double sample_rate, unsigned long buffer_size); void init_audio_device(PaHostApiIndex host_api, double sample_rate, unsigned long buffer_size);
void close_audio_device(void); void close_audio_device(void);
bool is_audio_device_ready(void); bool is_audio_device_ready(void);
@@ -289,6 +290,16 @@ void list_host_apis(void)
} }
} }
const char* get_host_api_name(PaHostApiIndex hostApi)
{
const PaHostApiInfo *hostApiInfo = Pa_GetHostApiInfo(hostApi);
if (!hostApiInfo) {
return NULL;
}
return hostApiInfo->name;
}
PaDeviceIndex get_best_output_device_for_host_api(PaHostApiIndex hostApi) PaDeviceIndex get_best_output_device_for_host_api(PaHostApiIndex hostApi)
{ {
const PaHostApiInfo *hostApiInfo = Pa_GetHostApiInfo(hostApi); const PaHostApiInfo *hostApiInfo = Pa_GetHostApiInfo(hostApi);

View File

@@ -77,6 +77,10 @@ void set_log_level(int level);
* Print available host APIs to the console * Print available host APIs to the console
*/ */
void list_host_apis(void); void list_host_apis(void);
/**
* Get the name of a host API by index
*/
const char* get_host_api_name(PaHostApiIndex hostApi);
/** /**
* Initialize the audio device and system * Initialize the audio device and system
* Must be called before using any other audio functions * Must be called before using any other audio functions

View File

@@ -1,3 +1,4 @@
import logging
import random import random
import libs.bg_collabs import libs.bg_collabs
@@ -11,6 +12,8 @@ from libs.bg_objects.footer import Footer
from libs.bg_objects.renda import RendaController from libs.bg_objects.renda import RendaController
from libs.texture import TextureWrapper from libs.texture import TextureWrapper
logger = logging.getLogger(__name__)
class Background: class Background:
"""The background class for the game.""" """The background class for the game."""
COLLABS = { COLLABS = {
@@ -24,6 +27,7 @@ class Background:
"IMAS_SIDEM": (libs.bg_collabs.imas_sidem.Background, 'background/collab/imas_sidem', 3), "IMAS_SIDEM": (libs.bg_collabs.imas_sidem.Background, 'background/collab/imas_sidem', 3),
"DAN": (libs.bg_collabs.dan.Background, 'background/collab/dan', 1) "DAN": (libs.bg_collabs.dan.Background, 'background/collab/dan', 1)
} }
def __init__(self, player_num: int, bpm: float, scene_preset: str = ''): def __init__(self, player_num: int, bpm: float, scene_preset: str = ''):
""" """
Initialize the background class. Initialize the background class.
@@ -87,7 +91,8 @@ class Background:
self.chibi = collab_bg.chibi self.chibi = collab_bg.chibi
self.is_clear = False self.is_clear = False
self.is_rainbow = False self.is_rainbow = False
self.last_milestone = 0 self.last_milestone = 1
logger.info(f"Background initialized for player {player_num}, bpm={bpm}, scene_preset={scene_preset}")
def add_chibi(self, bad: bool, player_num: int): def add_chibi(self, bad: bool, player_num: int):
""" """
@@ -122,14 +127,17 @@ class Background:
current_milestone = min(self.max_dancers - 1, int(gauge_1p.gauge_length / (clear_threshold / self.max_dancers))) current_milestone = min(self.max_dancers - 1, int(gauge_1p.gauge_length / (clear_threshold / self.max_dancers)))
else: else:
current_milestone = self.max_dancers current_milestone = self.max_dancers
if current_milestone > self.last_milestone and current_milestone < self.max_dancers: if current_milestone > self.last_milestone and current_milestone <= self.max_dancers:
self.dancer.add_dancer() self.dancer.add_dancer()
self.last_milestone = current_milestone self.last_milestone = current_milestone
logger.info(f"Dancer milestone reached: {current_milestone}/{self.max_dancers}")
if self.bg_fever is not None: if self.bg_fever is not None:
if not self.is_clear and gauge_1p.is_clear: if not self.is_clear and gauge_1p.is_clear:
self.bg_fever.start() self.bg_fever.start()
logger.info("Fever started")
if not self.is_rainbow and gauge_1p.is_rainbow and self.fever is not None: if not self.is_rainbow and gauge_1p.is_rainbow and self.fever is not None:
self.fever.start() self.fever.start()
logger.info("Rainbow fever started")
self.is_clear = gauge_1p.is_clear self.is_clear = gauge_1p.is_clear
self.is_rainbow = gauge_1p.is_rainbow self.is_rainbow = gauge_1p.is_rainbow
self.don_bg.update(current_time_ms, self.is_clear) self.don_bg.update(current_time_ms, self.is_clear)
@@ -182,3 +190,4 @@ class Background:
Unload the background. Unload the background.
""" """
self.tex_wrapper.unload_textures() self.tex_wrapper.unload_textures()
logger.info("Background textures unloaded")

View File

@@ -1,6 +1,9 @@
import logging
from libs.animation import Animation from libs.animation import Animation
from libs.utils import global_tex from libs.utils import global_tex
logger = logging.getLogger(__name__)
class Chara2D: class Chara2D:
def __init__(self, index: int, bpm: float, path: str = 'chara'): def __init__(self, index: int, bpm: float, path: str = 'chara'):
""" """
@@ -33,6 +36,7 @@ class Chara2D:
textures = [[duration*i, duration*(i+1), index] for i, index in enumerate(keyframes)] textures = [[duration*i, duration*(i+1), index] for i, index in enumerate(keyframes)]
self.anims[name] = Animation.create_texture_change(total_duration, textures=textures) self.anims[name] = Animation.create_texture_change(total_duration, textures=textures)
self.anims[name].start() self.anims[name].start()
logger.info(f"Chara2D initialized: index={index}, bpm={bpm}, path={path}")
def set_animation(self, name: str): def set_animation(self, name: str):
""" """
@@ -62,6 +66,7 @@ class Chara2D:
self.past_anim = 'gogo' self.past_anim = 'gogo'
self.current_anim = name self.current_anim = name
self.anims[name].start() self.anims[name].start()
logger.debug(f"Animation set: {name}")
def update(self, current_time_ms: float, bpm: float, is_clear: bool, is_rainbow: bool): def update(self, current_time_ms: float, bpm: float, is_clear: bool, is_rainbow: bool):
""" """
Update the character's animation state and appearance. Update the character's animation state and appearance.
@@ -75,10 +80,12 @@ class Chara2D:
if is_rainbow and not self.is_rainbow: if is_rainbow and not self.is_rainbow:
self.is_rainbow = True self.is_rainbow = True
self.set_animation('soul_in') self.set_animation('soul_in')
logger.info("Rainbow state entered, soul_in animation triggered")
if is_clear and not self.is_clear: if is_clear and not self.is_clear:
self.is_clear = True self.is_clear = True
self.set_animation('clear_in') self.set_animation('clear_in')
self.past_anim = 'clear' self.past_anim = 'clear'
logger.info("Clear state entered, clear_in animation triggered")
if bpm != self.bpm: if bpm != self.bpm:
self.bpm = bpm self.bpm = bpm
for name in self.tex.textures[self.name]: for name in self.tex.textures[self.name]:
@@ -90,6 +97,7 @@ class Chara2D:
textures = [[duration*i, duration*(i+1), index] for i, index in enumerate(keyframes)] textures = [[duration*i, duration*(i+1), index] for i, index in enumerate(keyframes)]
self.anims[name] = Animation.create_texture_change(total_duration, textures=textures) self.anims[name] = Animation.create_texture_change(total_duration, textures=textures)
self.anims[name].start() self.anims[name].start()
logger.info(f"BPM changed, animations updated: bpm={bpm}")
self.anims[self.current_anim] = self.anims[self.current_anim] self.anims[self.current_anim] = self.anims[self.current_anim]
self.anims[self.current_anim].update(current_time_ms) self.anims[self.current_anim].update(current_time_ms)
if self.anims[self.current_anim].is_finished: if self.anims[self.current_anim].is_finished:

View File

@@ -1,3 +1,4 @@
import logging
from pathlib import Path from pathlib import Path
import random import random
from typing import Optional, Union from typing import Optional, Union
@@ -12,6 +13,8 @@ import pyray as ray
BOX_CENTER = 444 BOX_CENTER = 444
logger = logging.getLogger(__name__)
class SongBox: class SongBox:
"""A box for the song select screen.""" """A box for the song select screen."""
OUTLINE_MAP = { OUTLINE_MAP = {
@@ -132,6 +135,8 @@ class SongBox:
if self.move.is_finished: if self.move.is_finished:
self.position = self.target_position self.position = self.target_position
self.move = None self.move = None
if not (-56 <= self.position <= 1280):
self.reset()
def update(self, is_diff_select): def update(self, is_diff_select):
self.is_diff_select = is_diff_select self.is_diff_select = is_diff_select
@@ -723,12 +728,14 @@ class FileNavigator:
self.box_open = False self.box_open = False
self.genre_bg = None self.genre_bg = None
self.song_count = 0 self.song_count = 0
logger.info("FileNavigator initialized")
def initialize(self, root_dirs: list[Path]): def initialize(self, root_dirs: list[Path]):
self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in root_dirs] self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in root_dirs]
self._generate_all_objects() self._generate_all_objects()
self._create_virtual_root() self._create_virtual_root()
self.load_current_directory() self.load_current_directory()
logger.info(f"FileNavigator initialized with root_dirs: {self.root_dirs}")
def _create_virtual_root(self): def _create_virtual_root(self):
"""Create a virtual root directory containing all root directories""" """Create a virtual root directory containing all root directories"""
@@ -762,12 +769,12 @@ class FileNavigator:
def _generate_all_objects(self): def _generate_all_objects(self):
"""Generate all Directory and SongFile objects in advance""" """Generate all Directory and SongFile objects in advance"""
print("Generating all Directory and SongFile objects...") logging.info("Generating all Directory and SongFile objects...")
# Generate objects for each root directory # Generate objects for each root directory
for root_path in self.root_dirs: for root_path in self.root_dirs:
if not root_path.exists(): if not root_path.exists():
print(f"Root directory does not exist: {root_path}") logging.warning(f"Root directory does not exist: {root_path}")
continue continue
self._generate_objects_recursive(root_path) self._generate_objects_recursive(root_path)
@@ -778,7 +785,7 @@ class FileNavigator:
if str(song_obj) in self.all_song_files: if str(song_obj) in self.all_song_files:
self.all_song_files[str(song_obj)].box.is_favorite = True self.all_song_files[str(song_obj)].box.is_favorite = True
print(f"Object generation complete. " logging.info(f"Object generation complete. "
f"Directories: {len(self.all_directories)}, " f"Directories: {len(self.all_directories)}, "
f"Songs: {len(self.all_song_files)}") f"Songs: {len(self.all_song_files)}")
@@ -905,7 +912,7 @@ class FileNavigator:
global_data.song_progress = self.song_count / global_data.total_songs global_data.song_progress = self.song_count / global_data.total_songs
self.all_song_files[song_key] = song_obj self.all_song_files[song_key] = song_obj
except Exception as e: except Exception as e:
print(f"Error creating SongFile for {tja_path}: {e}") logger.error(f"Error creating SongFile for {tja_path}: {e}")
continue continue
def is_at_root(self) -> bool: def is_at_root(self) -> bool:
@@ -1191,7 +1198,7 @@ class FileNavigator:
elif line.startswith("#COLLECTION"): elif line.startswith("#COLLECTION"):
collection = line.split(":", 1)[1].strip() collection = line.split(":", 1)[1].strip()
except Exception as e: except Exception as e:
print(f"Error parsing box.def in {path}: {e}") logger.error(f"Error parsing box.def in {path}: {e}")
return name, texture_index, collection return name, texture_index, collection
@@ -1237,7 +1244,7 @@ class FileNavigator:
if file_updated: if file_updated:
with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list: with open(path / 'song_list.txt', 'w', encoding='utf-8-sig') as song_list:
for line in updated_lines: for line in updated_lines:
print("updated", line) logger.info(f"updated: {line}")
song_list.write(line + '\n') song_list.write(line + '\n')
return tja_files return tja_files
@@ -1323,7 +1330,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)
print("Added recent: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) logger.info(f"Added Recent: {song.hash} {song.tja.metadata.title['en']} {song.tja.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"""
@@ -1351,11 +1358,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')
print("Removed favorite:", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) logger.info(f"Removed Favorite: {song.hash} {song.tja.metadata.title['en']} {song.tja.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.tja.metadata.title["en"]}|{song.tja.metadata.subtitle["en"]}\n')
print("Added favorite: ", song.hash, song.tja.metadata.title['en'], song.tja.metadata.subtitle['en']) logger.info(f"Added Favorite: {song.hash} {song.tja.metadata.title['en']} {song.tja.metadata.subtitle['en']}")
return True return True
navigator = FileNavigator() navigator = FileNavigator()

41
libs/screen.py Normal file
View File

@@ -0,0 +1,41 @@
import logging
from typing import Any
from libs.audio import audio
from libs.texture import tex
logger = logging.getLogger(__name__)
class Screen:
def __init__(self, name: str):
self.screen_init = False
self.screen_name = name
def _do_screen_start(self):
if not self.screen_init:
self.screen_init = True
self.on_screen_start()
logger.info(f"{self.__class__.__name__} initialized")
def on_screen_start(self) -> Any:
tex.load_screen_textures(self.screen_name)
logger.info(f"Loaded textures for screen: {self.screen_name}")
audio.load_screen_sounds(self.screen_name)
logger.info(f"Loaded sounds for screen: {self.screen_name}")
def on_screen_end(self, next_screen: str):
self.screen_init = False
logger.info(f"{self.__class__.__name__} ended, transitioning to {next_screen} screen")
audio.unload_all_sounds()
audio.unload_all_music()
logger.info(f"Unloaded sounds for screen: {next_screen}")
tex.unload_textures()
logger.info(f"Unloaded textures for screen: {next_screen}")
return next_screen
def update(self) -> Any:
ret_val = self._do_screen_start()
if ret_val:
return ret_val
def draw(self):
pass

View File

@@ -1,5 +1,6 @@
import configparser import configparser
import csv import csv
import logging
import json import json
import sqlite3 import sqlite3
import sys import sys
@@ -9,6 +10,7 @@ from pathlib import Path
from libs.tja import NoteList, TJAParser, test_encodings from libs.tja import NoteList, TJAParser, test_encodings
from libs.utils import get_config, global_data from libs.utils import get_config, global_data
logger = logging.getLogger(__name__)
def diff_hashes_object_hook(obj): def diff_hashes_object_hook(obj):
if "diff_hashes" in obj: if "diff_hashes" in obj:
@@ -137,7 +139,7 @@ def build_song_hashes(output_dir=Path("cache")):
all_notes.bars.extend(branch.bars) all_notes.bars.extend(branch.bars)
all_notes.bars.extend(diff_notes.bars) all_notes.bars.extend(diff_notes.bars)
except Exception as e: except Exception as e:
print(f"Failed to parse TJA {tja_path}: {e}") logger.error(f"Failed to parse TJA {tja_path}: {e}")
continue continue
if all_notes == NoteList(): if all_notes == NoteList():
@@ -190,7 +192,7 @@ def build_song_hashes(output_dir=Path("cache")):
""", (diff_hashes[i], en_name, jp_name, i, imported_scores[i], clear, bads)) """, (diff_hashes[i], en_name, jp_name, i, imported_scores[i], clear, bads))
if cursor.rowcount > 0: if cursor.rowcount > 0:
action = "Added" if not existing_record else "Updated" action = "Added" if not existing_record else "Updated"
print(f"{action} entry for {en_name} ({i}) - Score: {imported_scores[i]}") logger.info(f"{action} entry for {en_name} ({i}) - Score: {imported_scores[i]}")
conn.commit() conn.commit()
conn.close() conn.close()
@@ -213,18 +215,18 @@ def build_song_hashes(output_dir=Path("cache")):
WHERE (en_name = ? AND jp_name = ?) AND diff = ? WHERE (en_name = ? AND jp_name = ?) AND diff = ?
""", (diff_hash, en_name, jp_name, diff)) """, (diff_hash, en_name, jp_name, diff))
if cursor.rowcount > 0: if cursor.rowcount > 0:
print(f"Updated {cursor.rowcount} entries for {en_name} ({diff})") logger.info(f"Updated {cursor.rowcount} entries for {en_name} ({diff})")
conn.commit() conn.commit()
conn.close() conn.close()
print(f"Database update completed. Processed {len(db_updates)} difficulty hash updates.") logger.info(f"Database update completed. Processed {len(db_updates)} difficulty hash updates.")
except sqlite3.Error as e: except sqlite3.Error as e:
print(f"Database error: {e}") logger.error(f"Database error: {e}")
except Exception as e: except Exception as e:
print(f"Error updating database: {e}") logger.error(f"Error updating database: {e}")
elif db_updates: elif db_updates:
print(f"Warning: scores.db not found, skipping {len(db_updates)} database updates") logger.warning(f"Warning: scores.db not found, skipping {len(db_updates)} database updates")
# Save both files # Save both files
with open(output_path, "w", encoding="utf-8") as f: with open(output_path, "w", encoding="utf-8") as f:
@@ -345,11 +347,11 @@ def get_japanese_songs_for_version(csv_file_path, version_column):
if len(matches) == 1: if len(matches) == 1:
path = matches[0][1] path = matches[0][1]
elif len(matches) > 1: elif len(matches) > 1:
print( logger.info(
f"Multiple matches found for '{title.split('')[0]} ({title.split('')[1] if len(title.split('')) > 1 else ''})':" f"Multiple matches found for '{title.split('')[0]} ({title.split('')[1] if len(title.split('')) > 1 else ''})':"
) )
for i, (key, path_val) in enumerate(matches, 1): for i, (key, path_val) in enumerate(matches, 1):
print(f"{i}. {key}: {path_val}") logger.info(f"{i}. {key}: {path_val}")
choice = int(input("Choose number: ")) - 1 choice = int(input("Choose number: ")) - 1
path = matches[choice][1] path = matches[choice][1]
else: else:
@@ -362,7 +364,7 @@ def get_japanese_songs_for_version(csv_file_path, version_column):
text_files[genre].append( text_files[genre].append(
f"{hash}|{tja_parse.metadata.title['en'].strip()}|{tja_parse.metadata.subtitle['en'].strip()}" f"{hash}|{tja_parse.metadata.title['en'].strip()}|{tja_parse.metadata.subtitle['en'].strip()}"
) )
print(f"Added {title}: {path}") logger.info(f"Added {title}: {path}")
for genre in text_files: for genre in text_files:
if not Path(version_column).exists(): if not Path(version_column).exists():
Path(version_column).mkdir() Path(version_column).mkdir()

View File

@@ -1,6 +1,7 @@
import copy import copy
import json import json
import os import os
import logging
import tempfile import tempfile
import zipfile import zipfile
from pathlib import Path from pathlib import Path
@@ -13,6 +14,8 @@ from libs.animation import BaseAnimation, parse_animations
SCREEN_WIDTH = 1280 SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720 SCREEN_HEIGHT = 720
logger = logging.getLogger(__name__)
class Texture: class Texture:
"""Texture class for managing textures and animations.""" """Texture class for managing textures and animations."""
def __init__(self, name: str, texture: Union[ray.Texture, list[ray.Texture]], init_vals: dict[str, int]): def __init__(self, name: str, texture: Union[ray.Texture, list[ray.Texture]], init_vals: dict[str, int]):
@@ -42,14 +45,28 @@ class TextureWrapper:
def unload_textures(self): def unload_textures(self):
"""Unload all textures and animations.""" """Unload all textures and animations."""
ids = {} # Map ID to texture name
for zip in self.textures: for zip in self.textures:
for file in self.textures[zip]: for file in self.textures[zip]:
tex_object = self.textures[zip][file] tex_object = self.textures[zip][file]
if isinstance(tex_object.texture, list): if isinstance(tex_object.texture, list):
for texture in tex_object.texture: for i, texture in enumerate(tex_object.texture):
ray.unload_texture(texture) if texture.id in ids:
logger.warning(f"Duplicate texture ID {texture.id}: {ids[texture.id]} and {zip}/{file}[{i}]")
else:
ids[texture.id] = f"{zip}/{file}[{i}]"
ray.unload_texture(texture)
else: else:
ray.unload_texture(tex_object.texture) if tex_object.texture.id in ids:
logger.warning(f"Duplicate texture ID {tex_object.texture.id}: {ids[tex_object.texture.id]} and {zip}/{file}")
else:
ids[tex_object.texture.id] = f"{zip}/{file}"
ray.unload_texture(tex_object.texture)
self.textures.clear()
self.animations.clear()
logger.info("All textures unloaded")
def get_animation(self, index: int, is_copy: bool = False): def get_animation(self, index: int, is_copy: bool = False):
"""Get an animation by ID and returns a reference. """Get an animation by ID and returns a reference.
@@ -93,52 +110,57 @@ class TextureWrapper:
if (screen_path / 'animation.json').exists(): if (screen_path / 'animation.json').exists():
with open(screen_path / 'animation.json') as json_file: with open(screen_path / 'animation.json') as json_file:
self.animations = parse_animations(json.loads(json_file.read())) self.animations = parse_animations(json.loads(json_file.read()))
logger.info(f"Animations loaded for screen: {screen_name}")
def load_zip(self, screen_name: str, subset: str): def load_zip(self, screen_name: str, subset: str):
"""Load textures from a zip file.""" """Load textures from a zip file."""
zip = (self.graphics_path / screen_name / subset).with_suffix('.zip') zip = (self.graphics_path / screen_name / subset).with_suffix('.zip')
if screen_name in self.textures and subset in self.textures[screen_name]: if screen_name in self.textures and subset in self.textures[screen_name]:
return return
with zipfile.ZipFile(zip, 'r') as zip_ref: try:
if 'texture.json' not in zip_ref.namelist(): with zipfile.ZipFile(zip, 'r') as zip_ref:
raise Exception(f"texture.json file missing from {zip}") if 'texture.json' not in zip_ref.namelist():
raise Exception(f"texture.json file missing from {zip}")
with zip_ref.open('texture.json') as json_file: with zip_ref.open('texture.json') as json_file:
tex_mapping_data = json.loads(json_file.read().decode('utf-8')) tex_mapping_data = json.loads(json_file.read().decode('utf-8'))
self.textures[zip.stem] = dict() self.textures[zip.stem] = dict()
for tex_name in tex_mapping_data: for tex_name in tex_mapping_data:
if f"{tex_name}/" in zip_ref.namelist(): if f"{tex_name}/" in zip_ref.namelist():
tex_mapping = tex_mapping_data[tex_name] tex_mapping = tex_mapping_data[tex_name]
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
zip_ref.extractall(temp_dir, members=[name for name in zip_ref.namelist() zip_ref.extractall(temp_dir, members=[name for name in zip_ref.namelist()
if name.startswith(tex_name)]) if name.startswith(tex_name)])
extracted_path = Path(temp_dir) / tex_name extracted_path = Path(temp_dir) / tex_name
if extracted_path.is_dir(): if extracted_path.is_dir():
frames = [ray.load_texture(str(frame)) for frame in sorted(extracted_path.iterdir(), frames = [ray.load_texture(str(frame)) for frame in sorted(extracted_path.iterdir(),
key=lambda x: int(x.stem)) if frame.is_file()] key=lambda x: int(x.stem)) if frame.is_file()]
else: else:
frames = [ray.load_texture(str(extracted_path))] frames = [ray.load_texture(str(extracted_path))]
self.textures[zip.stem][tex_name] = Texture(tex_name, frames, tex_mapping) self.textures[zip.stem][tex_name] = Texture(tex_name, frames, tex_mapping)
self._read_tex_obj_data(tex_mapping, self.textures[zip.stem][tex_name])
elif f"{tex_name}.png" in zip_ref.namelist():
tex_mapping = tex_mapping_data[tex_name]
png_filename = f"{tex_name}.png"
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_file:
temp_file.write(zip_ref.read(png_filename))
temp_path = temp_file.name
try:
tex = ray.load_texture(temp_path)
self.textures[zip.stem][tex_name] = Texture(tex_name, tex, tex_mapping)
self._read_tex_obj_data(tex_mapping, self.textures[zip.stem][tex_name]) self._read_tex_obj_data(tex_mapping, self.textures[zip.stem][tex_name])
finally: elif f"{tex_name}.png" in zip_ref.namelist():
os.unlink(temp_path) tex_mapping = tex_mapping_data[tex_name]
else:
raise Exception(f"Texture {tex_name} was not found in {zip}") png_filename = f"{tex_name}.png"
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_file:
temp_file.write(zip_ref.read(png_filename))
temp_path = temp_file.name
try:
tex = ray.load_texture(temp_path)
self.textures[zip.stem][tex_name] = Texture(tex_name, tex, tex_mapping)
self._read_tex_obj_data(tex_mapping, self.textures[zip.stem][tex_name])
finally:
os.unlink(temp_path)
else:
raise Exception(f"Texture {tex_name} was not found in {zip}")
logger.info(f"Textures loaded from zip: {zip}")
except Exception as e:
logger.error(f"Failed to load textures from zip {zip}: {e}")
def load_screen_textures(self, screen_name: str) -> None: def load_screen_textures(self, screen_name: str) -> None:
"""Load textures for a screen.""" """Load textures for a screen."""
@@ -146,10 +168,12 @@ class TextureWrapper:
if (screen_path / 'animation.json').exists(): if (screen_path / 'animation.json').exists():
with open(screen_path / 'animation.json') as json_file: with open(screen_path / 'animation.json') as json_file:
self.animations = parse_animations(json.loads(json_file.read())) self.animations = parse_animations(json.loads(json_file.read()))
logger.info(f"Animations loaded for screen: {screen_name}")
for zip in screen_path.iterdir(): for zip in screen_path.iterdir():
if zip.is_dir() or zip.suffix != ".zip": if zip.is_dir() or zip.suffix != ".zip":
continue continue
self.load_zip(screen_name, zip.name) self.load_zip(screen_name, zip.name)
logger.info(f"Screen textures loaded for: {screen_name}")
def control(self, tex_object: Texture, index: int = 0): def control(self, tex_object: Texture, index: int = 0):
'''debug function''' '''debug function'''
@@ -158,16 +182,16 @@ class TextureWrapper:
distance = 10 distance = 10
if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT): if ray.is_key_pressed(ray.KeyboardKey.KEY_LEFT):
tex_object.x[index] -= distance tex_object.x[index] -= distance
print(f"{tex_object.name}: {tex_object.x[index]}, {tex_object.y[index]}") logger.info(f"{tex_object.name}: {tex_object.x[index]}, {tex_object.y[index]}")
if ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT): if ray.is_key_pressed(ray.KeyboardKey.KEY_RIGHT):
tex_object.x[index] += distance tex_object.x[index] += distance
print(f"{tex_object.name}: {tex_object.x[index]}, {tex_object.y[index]}") logger.info(f"{tex_object.name}: {tex_object.x[index]}, {tex_object.y[index]}")
if ray.is_key_pressed(ray.KeyboardKey.KEY_UP): if ray.is_key_pressed(ray.KeyboardKey.KEY_UP):
tex_object.y[index] -= distance tex_object.y[index] -= distance
print(f"{tex_object.name}: {tex_object.x[index]}, {tex_object.y[index]}") logger.info(f"{tex_object.name}: {tex_object.x[index]}, {tex_object.y[index]}")
if ray.is_key_pressed(ray.KeyboardKey.KEY_DOWN): if ray.is_key_pressed(ray.KeyboardKey.KEY_DOWN):
tex_object.y[index] += distance tex_object.y[index] += distance
print(f"{tex_object.name}: {tex_object.x[index]}, {tex_object.y[index]}") logger.info(f"{tex_object.name}: {tex_object.x[index]}, {tex_object.y[index]}")
def draw_texture(self, subset: str, texture: str, color: ray.Color=ray.WHITE, frame: int = 0, scale: float = 1.0, center: bool = False, def draw_texture(self, subset: str, texture: str, color: ray.Color=ray.WHITE, frame: int = 0, scale: float = 1.0, center: bool = False,
mirror: str = '', x: float = 0, y: float = 0, x2: float = 0, y2: float = 0, mirror: str = '', x: float = 0, y: float = 0, x2: float = 0, y2: float = 0,

View File

@@ -1,6 +1,7 @@
import bisect import bisect
import hashlib import hashlib
import math import math
import logging
import random import random
from collections import deque from collections import deque
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
@@ -285,6 +286,7 @@ def test_encodings(file_path):
continue continue
return final_encoding return final_encoding
logger = logging.getLogger(__name__)
class TJAParser: class TJAParser:
"""Parse a TJA file and extract metadata and data. """Parse a TJA file and extract metadata and data.
@@ -318,6 +320,7 @@ class TJAParser:
self.metadata = TJAMetadata() self.metadata = TJAMetadata()
self.ex_data = TJAEXData() self.ex_data = TJAEXData()
logger.debug(f"Parsing TJA file: {self.file_path}")
self.get_metadata() self.get_metadata()
self.distance = distance self.distance = distance

View File

@@ -2,6 +2,7 @@ import ctypes
import hashlib import hashlib
import math import math
import sys import sys
import logging
import time import time
import json import json
from libs.global_data import global_data from libs.global_data import global_data
@@ -19,6 +20,7 @@ from raylib import (
from libs.texture import TextureWrapper from libs.texture import TextureWrapper
logger = logging.getLogger(__name__)
def force_dedicated_gpu(): def force_dedicated_gpu():
"""Force Windows to use dedicated GPU for this application""" """Force Windows to use dedicated GPU for this application"""
@@ -29,13 +31,13 @@ def force_dedicated_gpu():
if nvapi: if nvapi:
ctypes.windll.kernel32.SetEnvironmentVariableW("SHIM_MCCOMPAT", "0x800000001") ctypes.windll.kernel32.SetEnvironmentVariableW("SHIM_MCCOMPAT", "0x800000001")
except Exception as e: except Exception as e:
print(e) logger.error(e)
try: try:
# AMD PowerXpress # AMD PowerXpress
ctypes.windll.kernel32.SetEnvironmentVariableW("AMD_VULKAN_ICD", "DISABLE") ctypes.windll.kernel32.SetEnvironmentVariableW("AMD_VULKAN_ICD", "DISABLE")
except Exception as e: except Exception as e:
print(e) logger.error(e)
def rounded(num: float) -> int: def rounded(num: float) -> int:
"""Round a number to the nearest integer""" """Round a number to the nearest integer"""

View File

@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
import logging
import pyray as ray import pyray as ray
from moviepy import VideoFileClip from moviepy import VideoFileClip
@@ -6,6 +7,7 @@ from moviepy import VideoFileClip
from libs.audio import audio from libs.audio import audio
from libs.utils import get_current_ms from libs.utils import get_current_ms
logger = logging.getLogger(__name__)
class VideoPlayer: class VideoPlayer:
def __init__(self, path: Path): def __init__(self, path: Path):
@@ -61,7 +63,7 @@ class VideoPlayer:
return texture return texture
except Exception as e: except Exception as e:
print(f"Error loading frame at index {index}: {e}") logger.error(f"Error loading frame at index {index}: {e}")
return None return None
def _manage_buffer(self): def _manage_buffer(self):

View File

@@ -1,16 +1,26 @@
[project] [project]
name = "pytaiko" name = "pytaiko"
version = "0.1.0" version = "0.9.0"
description = "Taiko no Tatsujin simulator written in python and raylib" description = "Taiko no Tatsujin simulator written in python and raylib"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.14"
dependencies = [ dependencies = [
"moviepy>=2.1.2", "moviepy>=2.1.2",
"pyinstrument>=5.1.1", "raylib-sdl>=5.5.0.3",
"raylib-sdl>=5.5.0.2",
"tomlkit>=0.13.3", "tomlkit>=0.13.3",
] ]
[tool.vulture] [tool.vulture]
exclude = ["*.git", "*./.github", "*./.venv", "*./cache"] exclude = ["*.git", ".github/", ".venv/", "cache/"]
paths = ["."] paths = ["."]
[tool.uv.sources]
raylib-sdl = { path = "raylib_sdl-5.5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" }
[dependency-groups]
dev = [
"nuitka>=2.8.4",
"pyinstrument>=5.1.1",
"ruff>=0.14.2",
"vulture>=2.14",
]

View File

@@ -1,38 +1,17 @@
import pyray as ray import pyray as ray
from libs.utils import get_current_ms from libs.screen import Screen
from libs.texture import tex
from scenes.game import JudgeCounter
class DevScreen: class DevScreen(Screen):
def __init__(self): def on_screen_start(self, screen_name: str):
self.width = 1280 super().on_screen_start(screen_name)
self.height = 720
self.screen_init = False
self.length = 100
def on_screen_start(self):
if not self.screen_init:
self.screen_init = True
tex.load_screen_textures('game')
self.obj = JudgeCounter()
def on_screen_end(self, next_screen: str): def on_screen_end(self, next_screen: str):
self.screen_init = False return super().on_screen_end(next_screen)
return next_screen
def update(self): def update(self):
self.on_screen_start() super().update()
self.obj.update(0, 0, 0, 0)
if ray.is_key_pressed(ray.KeyboardKey.KEY_ENTER):
return self.on_screen_end('GAME')
if ray.is_key_pressed(ray.KeyboardKey.KEY_SPACE):
self.obj = JudgeCounter()
def draw(self): def draw(self):
ray.draw_rectangle(0, 0, 1280, 720, ray.GREEN) ray.draw_rectangle(0, 0, 1280, 720, ray.GREEN)
self.obj.draw()
def draw_3d(self):
pass

View File

@@ -1,9 +1,12 @@
import logging
from typing import Optional
import pyray as ray import pyray as ray
from libs.audio import audio from libs.audio import audio
from libs.chara_2d import Chara2D from libs.chara_2d import Chara2D
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate, Indicator, EntryOverlay, Timer from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate, Indicator, EntryOverlay, Timer
from libs.texture import tex from libs.texture import tex
from libs.screen import Screen
from libs.utils import ( from libs.utils import (
OutlinedText, OutlinedText,
get_current_ms, get_current_ms,
@@ -14,53 +17,45 @@ from libs.utils import (
is_r_kat_pressed, is_r_kat_pressed,
) )
logger = logging.getLogger(__name__)
class State: class State:
"""State enum for the entry screen""" """State enum for the entry screen"""
SELECT_SIDE = 0 SELECT_SIDE = 0
SELECT_MODE = 1 SELECT_MODE = 1
class EntryScreen: class EntryScreen(Screen):
def __init__(self):
self.screen_init = False
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: super().on_screen_start()
tex.load_screen_textures('entry') self.side = 1
audio.load_screen_sounds('entry') self.is_2p = False
self.side = 1 self.box_manager = BoxManager()
self.is_2p = False self.state = State.SELECT_SIDE
self.box_manager = BoxManager()
self.state = State.SELECT_SIDE
# Initial nameplate for side selection # Initial nameplate for side selection
plate_info = global_data.config['nameplate_1p'] plate_info = global_data.config['nameplate_1p']
self.nameplate = Nameplate(plate_info['name'], plate_info['title'], -1, -1, False) self.nameplate = Nameplate(plate_info['name'], plate_info['title'], -1, -1, False)
self.coin_overlay = CoinOverlay() self.coin_overlay = CoinOverlay()
self.allnet_indicator = AllNetIcon() self.allnet_indicator = AllNetIcon()
self.entry_overlay = EntryOverlay() self.entry_overlay = EntryOverlay()
self.timer = Timer(60, get_current_ms(), self.box_manager.select_box) self.timer = Timer(60, get_current_ms(), self.box_manager.select_box)
self.screen_init = True self.screen_init = True
self.side_select_fade = tex.get_animation(0) self.side_select_fade = tex.get_animation(0)
self.bg_flicker = tex.get_animation(1) self.bg_flicker = tex.get_animation(1)
self.side_select_fade.start() self.side_select_fade.start()
self.chara = Chara2D(0, 100) self.chara = Chara2D(0, 100)
self.announce_played = False self.announce_played = False
self.players = [None, None] self.players: list[Optional[EntryPlayer]] = [None, None]
audio.play_sound('bgm', 'music') audio.play_sound('bgm', 'music')
def on_screen_end(self, next_screen: str): def on_screen_end(self, next_screen: str):
self.screen_init = False
audio.stop_sound('bgm') audio.stop_sound('bgm')
self.nameplate.unload() self.nameplate.unload()
for player in self.players: for player in self.players:
if player: if player:
player.unload() player.unload()
tex.unload_textures() return super().on_screen_end(next_screen)
audio.unload_all_sounds()
audio.unload_all_music()
return next_screen
def handle_input(self): def handle_input(self):
if self.state == State.SELECT_SIDE: if self.state == State.SELECT_SIDE:
@@ -118,7 +113,7 @@ class EntryScreen:
self.side = 1 self.side = 1
def update(self): def update(self):
self.on_screen_start() super().update()
current_time = get_current_ms() current_time = get_current_ms()
self.side_select_fade.update(current_time) self.side_select_fade.update(current_time)
self.bg_flicker.update(current_time) self.bg_flicker.update(current_time)
@@ -130,6 +125,7 @@ class EntryScreen:
if player: if player:
player.update(current_time) player.update(current_time)
if self.box_manager.is_finished(): if self.box_manager.is_finished():
logger.info(f"Box selection finished, transitioning to {self.box_manager.selected_box()}")
return self.on_screen_end(self.box_manager.selected_box()) return self.on_screen_end(self.box_manager.selected_box())
for player in self.players: for player in self.players:
if player and player.cloud_fade.is_finished and not audio.is_sound_playing(f'entry_start_{global_data.player_num}p') and not self.announce_played: if player and player.cloud_fade.is_finished and not audio.is_sound_playing(f'entry_start_{global_data.player_num}p') and not self.announce_played:

View File

@@ -1,5 +1,6 @@
import bisect import bisect
import math import math
import logging
import sqlite3 import sqlite3
from collections import deque from collections import deque
from pathlib import Path from pathlib import Path
@@ -13,6 +14,7 @@ from libs.background import Background
from libs.chara_2d import Chara2D from libs.chara_2d import Chara2D
from libs.global_data import Modifiers from libs.global_data import Modifiers
from libs.global_objects import AllNetIcon, Nameplate from libs.global_objects import AllNetIcon, Nameplate
from libs.screen import Screen
from libs.texture import tex from libs.texture import tex
from libs.tja import ( from libs.tja import (
Balloon, Balloon,
@@ -33,22 +35,70 @@ from libs.utils import (
is_l_kat_pressed, is_l_kat_pressed,
is_r_don_pressed, is_r_don_pressed,
is_r_kat_pressed, is_r_kat_pressed,
rounded
) )
from libs.video import VideoPlayer from libs.video import VideoPlayer
SCREEN_WIDTH = 1280 SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720 SCREEN_HEIGHT = 720
class GameScreen: logger = logging.getLogger(__name__)
class GameScreen(Screen):
JUDGE_X = 414 JUDGE_X = 414
def __init__(self): def on_screen_start(self):
super().on_screen_start()
self.mask_shader = ray.load_shader("shader/outline.vs", "shader/mask.fs")
self.current_ms = 0 self.current_ms = 0
self.screen_init = False
self.end_ms = 0 self.end_ms = 0
self.start_delay = 1000 self.start_delay = 1000
self.song_started = False self.song_started = False
self.mask_shader = ray.load_shader("shader/outline.vs", "shader/mask.fs") self.screen_init = True
self.movie = None
self.song_music = None
tex.load_screen_textures('game')
logger.info("Game screen textures loaded")
if global_data.config["general"]["nijiiro_notes"]:
# drop original
if "notes" in tex.textures:
del tex.textures["notes"]
# load nijiiro, rename "notes"
# to leave hardcoded 'notes' in calls below
tex.load_zip("game", "notes_nijiiro")
tex.textures["notes"] = tex.textures.pop("notes_nijiiro")
logger.info("Loaded nijiiro notes textures")
audio.load_screen_sounds('game')
logger.info("Game screen sounds loaded")
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture)
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture)
session_data = global_data.session_data[global_data.player_num-1]
self.init_tja(global_data.selected_song)
logger.info(f"TJA initialized for song: {global_data.selected_song}")
self.load_hitsounds()
self.song_info = SongInfo(session_data.song_title, session_data.genre_index)
self.result_transition = ResultTransition(global_data.player_num)
subtitle = self.tja.metadata.subtitle.get(global_data.config['general']['language'].lower(), '')
self.bpm = self.tja.metadata.bpm
scene_preset = self.tja.metadata.scene_preset
if self.movie is None:
self.background = Background(global_data.player_num, self.bpm, scene_preset=scene_preset)
logger.info("Background initialized")
else:
self.background = None
logger.info("Movie initialized")
self.transition = Transition(session_data.song_title, subtitle, is_second=True)
self.allnet_indicator = AllNetIcon()
self.transition.start()
def on_screen_end(self, next_screen):
self.song_started = False
self.end_ms = 0
if self.movie is not None:
self.movie.stop()
logger.info("Movie stopped")
if self.background is not None:
self.background.unload()
logger.info("Background unloaded")
return super().on_screen_end(next_screen)
def load_hitsounds(self): def load_hitsounds(self):
"""Load the hit sounds""" """Load the hit sounds"""
@@ -56,16 +106,19 @@ class GameScreen:
if global_data.hit_sound == -1: if global_data.hit_sound == -1:
audio.load_sound(Path('none.wav'), 'hitsound_don_1p') audio.load_sound(Path('none.wav'), 'hitsound_don_1p')
audio.load_sound(Path('none.wav'), 'hitsound_kat_1p') audio.load_sound(Path('none.wav'), 'hitsound_kat_1p')
logger.info("Loaded default (none) hit sounds for 1P")
if global_data.hit_sound == 0: if global_data.hit_sound == 0:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.wav", 'hitsound_don_1p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.wav", 'hitsound_don_1p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.wav", 'hitsound_kat_1p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.wav", 'hitsound_kat_1p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don.wav", 'hitsound_don_2p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don.wav", 'hitsound_don_2p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka.wav", 'hitsound_kat_2p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka.wav", 'hitsound_kat_2p')
logger.info("Loaded wav hit sounds for 1P and 2P")
else: else:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.ogg", 'hitsound_don_1p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.ogg", 'hitsound_don_1p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.ogg", 'hitsound_kat_1p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.ogg", 'hitsound_kat_1p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don.ogg", 'hitsound_don_2p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don.ogg", 'hitsound_don_2p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka.ogg", 'hitsound_kat_2p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka.ogg", 'hitsound_kat_2p')
logger.info("Loaded ogg hit sounds for 1P and 2P")
def init_tja(self, song: Path): def init_tja(self, song: Path):
"""Initialize the TJA file""" """Initialize the TJA file"""
@@ -82,52 +135,6 @@ class GameScreen:
self.player_1 = Player(self.tja, global_data.player_num, global_data.session_data[global_data.player_num-1].selected_difficulty, False, global_data.modifiers[0]) self.player_1 = Player(self.tja, global_data.player_num, global_data.session_data[global_data.player_num-1].selected_difficulty, False, global_data.modifiers[0])
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
def on_screen_start(self):
if not self.screen_init:
self.screen_init = True
self.movie = None
self.song_music = None
tex.load_screen_textures('game')
if global_data.config["general"]["nijiiro_notes"]:
# drop original
if "notes" in tex.textures:
del tex.textures["notes"]
# load nijiiro, rename "notes"
# to leave hardcoded 'notes' in calls below
tex.load_zip("game", "notes_nijiiro")
tex.textures["notes"] = tex.textures.pop("notes_nijiiro")
audio.load_screen_sounds('game')
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture0"), tex.textures['balloon']['rainbow_mask'].texture)
ray.set_shader_value_texture(self.mask_shader, ray.get_shader_location(self.mask_shader, "texture1"), tex.textures['balloon']['rainbow'].texture)
session_data = global_data.session_data[global_data.player_num-1]
self.init_tja(global_data.selected_song)
self.load_hitsounds()
self.song_info = SongInfo(session_data.song_title, session_data.genre_index)
self.result_transition = ResultTransition(global_data.player_num)
subtitle = self.tja.metadata.subtitle.get(global_data.config['general']['language'].lower(), '')
self.bpm = self.tja.metadata.bpm
scene_preset = self.tja.metadata.scene_preset
if self.movie is None:
self.background = Background(global_data.player_num, self.bpm, scene_preset=scene_preset)
else:
self.background = None
self.transition = Transition(session_data.song_title, subtitle, is_second=True)
self.allnet_indicator = AllNetIcon()
self.transition.start()
def on_screen_end(self, next_screen):
self.screen_init = False
tex.unload_textures()
audio.unload_all_sounds()
audio.unload_all_music()
self.song_started = False
self.end_ms = 0
if self.movie is not None:
self.movie.stop()
if self.background is not None:
self.background.unload()
return next_screen
def write_score(self): def write_score(self):
"""Write the score to the database""" """Write the score to the database"""
if self.tja is None: if self.tja is None:
@@ -193,7 +200,7 @@ class GameScreen:
if (self.current_ms >= self.tja.metadata.offset*1000 + self.start_delay - global_data.config["general"]["audio_offset"]) and not self.song_started: if (self.current_ms >= self.tja.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')
print(f"Song started at {self.current_ms}") logger.info(f"Song started at {self.current_ms}")
if self.movie is not None: if self.movie is not None:
self.movie.start(current_time) self.movie.start(current_time)
self.song_started = True self.song_started = True
@@ -229,7 +236,7 @@ class GameScreen:
self.background.update(current_time, self.bpm, self.player_1.gauge, None) self.background.update(current_time, self.bpm, self.player_1.gauge, None)
def update(self): def update(self):
self.on_screen_start() super().update()
current_time = get_current_ms() current_time = get_current_ms()
self.transition.update(current_time) self.transition.update(current_time)
self.current_ms = current_time - self.start_ms self.current_ms = current_time - self.start_ms
@@ -243,6 +250,7 @@ class GameScreen:
self.song_info.update(current_time) self.song_info.update(current_time)
self.result_transition.update(current_time) self.result_transition.update(current_time)
if self.result_transition.is_finished and not audio.is_sound_playing('result_transition'): if self.result_transition.is_finished and not audio.is_sound_playing('result_transition'):
logger.info("Result transition finished, moving to RESULT screen")
return self.on_screen_end('RESULT') return self.on_screen_end('RESULT')
elif self.current_ms >= self.player_1.end_time: elif self.current_ms >= self.player_1.end_time:
session_data = global_data.session_data[global_data.player_num-1] session_data = global_data.session_data[global_data.player_num-1]
@@ -252,11 +260,13 @@ class GameScreen:
if current_time >= self.end_ms + 1000: if current_time >= self.end_ms + 1000:
if self.player_1.ending_anim is None: if self.player_1.ending_anim is None:
self.write_score() self.write_score()
logger.info("Score written and ending animations spawned")
self.spawn_ending_anims() self.spawn_ending_anims()
if current_time >= self.end_ms + 8533.34: if current_time >= self.end_ms + 8533.34:
if not self.result_transition.is_started: if not self.result_transition.is_started:
self.result_transition.start() self.result_transition.start()
audio.play_sound('result_transition', 'voice') audio.play_sound('result_transition', 'voice')
logger.info("Result transition started and voice played")
else: else:
self.end_ms = current_time self.end_ms = current_time
@@ -692,7 +702,7 @@ class Player:
big = curr_note.type == 3 or curr_note.type == 4 big = curr_note.type == 3 or curr_note.type == 4
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):
self.draw_judge_list.append(Judgement('GOOD', big, self.is_2p, ms_display=ms_from_start - curr_note.hit_ms)) self.draw_judge_list.append(Judgement('GOOD', big, self.is_2p))
self.lane_hit_effect = LaneHitEffect('GOOD', self.is_2p) self.lane_hit_effect = LaneHitEffect('GOOD', self.is_2p)
self.good_count += 1 self.good_count += 1
self.score += self.base_score self.score += self.base_score
@@ -708,7 +718,7 @@ class Player:
background.add_chibi(False, 1) background.add_chibi(False, 1)
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(Judgement('OK', big, self.is_2p, ms_display=ms_from_start - curr_note.hit_ms)) self.draw_judge_list.append(Judgement('OK', big, 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)
self.base_score_list.append(ScoreCounterAnimation(self.player_number, 10 * math.floor(self.base_score / 2 / 10), self.is_2p)) self.base_score_list.append(ScoreCounterAnimation(self.player_number, 10 * math.floor(self.base_score / 2 / 10), self.is_2p))
@@ -723,7 +733,7 @@ class Player:
background.add_chibi(False, 1) background.add_chibi(False, 1)
elif (curr_note.hit_ms - bad_window_ms) <= ms_from_start <= (curr_note.hit_ms + bad_window_ms): elif (curr_note.hit_ms - bad_window_ms) <= ms_from_start <= (curr_note.hit_ms + bad_window_ms):
self.draw_judge_list.append(Judgement('BAD', big, self.is_2p, ms_display=ms_from_start - curr_note.hit_ms)) self.draw_judge_list.append(Judgement('BAD', big, self.is_2p))
self.bad_count += 1 self.bad_count += 1
self.combo = 0 self.combo = 0
# Remove from both the specific note list and the main play_notes list # Remove from both the specific note list and the main play_notes list
@@ -1109,14 +1119,11 @@ class Player:
class Judgement: class Judgement:
"""Shows the judgement of the player's hit""" """Shows the judgement of the player's hit"""
def __init__(self, type: str, big: bool, is_2p: bool, ms_display: Optional[float]=None): def __init__(self, type: str, big: bool, is_2p: bool):
self.is_2p = is_2p self.is_2p = is_2p
self.type = type self.type = type
self.big = big self.big = big
self.is_finished = False self.is_finished = False
self.curr_hit_ms = None
if ms_display is not None:
self.curr_hit_ms = str(round(ms_display, 2))
self.fade_animation_1 = Animation.create_fade(132, initial_opacity=0.5, delay=100) self.fade_animation_1 = Animation.create_fade(132, initial_opacity=0.5, delay=100)
self.fade_animation_1.start() self.fade_animation_1.start()
@@ -2120,14 +2127,14 @@ class JudgeCounter:
total_notes = self.good + self.ok + self.bad total_notes = self.good + self.ok + self.bad
if total_notes == 0: if total_notes == 0:
total_notes = 1 total_notes = 1
self.draw_counter(self.good / total_notes * 100, 260, 440, 23, ray.Color(253, 161, 0, 255)) self.draw_counter(self.good / total_notes * 100, 260, 440, 23, self.orange)
self.draw_counter(self.ok / total_notes * 100, 260, 477, 23, ray.Color(253, 161, 0, 255)) self.draw_counter(self.ok / total_notes * 100, 260, 477, 23, self.orange)
self.draw_counter(self.bad / total_notes * 100, 260, 515, 23, ray.Color(253, 161, 0, 255)) self.draw_counter(self.bad / total_notes * 100, 260, 515, 23, self.orange)
self.draw_counter((self.good + self.ok) / total_notes * 100, 270, 388, 23, ray.Color(253, 161, 0, 255)) self.draw_counter((self.good + self.ok) / total_notes * 100, 270, 388, 23, self.orange)
self.draw_counter(self.good, 180, 440, 23, ray.WHITE) self.draw_counter(self.good, 180, 440, 23, self.white)
self.draw_counter(self.ok, 180, 477, 23, ray.WHITE) self.draw_counter(self.ok, 180, 477, 23, self.white)
self.draw_counter(self.bad, 180, 515, 23, ray.WHITE) self.draw_counter(self.bad, 180, 515, 23, self.white)
self.draw_counter(self.drumrolls, 180, 577, 23, ray.WHITE) self.draw_counter(self.drumrolls, 180, 577, 23, self.white)
class Gauge: class Gauge:
"""The player's gauge""" """The player's gauge"""

View File

@@ -1,20 +1,24 @@
import logging
import threading import threading
import pyray as ray import pyray as ray
from libs.animation import Animation from libs.animation import Animation
from libs.global_objects import AllNetIcon from libs.global_objects import AllNetIcon
from libs.screen import Screen
from libs.song_hash import build_song_hashes from libs.song_hash import build_song_hashes
from libs.texture import tex from libs.texture import tex
from libs.utils import get_current_ms, global_data from libs.utils import get_current_ms, global_data
from libs.file_navigator import navigator from libs.file_navigator import navigator
class LoadScreen: logger = logging.getLogger(__name__)
def __init__(self):
class LoadScreen(Screen):
def __init__(self, name: str):
super().__init__(name)
self.width = 1280 self.width = 1280
self.height = 720 self.height = 720
self.screen_init = False
self.songs_loaded = False self.songs_loaded = False
self.navigator_started = False self.navigator_started = False
self.loading_complete = False self.loading_complete = False
@@ -37,42 +41,44 @@ class LoadScreen:
"""Background thread function to load song hashes""" """Background thread function to load song hashes"""
global_data.song_hashes = build_song_hashes() global_data.song_hashes = build_song_hashes()
self.songs_loaded = True self.songs_loaded = True
logger.info("Song hashes loaded")
def _load_navigator(self): def _load_navigator(self):
"""Background thread function to load navigator""" """Background thread function to load navigator"""
self.navigator.initialize(global_data.config["paths"]["tja_path"]) self.navigator.initialize(global_data.config["paths"]["tja_path"])
self.loading_complete = True self.loading_complete = True
logger.info("Navigator initialized")
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: super().on_screen_start()
tex.load_screen_textures('loading') self.loading_thread = threading.Thread(target=self._load_song_hashes)
self.loading_thread = threading.Thread(target=self._load_song_hashes) self.loading_thread.daemon = True
self.loading_thread.daemon = True self.loading_thread.start()
self.loading_thread.start() logger.info("Started song hashes loading thread")
self.screen_init = True
def on_screen_end(self, next_screen: str): def on_screen_end(self, next_screen: str):
self.screen_init = False
tex.unload_textures()
if self.loading_thread and self.loading_thread.is_alive(): if self.loading_thread and self.loading_thread.is_alive():
self.loading_thread.join(timeout=1.0) self.loading_thread.join(timeout=1.0)
logger.info("Joined song hashes loading thread")
if self.navigator_thread and self.navigator_thread.is_alive(): if self.navigator_thread and self.navigator_thread.is_alive():
self.navigator_thread.join(timeout=1.0) self.navigator_thread.join(timeout=1.0)
logger.info("Joined navigator loading thread")
return next_screen return super().on_screen_end(next_screen)
def update(self): def update(self):
self.on_screen_start() super().update()
if self.songs_loaded and not self.navigator_started: if self.songs_loaded and not self.navigator_started:
self.navigator_thread = threading.Thread(target=self._load_navigator) self.navigator_thread = threading.Thread(target=self._load_navigator)
self.navigator_thread.daemon = True self.navigator_thread.daemon = True
self.navigator_thread.start() self.navigator_thread.start()
self.navigator_started = True self.navigator_started = True
logger.info("Started navigator loading thread")
if self.loading_complete and self.fade_in is None: if self.loading_complete and self.fade_in is None:
self.fade_in = Animation.create_fade(1000, initial_opacity=0.0, final_opacity=1.0, ease_in='cubic') self.fade_in = Animation.create_fade(1000, initial_opacity=0.0, final_opacity=1.0, ease_in='cubic')
self.fade_in.start() self.fade_in.start()
logger.info("Fade-in animation started")
if self.fade_in is not None: if self.fade_in is not None:
self.fade_in.update(get_current_ms()) self.fade_in.update(get_current_ms())
@@ -107,5 +113,3 @@ class LoadScreen:
if self.fade_in is not None: if self.fade_in is not None:
ray.draw_rectangle(0, 0, self.width, self.height, ray.fade(ray.WHITE, self.fade_in.attribute)) ray.draw_rectangle(0, 0, self.width, self.height, ray.fade(ray.WHITE, self.fade_in.attribute))
self.allnet_indicator.draw() self.allnet_indicator.draw()
def draw_3d(self):
pass

View File

@@ -1,9 +1,11 @@
import logging
import pyray as ray import pyray as ray
from libs.global_data import reset_session from libs.global_data import reset_session
from libs.audio import audio from libs.audio import audio
from libs.chara_2d import Chara2D from libs.chara_2d import Chara2D
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate
from libs.screen import Screen
from libs.texture import tex from libs.texture import tex
from libs.utils import ( from libs.utils import (
OutlinedText, OutlinedText,
@@ -13,46 +15,32 @@ from libs.utils import (
is_r_don_pressed is_r_don_pressed
) )
logger = logging.getLogger(__name__)
class State: class State:
""" """Enum representing the state of the result screen."""
Enum representing the state of the result screen.
"""
FAIL = 0 FAIL = 0
CLEAR = 1 CLEAR = 1
RAINBOW = 2 RAINBOW = 2
class ResultScreen: class ResultScreen(Screen):
def __init__(self):
self.width = 1280
self.height = 720
self.screen_init = False
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: super().on_screen_start()
tex.load_screen_textures('result') self.song_info = OutlinedText(global_data.session_data[0].song_title, 40, ray.WHITE, ray.BLACK, outline_thickness=5)
audio.load_screen_sounds('result') audio.play_sound('bgm', 'music')
self.screen_init = True self.fade_in = FadeIn(str(global_data.player_num))
self.song_info = OutlinedText(global_data.session_data[0].song_title, 40, ray.WHITE, ray.BLACK, outline_thickness=5) self.fade_out = tex.get_animation(0)
audio.play_sound('bgm', 'music') self.coin_overlay = CoinOverlay()
self.fade_in = FadeIn(str(global_data.player_num)) self.allnet_indicator = AllNetIcon()
self.fade_out = tex.get_animation(0) self.start_ms = get_current_ms()
self.coin_overlay = CoinOverlay() self.is_skipped = False
self.allnet_indicator = AllNetIcon() self.background = Background(str(global_data.player_num), 1280)
self.start_ms = get_current_ms() self.player_1 = ResultPlayer(str(global_data.player_num), False, False)
self.is_skipped = False
self.background = Background(str(global_data.player_num), self.width)
self.player_1 = ResultPlayer(str(global_data.player_num), False, False)
def on_screen_end(self, next_screen: str): def on_screen_end(self, next_screen: str):
self.screen_init = False
global_data.songs_played += 1 global_data.songs_played += 1
tex.unload_textures()
audio.stop_sound('bgm')
audio.unload_all_sounds()
audio.unload_all_music()
reset_session() reset_session()
return next_screen return super().on_screen_end(next_screen)
def handle_input(self): def handle_input(self):
if is_r_don_pressed() or is_l_don_pressed(): if is_r_don_pressed() or is_l_don_pressed():
@@ -63,7 +51,7 @@ class ResultScreen:
audio.play_sound('don', 'sound') audio.play_sound('don', 'sound')
def update(self): def update(self):
self.on_screen_start() super().update()
current_time = get_current_ms() current_time = get_current_ms()
self.fade_in.update(current_time) self.fade_in.update(current_time)
self.player_1.update(current_time, self.fade_in.is_finished, self.is_skipped) self.player_1.update(current_time, self.fade_in.is_finished, self.is_skipped)
@@ -78,7 +66,7 @@ class ResultScreen:
def draw_overlay(self): def draw_overlay(self):
self.fade_in.draw() self.fade_in.draw()
ray.draw_rectangle(0, 0, self.width, self.height, ray.fade(ray.BLACK, self.fade_out.attribute)) ray.draw_rectangle(0, 0, 1280, 720, ray.fade(ray.BLACK, self.fade_out.attribute))
self.coin_overlay.draw() self.coin_overlay.draw()
self.allnet_indicator.draw() self.allnet_indicator.draw()

View File

@@ -1,6 +1,8 @@
import logging
import pyray as ray import pyray as ray
from libs.audio import audio from libs.audio import audio
from libs.screen import Screen
from libs.utils import ( from libs.utils import (
global_data, global_data,
is_l_don_pressed, is_l_don_pressed,
@@ -10,10 +12,12 @@ from libs.utils import (
save_config, save_config,
) )
logger = logging.getLogger(__name__)
class SettingsScreen:
def __init__(self): class SettingsScreen(Screen):
self.screen_init = False def on_screen_start(self):
super().on_screen_start()
self.config = global_data.config self.config = global_data.config
self.headers = list(self.config.keys()) self.headers = list(self.config.keys())
self.headers.append('Exit') self.headers.append('Exit')
@@ -23,13 +27,7 @@ class SettingsScreen:
self.editing_key = False self.editing_key = False
self.editing_gamepad = False self.editing_gamepad = False
def on_screen_start(self): def on_screen_end(self, next_screen: str):
if not self.screen_init:
audio.list_host_apis()
self.screen_init = True
def on_screen_end(self):
self.screen_init = False
save_config(self.config) save_config(self.config)
global_data.config = self.config global_data.config = self.config
audio.close_audio_device() audio.close_audio_device()
@@ -41,7 +39,8 @@ class SettingsScreen:
audio.buffer_size = global_data.config["audio"]["buffer_size"] audio.buffer_size = global_data.config["audio"]["buffer_size"]
audio.volume_presets = global_data.config["volume"] audio.volume_presets = global_data.config["volume"]
audio.init_audio_device() audio.init_audio_device()
return "ENTRY" logger.info("Settings saved and audio device re-initialized")
return next_screen
def get_current_settings(self): def get_current_settings(self):
"""Get the current section's settings as a list""" """Get the current section's settings as a list"""
@@ -53,6 +52,7 @@ class SettingsScreen:
def handle_boolean_toggle(self, section, key): def handle_boolean_toggle(self, section, key):
"""Toggle boolean values""" """Toggle boolean values"""
self.config[section][key] = not self.config[section][key] self.config[section][key] = not self.config[section][key]
logger.info(f"Toggled boolean setting: {section}.{key} -> {self.config[section][key]}")
def handle_numeric_change(self, section, key, increment): def handle_numeric_change(self, section, key, increment):
"""Handle numeric value changes""" """Handle numeric value changes"""
@@ -81,6 +81,7 @@ class SettingsScreen:
new_value = valid_sizes[new_idx] new_value = valid_sizes[new_idx]
self.config[section][key] = new_value self.config[section][key] = new_value
logger.info(f"Changed numeric setting: {section}.{key} -> {new_value}")
def handle_string_cycle(self, section, key): def handle_string_cycle(self, section, key):
"""Cycle through predefined string values""" """Cycle through predefined string values"""
@@ -98,10 +99,12 @@ class SettingsScreen:
self.config[section][key] = values[new_idx] self.config[section][key] = values[new_idx]
except ValueError: except ValueError:
self.config[section][key] = values[0] self.config[section][key] = values[0]
logger.info(f"Cycled string setting: {section}.{key} -> {self.config[section][key]}")
def handle_key_binding(self, section, key): def handle_key_binding(self, section, key):
"""Handle key binding changes""" """Handle key binding changes"""
self.editing_key = True self.editing_key = True
logger.info(f"Started key binding edit for: {section}.{key}")
def update_key_binding(self): def update_key_binding(self):
"""Update key binding based on input""" """Update key binding based on input"""
@@ -116,11 +119,14 @@ class SettingsScreen:
setting_key, _ = settings[self.setting_index] setting_key, _ = settings[self.setting_index]
self.config[current_header][setting_key] = [new_key] self.config[current_header][setting_key] = [new_key]
self.editing_key = False self.editing_key = False
logger.info(f"Key binding updated: {current_header}.{setting_key} -> {new_key}")
elif key_pressed == ray.KeyboardKey.KEY_ESCAPE: elif key_pressed == ray.KeyboardKey.KEY_ESCAPE:
self.editing_key = False self.editing_key = False
logger.info("Key binding edit cancelled")
def handle_gamepad_binding(self, section, key): def handle_gamepad_binding(self, section, key):
self.editing_gamepad = True self.editing_gamepad = True
logger.info(f"Started gamepad binding edit for: {section}.{key}")
def update_gamepad_binding(self): def update_gamepad_binding(self):
"""Update gamepad binding based on input""" """Update gamepad binding based on input"""
@@ -132,11 +138,13 @@ class SettingsScreen:
setting_key, _ = settings[self.setting_index] setting_key, _ = settings[self.setting_index]
self.config[current_header][setting_key] = [button_pressed] self.config[current_header][setting_key] = [button_pressed]
self.editing_gamepad = False self.editing_gamepad = False
logger.info(f"Gamepad binding updated: {current_header}.{setting_key} -> {button_pressed}")
if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE):
self.editing_gamepad = False self.editing_gamepad = False
logger.info("Gamepad binding edit cancelled")
def update(self): def update(self):
self.on_screen_start() super().update()
# Handle key binding editing # Handle key binding editing
if self.editing_key: if self.editing_key:
@@ -151,18 +159,22 @@ class SettingsScreen:
# Exit handling # Exit handling
if current_header == 'Exit' and (is_l_don_pressed() or is_r_don_pressed()): if current_header == 'Exit' and (is_l_don_pressed() or is_r_don_pressed()):
return self.on_screen_end() logger.info("Exiting settings screen")
return self.on_screen_end("ENTRY")
# Navigation between sections # Navigation between sections
if not self.in_setting_edit: if not self.in_setting_edit:
if is_r_kat_pressed(): if is_r_kat_pressed():
self.header_index = (self.header_index + 1) % len(self.headers) self.header_index = (self.header_index + 1) % len(self.headers)
self.setting_index = 0 self.setting_index = 0
logger.info(f"Navigated to next section: {self.headers[self.header_index]}")
elif is_l_kat_pressed(): elif is_l_kat_pressed():
self.header_index = (self.header_index - 1) % len(self.headers) self.header_index = (self.header_index - 1) % len(self.headers)
self.setting_index = 0 self.setting_index = 0
logger.info(f"Navigated to previous section: {self.headers[self.header_index]}")
elif (is_l_don_pressed() or is_r_don_pressed()) and current_header != 'Exit': elif (is_l_don_pressed() or is_r_don_pressed()) and current_header != 'Exit':
self.in_setting_edit = True self.in_setting_edit = True
logger.info(f"Entered section edit: {current_header}")
else: else:
# Navigation within settings # Navigation within settings
settings = self.get_current_settings() settings = self.get_current_settings()
@@ -172,8 +184,10 @@ class SettingsScreen:
if is_r_kat_pressed(): if is_r_kat_pressed():
self.setting_index = (self.setting_index + 1) % len(settings) self.setting_index = (self.setting_index + 1) % len(settings)
logger.info(f"Navigated to next setting: {settings[self.setting_index][0]}")
elif is_l_kat_pressed(): elif is_l_kat_pressed():
self.setting_index = (self.setting_index - 1) % len(settings) self.setting_index = (self.setting_index - 1) % len(settings)
logger.info(f"Navigated to previous setting: {settings[self.setting_index][0]}")
elif is_r_don_pressed(): elif is_r_don_pressed():
# Modify setting value # Modify setting value
setting_key, setting_value = settings[self.setting_index] setting_key, setting_value = settings[self.setting_index]
@@ -209,6 +223,7 @@ class SettingsScreen:
elif ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): elif ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE):
self.in_setting_edit = False self.in_setting_edit = False
logger.info("Exited section edit")
def draw(self): def draw(self):
# Draw title # Draw title
@@ -237,7 +252,8 @@ class SettingsScreen:
display_value = ', '.join(map(str, value)) display_value = ', '.join(map(str, value))
else: else:
display_value = str(value) display_value = str(value)
if key == 'device_type':
display_value = f'{display_value} ({audio.get_host_api_name(value)})'
ray.draw_text(f'{key}: {display_value}', 250, i*25 + 70, 20, color) ray.draw_text(f'{key}: {display_value}', 250, i*25 + 70, 20, color)
# Draw instructions # Draw instructions
@@ -255,5 +271,3 @@ class SettingsScreen:
else: else:
# Draw exit instruction # Draw exit instruction
ray.draw_text("Press Don to exit settings", 250, 100, 20, ray.GREEN) ray.draw_text("Press Don to exit settings", 250, 100, 20, ray.GREEN)
def draw_3d(self):
pass

View File

@@ -3,6 +3,7 @@ from dataclasses import fields
from pathlib import Path from pathlib import Path
import pyray as ray import pyray as ray
import logging
from libs.file_navigator import navigator from libs.file_navigator import navigator
from libs.audio import audio from libs.audio import audio
@@ -10,6 +11,7 @@ from libs.chara_2d import Chara2D
from libs.file_navigator import Directory, SongBox, SongFile from libs.file_navigator import Directory, SongBox, SongFile
from libs.global_data import Modifiers from libs.global_data import Modifiers
from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate, Indicator, Timer from libs.global_objects import AllNetIcon, CoinOverlay, Nameplate, Indicator, Timer
from libs.screen import Screen
from libs.texture import tex from libs.texture import tex
from libs.transition import Transition from libs.transition import Transition
from libs.utils import ( from libs.utils import (
@@ -22,63 +24,59 @@ from libs.utils import (
is_r_kat_pressed, is_r_kat_pressed,
) )
logger = logging.getLogger(__name__)
class State: class State:
BROWSING = 0 BROWSING = 0
SONG_SELECTED = 1 SONG_SELECTED = 1
DIFF_SORTING = 2 DIFF_SORTING = 2
class SongSelectScreen: class SongSelectScreen(Screen):
BOX_CENTER = 444 BOX_CENTER = 444
def __init__(self, screen_width: int = 1280):
self.screen_init = False
self.screen_width = screen_width
self.indicator = Indicator(Indicator.State.SELECT)
self.navigator = navigator
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: super().on_screen_start()
tex.load_screen_textures('song_select') audio.set_sound_volume('ura_switch', 0.25)
audio.load_screen_sounds('song_select') audio.set_sound_volume('add_favorite', 3.0)
audio.set_sound_volume('ura_switch', 0.25) audio.play_sound('bgm', 'music')
audio.set_sound_volume('add_favorite', 3.0) audio.play_sound('voice_enter', 'voice')
audio.play_sound('bgm', 'music') self.navigator = navigator
audio.play_sound('voice_enter', 'voice') self.background_move = tex.get_animation(0)
self.background_move = tex.get_animation(0) self.move_away = tex.get_animation(1)
self.move_away = tex.get_animation(1) self.diff_fade_out = tex.get_animation(2)
self.diff_fade_out = tex.get_animation(2) self.text_fade_out = tex.get_animation(3)
self.text_fade_out = tex.get_animation(3) self.text_fade_in = tex.get_animation(4)
self.text_fade_in = tex.get_animation(4) self.background_fade_change = tex.get_animation(5)
self.background_fade_change = tex.get_animation(5) self.blue_arrow_fade = tex.get_animation(29)
self.blue_arrow_fade = tex.get_animation(29) self.blue_arrow_move = tex.get_animation(30)
self.blue_arrow_move = tex.get_animation(30) self.blue_arrow_fade.start()
self.blue_arrow_fade.start() self.blue_arrow_move.start()
self.blue_arrow_move.start() self.state = State.BROWSING
self.state = State.BROWSING self.game_transition = None
self.game_transition = None self.demo_song = None
self.demo_song = None self.diff_sort_selector = None
self.diff_sort_selector = None self.coin_overlay = CoinOverlay()
self.coin_overlay = CoinOverlay() self.allnet_indicator = AllNetIcon()
self.allnet_indicator = AllNetIcon() self.indicator = Indicator(Indicator.State.SELECT)
self.texture_index = SongBox.DEFAULT_INDEX self.texture_index = SongBox.DEFAULT_INDEX
self.last_texture_index = SongBox.DEFAULT_INDEX self.last_texture_index = SongBox.DEFAULT_INDEX
self.last_moved = get_current_ms() self.last_moved = get_current_ms()
self.timer_browsing = Timer(100, get_current_ms(), self.navigator.select_current_item) self.timer_browsing = Timer(100, get_current_ms(), self.navigator.select_current_item)
self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection_wrapper) self.timer_selected = Timer(40, get_current_ms(), self._confirm_selection_wrapper)
self.screen_init = True self.screen_init = True
self.ura_switch_animation = UraSwitchAnimation() self.ura_switch_animation = UraSwitchAnimation()
self.player_1 = SongSelectPlayer(str(global_data.player_num), self.text_fade_in) self.player_1 = SongSelectPlayer(str(global_data.player_num), self.text_fade_in)
if self.navigator.items == []: if self.navigator.items == []:
return self.on_screen_end("ENTRY") logger.warning("No navigator items found, returning to ENTRY screen")
return self.on_screen_end("ENTRY")
if str(global_data.selected_song) in self.navigator.all_song_files: if str(global_data.selected_song) in self.navigator.all_song_files:
self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(global_data.selected_song)]) self.navigator.mark_crowns_dirty_for_song(self.navigator.all_song_files[str(global_data.selected_song)])
self.navigator.reset_items() curr_item = self.navigator.get_current_item()
curr_item = self.navigator.get_current_item() curr_item.box.get_scores()
curr_item.box.get_scores() self.navigator.add_recent()
self.navigator.add_recent()
def finalize_song(self): def finalize_song(self):
global_data.selected_song = self.navigator.get_current_item().path global_data.selected_song = self.navigator.get_current_item().path
@@ -88,13 +86,10 @@ class SongSelectScreen:
def on_screen_end(self, next_screen): def on_screen_end(self, next_screen):
self.screen_init = False self.screen_init = False
self.reset_demo_music() self.reset_demo_music()
self.navigator.reset_items()
self.finalize_song() self.finalize_song()
audio.unload_all_sounds()
audio.unload_all_music()
tex.unload_textures()
self.player_1.nameplate.unload() self.player_1.nameplate.unload()
return next_screen self.navigator.get_current_item().box.yellow_box.create_anim()
return super().on_screen_end(next_screen)
def reset_demo_music(self): def reset_demo_music(self):
"""Reset the preview music to the song select bgm.""" """Reset the preview music to the song select bgm."""
@@ -199,7 +194,7 @@ class SongSelectScreen:
"""Wrapper for timer callback""" """Wrapper for timer callback"""
self._confirm_selection() self._confirm_selection()
def _confirm_selection(self): def _confirm_selection(self, player_selected: int = 1):
"""Confirm song selection and create game transition""" """Confirm song selection and create game transition"""
audio.play_sound('don', 'sound') audio.play_sound('don', 'sound')
audio.play_sound(f'voice_start_song_{global_data.player_num}p', 'voice') audio.play_sound(f'voice_start_song_{global_data.player_num}p', 'voice')
@@ -214,7 +209,8 @@ class SongSelectScreen:
self.player_1.update(current_time) self.player_1.update(current_time)
if self.text_fade_out.is_finished: if self.text_fade_out.is_finished:
self.player_1.selected_song = True self.player_1.selected_song = True
return "GAME" next_screen = "GAME"
return next_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:
@@ -230,7 +226,7 @@ class SongSelectScreen:
self.game_transition.start() self.game_transition.start()
def update(self): def update(self):
ret_val = self.on_screen_start() ret_val = super().update()
if ret_val is not None: if ret_val is not None:
return ret_val return ret_val
current_time = get_current_ms() current_time = get_current_ms()
@@ -288,12 +284,14 @@ class SongSelectScreen:
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.tja.metadata.demostart)
audio.stop_sound('bgm') audio.stop_sound('bgm')
logger.info(f"Demo song loaded and playing for {song.tja.metadata.title}")
if song.box.is_open: if song.box.is_open:
current_box = song.box current_box = song.box
if not current_box.is_back and get_current_ms() >= song.box.wait + (83.33*3): if not current_box.is_back and get_current_ms() >= song.box.wait + (83.33*3):
self.texture_index = current_box.texture_index self.texture_index = current_box.texture_index
if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE):
logger.info("Escape key pressed, returning to ENTRY screen")
return self.on_screen_end('ENTRY') return self.on_screen_end('ENTRY')
def draw_background_diffs(self): def draw_background_diffs(self):
@@ -315,7 +313,7 @@ class SongSelectScreen:
for item in self.navigator.items: for item in self.navigator.items:
box = item.box box = item.box
if -156 <= box.position <= self.screen_width + 144: if -156 <= box.position <= 1280 + 144:
if box.position <= 500: if box.position <= 500:
box.draw(box.position - int(self.move_away.attribute), 95, self.player_1.is_ura, fade_override=self.diff_fade_out.attribute) box.draw(box.position - int(self.move_away.attribute), 95, self.player_1.is_ura, fade_override=self.diff_fade_out.attribute)
else: else:
@@ -534,6 +532,7 @@ class SongSelectPlayer:
raise Exception("Directory was chosen instead of song") raise Exception("Directory was chosen instead of song")
diffs = sorted(selected_song.tja.metadata.course_data) diffs = sorted(selected_song.tja.metadata.course_data)
prev_diff = self.selected_difficulty prev_diff = self.selected_difficulty
ret_val = None
if is_l_kat_pressed(self.player_num): if is_l_kat_pressed(self.player_num):
ret_val = self._navigate_difficulty_left(diffs) ret_val = self._navigate_difficulty_left(diffs)
@@ -616,7 +615,7 @@ class SongSelectPlayer:
self.selected_difficulty = 7 - self.selected_difficulty self.selected_difficulty = 7 - self.selected_difficulty
return "ura_toggle" return "ura_toggle"
def draw_selector(self, state: State, is_half: bool): def draw_selector(self, is_half: bool):
fade = 0.5 if (self.neiro_selector is not None or self.modifier_selector is not None) else self.text_fade_in.attribute fade = 0.5 if (self.neiro_selector is not None or self.modifier_selector is not None) else self.text_fade_in.attribute
direction = 1 if self.diff_select_move_right else -1 direction = 1 if self.diff_select_move_right else -1
if self.selected_difficulty <= -1 or self.prev_diff == -1: if self.selected_difficulty <= -1 or self.prev_diff == -1:
@@ -660,7 +659,7 @@ class SongSelectPlayer:
name = f'{self.player_num}p_outline_half' if is_half else f'{self.player_num}p_outline' name = f'{self.player_num}p_outline_half' if is_half else f'{self.player_num}p_outline'
tex.draw_texture('diff_select', name, x=(difficulty * 115)) tex.draw_texture('diff_select', name, x=(difficulty * 115))
def draw_background_diffs(self, state: State): def draw_background_diffs(self, state: int):
if (self.selected_song and state == State.SONG_SELECTED and self.selected_difficulty >= 0): if (self.selected_song and state == State.SONG_SELECTED and self.selected_difficulty >= 0):
if self.player_num == '2': if self.player_num == '2':
tex.draw_texture('global', 'background_diff', frame=self.selected_difficulty, fade=min(0.5, self.selected_diff_fadein.attribute), x=1025, y=-self.selected_diff_bounce.attribute, y2=self.selected_diff_bounce.attribute) tex.draw_texture('global', 'background_diff', frame=self.selected_difficulty, fade=min(0.5, self.selected_diff_fadein.attribute), x=1025, y=-self.selected_diff_bounce.attribute, y2=self.selected_diff_bounce.attribute)
@@ -677,9 +676,9 @@ class SongSelectPlayer:
tex.draw_texture('global', 'bg_diff_text_bg', fade=min(0.5, self.selected_diff_text_fadein.attribute), scale=self.selected_diff_text_resize.attribute, center=True) tex.draw_texture('global', 'bg_diff_text_bg', fade=min(0.5, self.selected_diff_text_fadein.attribute), scale=self.selected_diff_text_resize.attribute, center=True)
tex.draw_texture('global', 'bg_diff_text', frame=min(3, self.selected_difficulty), fade=self.selected_diff_text_fadein.attribute, scale=self.selected_diff_text_resize.attribute, center=True) tex.draw_texture('global', 'bg_diff_text', frame=min(3, self.selected_difficulty), fade=self.selected_diff_text_fadein.attribute, scale=self.selected_diff_text_resize.attribute, center=True)
def draw(self, state: State, is_half: bool = False): def draw(self, state: int, is_half: bool = False):
if (self.selected_song and state == State.SONG_SELECTED): if (self.selected_song and state == State.SONG_SELECTED):
self.draw_selector(state, is_half) self.draw_selector(is_half)
offset = 0 offset = 0
if self.neiro_selector is not None: if self.neiro_selector is not None:

View File

@@ -1,3 +1,4 @@
import logging
import random import random
from pathlib import Path from pathlib import Path
@@ -12,15 +13,18 @@ from libs.utils import (
is_r_don_pressed, is_r_don_pressed,
) )
from libs.video import VideoPlayer from libs.video import VideoPlayer
from libs.screen import Screen
logger = logging.getLogger(__name__)
class State: class State:
OP_VIDEO = 0 OP_VIDEO = 0
WARNING = 1 WARNING = 1
ATTRACT_VIDEO = 2 ATTRACT_VIDEO = 2
class TitleScreen: class TitleScreen(Screen):
def __init__(self): def __init__(self, name: str):
super().__init__(name)
#normalize to accept both stings and lists in toml #normalize to accept both stings and lists in toml
#maybe normalize centrally? but it's used only here #maybe normalize centrally? but it's used only here
vp = global_data.config["paths"]["video_path"] vp = global_data.config["paths"]["video_path"]
@@ -31,33 +35,27 @@ class TitleScreen:
base = Path(base) base = Path(base)
self.op_video_list += list((base/"op_videos").glob("**/*.mp4")) self.op_video_list += list((base/"op_videos").glob("**/*.mp4"))
self.attract_video_list += list((base/"attract_videos").glob("**/*.mp4")) self.attract_video_list += list((base/"attract_videos").glob("**/*.mp4"))
self.screen_init = False
self.coin_overlay = CoinOverlay() self.coin_overlay = CoinOverlay()
self.allnet_indicator = AllNetIcon() self.allnet_indicator = AllNetIcon()
self.entry_overlay = EntryOverlay() self.entry_overlay = EntryOverlay()
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: super().on_screen_start()
self.screen_init = True self.state = State.OP_VIDEO
tex.load_screen_textures('title') self.op_video = None
audio.load_screen_sounds('title') self.attract_video = None
self.state = State.OP_VIDEO self.warning_board = None
self.op_video = None self.fade_out = tex.get_animation(13)
self.attract_video = None self.text_overlay_fade = tex.get_animation(14)
self.warning_board = None
self.fade_out = tex.get_animation(13)
self.text_overlay_fade = tex.get_animation(14)
def on_screen_end(self) -> str: def on_screen_end(self, next_screen) -> str:
if self.op_video is not None: if self.op_video is not None:
self.op_video.stop() self.op_video.stop()
logger.info("OP video stopped")
if self.attract_video is not None: if self.attract_video is not None:
self.attract_video.stop() self.attract_video.stop()
audio.unload_all_sounds() logger.info("Attract video stopped")
audio.unload_all_music() return super().on_screen_end(next_screen)
tex.unload_textures()
self.screen_init = False
return "ENTRY"
def scene_manager(self, current_time): def scene_manager(self, current_time):
"""Manage the scene transitions""" """Manage the scene transitions"""
@@ -65,38 +63,44 @@ class TitleScreen:
if self.op_video is None: if self.op_video is None:
self.op_video = VideoPlayer(random.choice(self.op_video_list)) self.op_video = VideoPlayer(random.choice(self.op_video_list))
self.op_video.start(current_time) self.op_video.start(current_time)
logger.info("Started OP video")
self.op_video.update() self.op_video.update()
if self.op_video.is_finished(): if self.op_video.is_finished():
self.op_video.stop() self.op_video.stop()
self.op_video = None self.op_video = None
self.state = State.WARNING self.state = State.WARNING
logger.info("OP video finished, transitioning to WARNING state")
elif self.state == State.WARNING: elif self.state == State.WARNING:
if self.warning_board is None: if self.warning_board is None:
self.warning_board = WarningScreen(current_time) self.warning_board = WarningScreen(current_time)
logger.info("Warning screen started")
self.warning_board.update(current_time) self.warning_board.update(current_time)
if self.warning_board.is_finished: if self.warning_board.is_finished:
self.state = State.ATTRACT_VIDEO self.state = State.ATTRACT_VIDEO
self.warning_board = None self.warning_board = None
logger.info("Warning finished, transitioning to ATTRACT_VIDEO state")
elif self.state == State.ATTRACT_VIDEO: elif self.state == State.ATTRACT_VIDEO:
if self.attract_video is None: if self.attract_video is None:
self.attract_video = VideoPlayer(random.choice(self.attract_video_list)) self.attract_video = VideoPlayer(random.choice(self.attract_video_list))
self.attract_video.start(current_time) self.attract_video.start(current_time)
logger.info("Started attract video")
self.attract_video.update() self.attract_video.update()
if self.attract_video.is_finished(): if self.attract_video.is_finished():
self.attract_video.stop() self.attract_video.stop()
self.attract_video = None self.attract_video = None
self.state = State.OP_VIDEO self.state = State.OP_VIDEO
logger.info("Attract video finished, transitioning to OP_VIDEO state")
def update(self): def update(self):
self.on_screen_start() super().update()
current_time = get_current_ms() current_time = get_current_ms()
self.text_overlay_fade.update(current_time) self.text_overlay_fade.update(current_time)
self.fade_out.update(current_time) self.fade_out.update(current_time)
if self.fade_out.is_finished: if self.fade_out.is_finished:
self.fade_out.update(current_time) self.fade_out.update(current_time)
return self.on_screen_end() return self.on_screen_end("ENTRY")
self.scene_manager(current_time) self.scene_manager(current_time)
if is_l_don_pressed() or is_r_don_pressed(): if is_l_don_pressed() or is_r_don_pressed():
@@ -120,9 +124,6 @@ class TitleScreen:
global_tex.draw_texture('overlay', 'hit_taiko_to_start', index=0, fade=self.text_overlay_fade.attribute) global_tex.draw_texture('overlay', 'hit_taiko_to_start', index=0, fade=self.text_overlay_fade.attribute)
global_tex.draw_texture('overlay', 'hit_taiko_to_start', index=1, fade=self.text_overlay_fade.attribute) global_tex.draw_texture('overlay', 'hit_taiko_to_start', index=1, fade=self.text_overlay_fade.attribute)
def draw_3d(self):
pass
class WarningScreen: class WarningScreen:
"""Warning screen for the game""" """Warning screen for the game"""
class X: class X:

View File

@@ -1,3 +1,4 @@
import logging
import copy import copy
from pathlib import Path from pathlib import Path
from libs.tja import TJAParser from libs.tja import TJAParser
@@ -8,15 +9,16 @@ from libs.video import VideoPlayer
import pyray as ray import pyray as ray
from scenes.game import ClearAnimation, FCAnimation, FailAnimation, GameScreen, Player, Background, SCREEN_WIDTH, ResultTransition from scenes.game import ClearAnimation, FCAnimation, FailAnimation, GameScreen, Player, Background, SCREEN_WIDTH, ResultTransition
logger = logging.getLogger(__name__)
class TwoPlayerGameScreen(GameScreen): class TwoPlayerGameScreen(GameScreen):
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: super().on_screen_start()
super().on_screen_start() scene_preset = self.tja.metadata.scene_preset
scene_preset = self.tja.metadata.scene_preset if self.background is not None:
if self.background is not None: self.background.unload()
self.background.unload() self.background = Background(3, self.bpm, scene_preset=scene_preset)
self.background = Background(3, self.bpm, scene_preset=scene_preset) self.result_transition = ResultTransition(3)
self.result_transition = ResultTransition(3)
def load_hitsounds(self): def load_hitsounds(self):
"""Load the hit sounds""" """Load the hit sounds"""
@@ -26,12 +28,15 @@ class TwoPlayerGameScreen(GameScreen):
if global_data.hit_sound[0] == -1: if global_data.hit_sound[0] == -1:
audio.load_sound(Path('none.wav'), 'hitsound_don_1p') audio.load_sound(Path('none.wav'), 'hitsound_don_1p')
audio.load_sound(Path('none.wav'), 'hitsound_kat_1p') audio.load_sound(Path('none.wav'), 'hitsound_kat_1p')
logger.info("Loaded default (none) hit sounds for 1P")
elif global_data.hit_sound[0] == 0: elif global_data.hit_sound[0] == 0:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.wav", 'hitsound_don_1p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.wav", 'hitsound_don_1p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.wav", 'hitsound_kat_1p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.wav", 'hitsound_kat_1p')
logger.info("Loaded wav hit sounds for 1P")
else: else:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.ogg", 'hitsound_don_1p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "don.ogg", 'hitsound_don_1p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.ogg", 'hitsound_kat_1p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[0]) / "ka.ogg", 'hitsound_kat_1p')
logger.info("Loaded ogg hit sounds for 1P")
audio.set_sound_pan('hitsound_don_1p', 1.0) audio.set_sound_pan('hitsound_don_1p', 1.0)
audio.set_sound_pan('hitsound_kat_1p', 1.0) audio.set_sound_pan('hitsound_kat_1p', 1.0)
@@ -39,12 +44,15 @@ class TwoPlayerGameScreen(GameScreen):
if global_data.hit_sound[1] == -1: if global_data.hit_sound[1] == -1:
audio.load_sound(Path('none.wav'), 'hitsound_don_2p') audio.load_sound(Path('none.wav'), 'hitsound_don_2p')
audio.load_sound(Path('none.wav'), 'hitsound_kat_2p') audio.load_sound(Path('none.wav'), 'hitsound_kat_2p')
logger.info("Loaded default (none) hit sounds for 2P")
elif global_data.hit_sound[1] == 0: elif global_data.hit_sound[1] == 0:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don_2p.wav", 'hitsound_don_2p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don_2p.wav", 'hitsound_don_2p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka_2p.wav", 'hitsound_kat_2p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka_2p.wav", 'hitsound_kat_2p')
logger.info("Loaded wav hit sounds for 2P")
else: else:
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don.ogg", 'hitsound_don_2p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "don.ogg", 'hitsound_don_2p')
audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka.ogg", 'hitsound_kat_2p') audio.load_sound(sounds_dir / "hit_sounds" / str(global_data.hit_sound[1]) / "ka.ogg", 'hitsound_kat_2p')
logger.info("Loaded ogg hit sounds for 2P")
audio.set_sound_pan('hitsound_don_2p', 0.0) audio.set_sound_pan('hitsound_don_2p', 0.0)
audio.set_sound_pan('hitsound_kat_2p', 0.0) audio.set_sound_pan('hitsound_kat_2p', 0.0)
@@ -55,10 +63,12 @@ class TwoPlayerGameScreen(GameScreen):
self.init_tja(global_data.selected_song) self.init_tja(global_data.selected_song)
audio.play_sound('restart', 'sound') audio.play_sound('restart', 'sound')
self.song_started = False self.song_started = False
logger.info("F1 pressed: song restarted")
if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE): if ray.is_key_pressed(ray.KeyboardKey.KEY_ESCAPE):
if self.song_music is not None: if self.song_music is not None:
audio.stop_music_stream(self.song_music) audio.stop_music_stream(self.song_music)
logger.info("Escape pressed: returning to SONG_SELECT_2P")
return self.on_screen_end('SONG_SELECT_2P') return self.on_screen_end('SONG_SELECT_2P')
def init_tja(self, song: Path): def init_tja(self, song: Path):
@@ -77,6 +87,7 @@ class TwoPlayerGameScreen(GameScreen):
self.player_1 = Player(self.tja, 1, global_data.session_data[0].selected_difficulty, False, global_data.modifiers[0]) self.player_1 = Player(self.tja, 1, global_data.session_data[0].selected_difficulty, False, global_data.modifiers[0])
self.player_2 = Player(tja_copy, 2, global_data.session_data[1].selected_difficulty, True, global_data.modifiers[1]) self.player_2 = Player(tja_copy, 2, global_data.session_data[1].selected_difficulty, True, global_data.modifiers[1])
self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000) self.start_ms = (get_current_ms() - self.tja.metadata.offset*1000)
logger.info(f"TJA initialized for two-player song: {song}")
def spawn_ending_anims(self): def spawn_ending_anims(self):
if global_data.session_data[0].result_bad == 0: if global_data.session_data[0].result_bad == 0:
@@ -94,7 +105,7 @@ class TwoPlayerGameScreen(GameScreen):
self.player_2.ending_anim = FailAnimation(self.player_2.is_2p) self.player_2.ending_anim = FailAnimation(self.player_2.is_2p)
def update(self): def update(self):
self.on_screen_start() super(GameScreen, self).update()
current_time = get_current_ms() current_time = get_current_ms()
self.transition.update(current_time) self.transition.update(current_time)
self.current_ms = current_time - self.start_ms self.current_ms = current_time - self.start_ms

View File

@@ -1,20 +1,20 @@
import logging
from libs.utils import get_current_ms from libs.utils import get_current_ms
from scenes.result import Background, FadeIn, ResultPlayer, ResultScreen from scenes.result import Background, FadeIn, ResultPlayer, ResultScreen
class TwoPlayerResultScreen(ResultScreen): logger = logging.getLogger(__name__)
def __init__(self):
super().__init__()
class TwoPlayerResultScreen(ResultScreen):
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: super().on_screen_start()
super().on_screen_start() self.background = Background('3', 1280)
self.background = Background('3', self.width) self.fade_in = FadeIn('3')
self.fade_in = FadeIn('3') self.player_1 = ResultPlayer('1', True, False)
self.player_1 = ResultPlayer('1', True, False) self.player_2 = ResultPlayer('2', True, True)
self.player_2 = ResultPlayer('2', True, True) logger.info("TwoPlayerResultScreen started, background and players initialized")
def update(self): def update(self):
self.on_screen_start() super(ResultScreen, self).update()
current_time = get_current_ms() current_time = get_current_ms()
self.fade_in.update(current_time) self.fade_in.update(current_time)
self.player_1.update(current_time, self.fade_in.is_finished, self.is_skipped) self.player_1.update(current_time, self.fade_in.is_finished, self.is_skipped)

View File

@@ -1,19 +1,22 @@
import logging
from libs.file_navigator import SongFile from libs.file_navigator import SongFile
from libs.transition import Transition from libs.transition import Transition
from scenes.song_select import DiffSortSelect, SongSelectPlayer, SongSelectScreen, State from scenes.song_select import DiffSortSelect, SongSelectPlayer, SongSelectScreen, State
from libs.utils import get_current_ms, global_data from libs.utils import get_current_ms, global_data
from libs.audio import audio from libs.audio import audio
logger = logging.getLogger(__name__)
class TwoPlayerSongSelectScreen(SongSelectScreen): class TwoPlayerSongSelectScreen(SongSelectScreen):
def on_screen_start(self): def on_screen_start(self):
if not self.screen_init: super().on_screen_start()
super().on_screen_start() self.player_1 = SongSelectPlayer('1', self.text_fade_in)
self.player_1 = SongSelectPlayer('1', self.text_fade_in) self.player_2 = SongSelectPlayer('2', self.text_fade_in)
self.player_2 = SongSelectPlayer('2', self.text_fade_in)
def finalize_song(self): def finalize_song(self):
global_data.selected_song = self.navigator.get_current_item().path global_data.selected_song = self.navigator.get_current_item().path
global_data.session_data[0].genre_index = self.navigator.get_current_item().box.name_texture_index global_data.session_data[0].genre_index = self.navigator.get_current_item().box.name_texture_index
logger.info(f"Finalized song selection: {global_data.selected_song}")
def handle_input_browsing(self): def handle_input_browsing(self):
"""Handle input for browsing songs.""" """Handle input for browsing songs."""
@@ -79,24 +82,25 @@ class TwoPlayerSongSelectScreen(SongSelectScreen):
return return
p2_result = True p2_result = True
if result is not None: if result is not None:
print(result, p2_result) logger.info(f"Difficulty selection result: {result}, p2_result={p2_result}")
if result == "cancel": if result == "cancel":
self._cancel_selection() self._cancel_selection()
logger.info("Selection cancelled")
elif result == "confirm": elif result == "confirm":
if p2_result: if p2_result:
self._confirm_selection(2) self._confirm_selection(2)
else: else:
self._confirm_selection(1) self._confirm_selection(1)
logger.info("Selection confirmed")
elif result == "ura_toggle": elif result == "ura_toggle":
if p2_result: if p2_result:
self.ura_switch_animation.start(not self.player_2.is_ura) self.ura_switch_animation.start(not self.player_2.is_ura)
else: else:
self.ura_switch_animation.start(not self.player_1.is_ura) self.ura_switch_animation.start(not self.player_1.is_ura)
logger.info("Ura toggled")
def handle_input_diff_sort(self): def handle_input_diff_sort(self):
""" """Handle input for sorting difficulty."""
Handle input for sorting difficulty.
"""
if self.diff_sort_selector is None: if self.diff_sort_selector is None:
raise Exception("Diff sort selector was not able to be created") raise Exception("Diff sort selector was not able to be created")
@@ -108,6 +112,7 @@ class TwoPlayerSongSelectScreen(SongSelectScreen):
self.state = State.BROWSING self.state = State.BROWSING
self.text_fade_out.reset() self.text_fade_out.reset()
self.text_fade_in.reset() self.text_fade_in.reset()
logger.info(f"Diff sort selected: diff={diff}, level={level}")
if diff != -1: if diff != -1:
if level != -1: if level != -1:
self.navigator.diff_sort_diff = diff self.navigator.diff_sort_diff = diff
@@ -117,7 +122,7 @@ class TwoPlayerSongSelectScreen(SongSelectScreen):
def _cancel_selection(self): def _cancel_selection(self):
"""Reset to browsing state""" """Reset to browsing state"""
super()._cancel_selection() super()._cancel_selection()
self.player_2.selected_song = None self.player_2.selected_song = False
def _confirm_selection(self, player_selected: int): def _confirm_selection(self, player_selected: int):
"""Confirm song selection and create game transition""" """Confirm song selection and create game transition"""
@@ -133,6 +138,7 @@ class TwoPlayerSongSelectScreen(SongSelectScreen):
self.player_2.selected_diff_highlight_fade.start() self.player_2.selected_diff_highlight_fade.start()
self.player_2.selected_diff_text_resize.start() self.player_2.selected_diff_text_resize.start()
self.player_2.selected_diff_text_fadein.start() self.player_2.selected_diff_text_fadein.start()
logger.info(f"Confirmed selection for player {player_selected}")
def check_for_selection(self): def check_for_selection(self):
if (self.player_1.selected_diff_highlight_fade.is_finished and if (self.player_1.selected_diff_highlight_fade.is_finished and
@@ -148,6 +154,7 @@ class TwoPlayerSongSelectScreen(SongSelectScreen):
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()
logger.info(f"Game transition started for song: {title} - {subtitle}")
def update_players(self, current_time): def update_players(self, current_time):
self.player_1.update(current_time) self.player_1.update(current_time)
@@ -155,7 +162,8 @@ class TwoPlayerSongSelectScreen(SongSelectScreen):
if self.text_fade_out.is_finished: if self.text_fade_out.is_finished:
self.player_1.selected_song = True self.player_1.selected_song = True
self.player_2.selected_song = True self.player_2.selected_song = True
return "GAME_2P" next_screen = "GAME_2P"
return next_screen
def draw_background_diffs(self): def draw_background_diffs(self):
self.player_1.draw_background_diffs(self.state) self.player_1.draw_background_diffs(self.state)

317
uv.lock generated
View File

@@ -1,54 +1,38 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.11" requires-python = ">=3.14"
resolution-markers = [
"python_full_version >= '3.13'",
"python_full_version < '3.13'",
]
[[package]] [[package]]
name = "cffi" name = "cffi"
version = "1.17.1" version = "2.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pycparser" }, { name = "pycparser", marker = "implementation_name != 'PyPy'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
] ]
[[package]] [[package]]
@@ -114,62 +98,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/73/7d3b2010baa0b5eb1e4dfa9e4385e89b6716be76f2fa21a6c0fe34b68e5a/moviepy-2.2.1-py3-none-any.whl", hash = "sha256:6b56803fec2ac54b557404126ac1160e65448e03798fa282bd23e8fab3795060", size = 129871, upload-time = "2025-05-21T19:31:50.11Z" }, { url = "https://files.pythonhosted.org/packages/9a/73/7d3b2010baa0b5eb1e4dfa9e4385e89b6716be76f2fa21a6c0fe34b68e5a/moviepy-2.2.1-py3-none-any.whl", hash = "sha256:6b56803fec2ac54b557404126ac1160e65448e03798fa282bd23e8fab3795060", size = 129871, upload-time = "2025-05-21T19:31:50.11Z" },
] ]
[[package]]
name = "nuitka"
version = "2.8.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ordered-set" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/87/f20ffda1b6dc04361fa95390f4d47d974ee194e6e1e7688f13d324f3d89b/Nuitka-2.8.4.tar.gz", hash = "sha256:06b020ef33be97194f888dcfcd4c69c8452ceb61b31c7622e610d5156eb7923d", size = 3885111, upload-time = "2025-10-21T10:28:45.499Z" }
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.3.0" version = "2.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/db/8e12381333aea300890829a0a36bfa738cac95475d88982d538725143fd9/numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", size = 20382813, upload-time = "2025-06-07T14:54:32.608Z" } sdist = { url = "https://files.pythonhosted.org/packages/f3/db/8e12381333aea300890829a0a36bfa738cac95475d88982d538725143fd9/numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", size = 20382813, upload-time = "2025-06-07T14:54:32.608Z" }
[[package]]
name = "ordered-set"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826, upload-time = "2022-01-26T14:38:56.6Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/5f/df67435257d827eb3b8af66f585223dc2c3f2eb7ad0b50cb1dae2f35f494/numpy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9", size = 21199688, upload-time = "2025-06-07T14:36:52.067Z" }, { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/e5/ce/aad219575055d6c9ef29c8c540c81e1c38815d3be1fe09cdbe53d48ee838/numpy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b", size = 14359277, upload-time = "2025-06-07T14:37:15.325Z" },
{ url = "https://files.pythonhosted.org/packages/29/6b/2d31da8e6d2ec99bed54c185337a87f8fbeccc1cd9804e38217e92f3f5e2/numpy-2.3.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3", size = 5376069, upload-time = "2025-06-07T14:37:25.636Z" },
{ url = "https://files.pythonhosted.org/packages/7d/2a/6c59a062397553ec7045c53d5fcdad44e4536e54972faa2ba44153bca984/numpy-2.3.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4", size = 6913057, upload-time = "2025-06-07T14:37:37.215Z" },
{ url = "https://files.pythonhosted.org/packages/d5/5a/8df16f258d28d033e4f359e29d3aeb54663243ac7b71504e89deeb813202/numpy-2.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96", size = 14568083, upload-time = "2025-06-07T14:37:59.337Z" },
{ url = "https://files.pythonhosted.org/packages/0a/92/0528a563dfc2cdccdcb208c0e241a4bb500d7cde218651ffb834e8febc50/numpy-2.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779", size = 16929402, upload-time = "2025-06-07T14:38:24.343Z" },
{ url = "https://files.pythonhosted.org/packages/e4/2f/e7a8c8d4a2212c527568d84f31587012cf5497a7271ea1f23332142f634e/numpy-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58", size = 15879193, upload-time = "2025-06-07T14:38:48.007Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c3/dada3f005953847fe35f42ac0fe746f6e1ea90b4c6775e4be605dcd7b578/numpy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8", size = 18665318, upload-time = "2025-06-07T14:39:15.794Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ae/3f448517dedefc8dd64d803f9d51a8904a48df730e00a3c5fb1e75a60620/numpy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f", size = 6601108, upload-time = "2025-06-07T14:39:27.176Z" },
{ url = "https://files.pythonhosted.org/packages/8c/4a/556406d2bb2b9874c8cbc840c962683ac28f21efbc9b01177d78f0199ca1/numpy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd", size = 13021525, upload-time = "2025-06-07T14:39:46.637Z" },
{ url = "https://files.pythonhosted.org/packages/ed/ee/bf54278aef30335ffa9a189f869ea09e1a195b3f4b93062164a3b02678a7/numpy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8", size = 10170327, upload-time = "2025-06-07T14:40:02.703Z" },
{ url = "https://files.pythonhosted.org/packages/89/59/9df493df81ac6f76e9f05cdbe013cdb0c9a37b434f6e594f5bd25e278908/numpy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba", size = 20897025, upload-time = "2025-06-07T14:40:33.558Z" },
{ url = "https://files.pythonhosted.org/packages/2f/86/4ff04335901d6cf3a6bb9c748b0097546ae5af35e455ae9b962ebff4ecd7/numpy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e", size = 14129882, upload-time = "2025-06-07T14:40:55.034Z" },
{ url = "https://files.pythonhosted.org/packages/71/8d/a942cd4f959de7f08a79ab0c7e6cecb7431d5403dce78959a726f0f57aa1/numpy-2.3.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2", size = 5110181, upload-time = "2025-06-07T14:41:04.4Z" },
{ url = "https://files.pythonhosted.org/packages/86/5d/45850982efc7b2c839c5626fb67fbbc520d5b0d7c1ba1ae3651f2f74c296/numpy-2.3.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459", size = 6647581, upload-time = "2025-06-07T14:41:14.695Z" },
{ url = "https://files.pythonhosted.org/packages/1a/c0/c871d4a83f93b00373d3eebe4b01525eee8ef10b623a335ec262b58f4dc1/numpy-2.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a", size = 14262317, upload-time = "2025-06-07T14:41:35.862Z" },
{ url = "https://files.pythonhosted.org/packages/b7/f6/bc47f5fa666d5ff4145254f9e618d56e6a4ef9b874654ca74c19113bb538/numpy-2.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a", size = 16633919, upload-time = "2025-06-07T14:42:00.622Z" },
{ url = "https://files.pythonhosted.org/packages/f5/b4/65f48009ca0c9b76df5f404fccdea5a985a1bb2e34e97f21a17d9ad1a4ba/numpy-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67", size = 15567651, upload-time = "2025-06-07T14:42:24.429Z" },
{ url = "https://files.pythonhosted.org/packages/f1/62/5367855a2018578e9334ed08252ef67cc302e53edc869666f71641cad40b/numpy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc", size = 18361723, upload-time = "2025-06-07T14:42:51.167Z" },
{ url = "https://files.pythonhosted.org/packages/d4/75/5baed8cd867eabee8aad1e74d7197d73971d6a3d40c821f1848b8fab8b84/numpy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570", size = 6318285, upload-time = "2025-06-07T14:43:02.052Z" },
{ url = "https://files.pythonhosted.org/packages/bc/49/d5781eaa1a15acb3b3a3f49dc9e2ff18d92d0ce5c2976f4ab5c0a7360250/numpy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd", size = 12732594, upload-time = "2025-06-07T14:43:21.071Z" },
{ url = "https://files.pythonhosted.org/packages/c2/1c/6d343e030815c7c97a1f9fbad00211b47717c7fe446834c224bd5311e6f1/numpy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea", size = 9891498, upload-time = "2025-06-07T14:43:36.332Z" },
{ url = "https://files.pythonhosted.org/packages/73/fc/1d67f751fd4dbafc5780244fe699bc4084268bad44b7c5deb0492473127b/numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a", size = 20889633, upload-time = "2025-06-07T14:44:06.839Z" },
{ url = "https://files.pythonhosted.org/packages/e8/95/73ffdb69e5c3f19ec4530f8924c4386e7ba097efc94b9c0aff607178ad94/numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959", size = 14151683, upload-time = "2025-06-07T14:44:28.847Z" },
{ url = "https://files.pythonhosted.org/packages/64/d5/06d4bb31bb65a1d9c419eb5676173a2f90fd8da3c59f816cc54c640ce265/numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe", size = 5102683, upload-time = "2025-06-07T14:44:38.417Z" },
{ url = "https://files.pythonhosted.org/packages/12/8b/6c2cef44f8ccdc231f6b56013dff1d71138c48124334aded36b1a1b30c5a/numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb", size = 6640253, upload-time = "2025-06-07T14:44:49.359Z" },
{ url = "https://files.pythonhosted.org/packages/62/aa/fca4bf8de3396ddb59544df9b75ffe5b73096174de97a9492d426f5cd4aa/numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0", size = 14258658, upload-time = "2025-06-07T14:45:10.156Z" },
{ url = "https://files.pythonhosted.org/packages/1c/12/734dce1087eed1875f2297f687e671cfe53a091b6f2f55f0c7241aad041b/numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f", size = 16628765, upload-time = "2025-06-07T14:45:35.076Z" },
{ url = "https://files.pythonhosted.org/packages/48/03/ffa41ade0e825cbcd5606a5669962419528212a16082763fc051a7247d76/numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8", size = 15564335, upload-time = "2025-06-07T14:45:58.797Z" },
{ url = "https://files.pythonhosted.org/packages/07/58/869398a11863310aee0ff85a3e13b4c12f20d032b90c4b3ee93c3b728393/numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270", size = 18360608, upload-time = "2025-06-07T14:46:25.687Z" },
{ url = "https://files.pythonhosted.org/packages/2f/8a/5756935752ad278c17e8a061eb2127c9a3edf4ba2c31779548b336f23c8d/numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f", size = 6310005, upload-time = "2025-06-07T14:50:13.138Z" },
{ url = "https://files.pythonhosted.org/packages/08/60/61d60cf0dfc0bf15381eaef46366ebc0c1a787856d1db0c80b006092af84/numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5", size = 12729093, upload-time = "2025-06-07T14:50:31.82Z" },
{ url = "https://files.pythonhosted.org/packages/66/31/2f2f2d2b3e3c32d5753d01437240feaa32220b73258c9eef2e42a0832866/numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e", size = 9885689, upload-time = "2025-06-07T14:50:47.888Z" },
{ url = "https://files.pythonhosted.org/packages/f1/89/c7828f23cc50f607ceb912774bb4cff225ccae7131c431398ad8400e2c98/numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8", size = 20986612, upload-time = "2025-06-07T14:46:56.077Z" },
{ url = "https://files.pythonhosted.org/packages/dd/46/79ecf47da34c4c50eedec7511e53d57ffdfd31c742c00be7dc1d5ffdb917/numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3", size = 14298953, upload-time = "2025-06-07T14:47:18.053Z" },
{ url = "https://files.pythonhosted.org/packages/59/44/f6caf50713d6ff4480640bccb2a534ce1d8e6e0960c8f864947439f0ee95/numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f", size = 5225806, upload-time = "2025-06-07T14:47:27.524Z" },
{ url = "https://files.pythonhosted.org/packages/a6/43/e1fd1aca7c97e234dd05e66de4ab7a5be54548257efcdd1bc33637e72102/numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808", size = 6735169, upload-time = "2025-06-07T14:47:38.057Z" },
{ url = "https://files.pythonhosted.org/packages/84/89/f76f93b06a03177c0faa7ca94d0856c4e5c4bcaf3c5f77640c9ed0303e1c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8", size = 14330701, upload-time = "2025-06-07T14:47:59.113Z" },
{ url = "https://files.pythonhosted.org/packages/aa/f5/4858c3e9ff7a7d64561b20580cf7cc5d085794bd465a19604945d6501f6c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad", size = 16692983, upload-time = "2025-06-07T14:48:24.196Z" },
{ url = "https://files.pythonhosted.org/packages/08/17/0e3b4182e691a10e9483bcc62b4bb8693dbf9ea5dc9ba0b77a60435074bb/numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b", size = 15641435, upload-time = "2025-06-07T14:48:47.712Z" },
{ url = "https://files.pythonhosted.org/packages/4e/d5/463279fda028d3c1efa74e7e8d507605ae87f33dbd0543cf4c4527c8b882/numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555", size = 18433798, upload-time = "2025-06-07T14:49:14.866Z" },
{ url = "https://files.pythonhosted.org/packages/0e/1e/7a9d98c886d4c39a2b4d3a7c026bffcf8fbcaf518782132d12a301cfc47a/numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61", size = 6438632, upload-time = "2025-06-07T14:49:25.67Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ab/66fc909931d5eb230107d016861824f335ae2c0533f422e654e5ff556784/numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb", size = 12868491, upload-time = "2025-06-07T14:49:44.898Z" },
{ url = "https://files.pythonhosted.org/packages/ee/e8/2c8a1c9e34d6f6d600c83d5ce5b71646c32a13f34ca5c518cc060639841c/numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944", size = 9935345, upload-time = "2025-06-07T14:50:02.311Z" },
{ url = "https://files.pythonhosted.org/packages/6a/a2/f8c1133f90eaa1c11bbbec1dc28a42054d0ce74bc2c9838c5437ba5d4980/numpy-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b", size = 21070759, upload-time = "2025-06-07T14:51:18.241Z" },
{ url = "https://files.pythonhosted.org/packages/6c/e0/4c05fc44ba28463096eee5ae2a12832c8d2759cc5bcec34ae33386d3ff83/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5", size = 5301054, upload-time = "2025-06-07T14:51:27.413Z" },
{ url = "https://files.pythonhosted.org/packages/8a/3b/6c06cdebe922bbc2a466fe2105f50f661238ea223972a69c7deb823821e7/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2", size = 6817520, upload-time = "2025-06-07T14:51:38.015Z" },
{ url = "https://files.pythonhosted.org/packages/9d/a3/1e536797fd10eb3c5dbd2e376671667c9af19e241843548575267242ea02/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12", size = 14398078, upload-time = "2025-06-07T14:52:00.122Z" },
{ url = "https://files.pythonhosted.org/packages/7c/61/9d574b10d9368ecb1a0c923952aa593510a20df4940aa615b3a71337c8db/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97", size = 16751324, upload-time = "2025-06-07T14:52:25.077Z" },
{ url = "https://files.pythonhosted.org/packages/39/de/bcad52ce972dc26232629ca3a99721fd4b22c1d2bda84d5db6541913ef9c/numpy-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d", size = 12924237, upload-time = "2025-06-07T14:52:44.713Z" },
] ]
[[package]] [[package]]
@@ -177,59 +128,6 @@ name = "pillow"
version = "11.2.1" version = "11.2.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450, upload-time = "2025-04-12T17:47:37.135Z" },
{ url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550, upload-time = "2025-04-12T17:47:39.345Z" },
{ url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018, upload-time = "2025-04-12T17:47:41.128Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006, upload-time = "2025-04-12T17:47:42.912Z" },
{ url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773, upload-time = "2025-04-12T17:47:44.611Z" },
{ url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069, upload-time = "2025-04-12T17:47:46.46Z" },
{ url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460, upload-time = "2025-04-12T17:47:49.255Z" },
{ url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304, upload-time = "2025-04-12T17:47:51.067Z" },
{ url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809, upload-time = "2025-04-12T17:47:54.425Z" },
{ url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338, upload-time = "2025-04-12T17:47:56.535Z" },
{ url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918, upload-time = "2025-04-12T17:47:58.217Z" },
{ url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" },
{ url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" },
{ url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" },
{ url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" },
{ url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" },
{ url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" },
{ url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" },
{ url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" },
{ url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" },
{ url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" },
{ url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" },
{ url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" },
{ url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" },
{ url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" },
{ url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" },
{ url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" },
{ url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" },
{ url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" },
{ url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" },
{ url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" },
{ url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" },
{ url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" },
{ url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" },
{ url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" },
{ url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" },
{ url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" },
{ url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" },
{ url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" },
{ url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734, upload-time = "2025-04-12T17:49:46.789Z" },
{ url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841, upload-time = "2025-04-12T17:49:48.812Z" },
{ url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470, upload-time = "2025-04-12T17:49:50.831Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013, upload-time = "2025-04-12T17:49:53.278Z" },
{ url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165, upload-time = "2025-04-12T17:49:55.164Z" },
{ url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586, upload-time = "2025-04-12T17:49:57.171Z" },
{ url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751, upload-time = "2025-04-12T17:49:59.628Z" },
]
[[package]] [[package]]
name = "proglog" name = "proglog"
@@ -258,30 +156,6 @@ version = "5.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/ce/824ee634994e612156f7b84eaf50b8523c676ebfed8d8dd12939a82f4c15/pyinstrument-5.1.1.tar.gz", hash = "sha256:bc401cda990b3c1cfe8e0e0473cbd605df3c63b73478a89ac4ab108f2184baa8", size = 264730, upload-time = "2025-08-12T11:35:43.426Z" } sdist = { url = "https://files.pythonhosted.org/packages/80/ce/824ee634994e612156f7b84eaf50b8523c676ebfed8d8dd12939a82f4c15/pyinstrument-5.1.1.tar.gz", hash = "sha256:bc401cda990b3c1cfe8e0e0473cbd605df3c63b73478a89ac4ab108f2184baa8", size = 264730, upload-time = "2025-08-12T11:35:43.426Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/65/dc0fcc6122e6f484ffa48260d1023e1fe90e53da8d69a7de656205af91ba/pyinstrument-5.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5272cff6eea21163b2105f6a80c907315e0f567720621e6d5672dc01bf71ee48", size = 130287, upload-time = "2025-08-12T11:34:32.436Z" },
{ url = "https://files.pythonhosted.org/packages/90/96/8a6cab312342f1ad7322b459dcbdb2e041251b7bbe4c1d0dd38ccbf2ae20/pyinstrument-5.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4e7dc5a4aee37a44ff2e63db3127f2044dd95edcae240cb95915adbf223d4be", size = 122879, upload-time = "2025-08-12T11:34:33.506Z" },
{ url = "https://files.pythonhosted.org/packages/8a/89/1ad1ae703951832a34ec6b32354c6df1d0690b0333b7b8396c92afcdc23e/pyinstrument-5.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:602d55121df1d88aeb6d8ebc801597fdcb9718f78d602ae81458d65c56f25d24", size = 146412, upload-time = "2025-08-12T11:34:34.975Z" },
{ url = "https://files.pythonhosted.org/packages/a5/70/588ec07eeaa5d5d4aff2d47c8010379e9b39a7aee7e3fda625a0ce4f5838/pyinstrument-5.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63b5a788ff955e0597bc95463e64d5fa3747017524fdc02a0f5d12d5117cf2b9", size = 144885, upload-time = "2025-08-12T11:34:36.546Z" },
{ url = "https://files.pythonhosted.org/packages/0a/8f/119ad44d454edb927434bb53d8a69904ecaa47ec56f95c688c557a9590de/pyinstrument-5.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7f66038ec55a12e5510689240cdc745f8e98c90b93363f745106976e5cfb7397", size = 145530, upload-time = "2025-08-12T11:34:37.726Z" },
{ url = "https://files.pythonhosted.org/packages/8e/31/d53b20d1967ba78574807c5940251c0f238e8e3515ee9c4f20207eb09199/pyinstrument-5.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:110a08b6e7fa9542eb37c337e79467913d364a03bc2062f85566ba96bc82f54e", size = 144708, upload-time = "2025-08-12T11:34:38.855Z" },
{ url = "https://files.pythonhosted.org/packages/48/18/78ccb35dd265e83c0b7404a501004387b2e2390a0a44b6e4a004307931d1/pyinstrument-5.1.1-cp311-cp311-win32.whl", hash = "sha256:a223d5e9226ccede5bf2fbd4d13ce0aeb5120501b633ba85290ed94df37d3623", size = 124151, upload-time = "2025-08-12T11:34:40.392Z" },
{ url = "https://files.pythonhosted.org/packages/43/c0/3eb94aa2c2f0b8d49e290d4df662e21e707eb4a23f3b938239634fbfdc17/pyinstrument-5.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:16ce582a7b56287d338a8b59688458341aab5c6abda970ba50b2f7b3fd69f89d", size = 124942, upload-time = "2025-08-12T11:34:41.564Z" },
{ url = "https://files.pythonhosted.org/packages/76/3a/7824caf1fb419d0108f375a15b28cdd7ace8593f1ea56ef8276fddce9526/pyinstrument-5.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bcd6a03bdf180d73bc8dc7371e09dda089a48057095584e5f2818df1c820525b", size = 130306, upload-time = "2025-08-12T11:34:42.624Z" },
{ url = "https://files.pythonhosted.org/packages/3a/54/60ddd5eae617e29b58de774d178f4e4f7cdffd07ed1de36f976927ce69d3/pyinstrument-5.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ffa0948c1e268356dcf930c128624f34037ce92ee865fa4c056dee067aee4c5", size = 122817, upload-time = "2025-08-12T11:34:44.182Z" },
{ url = "https://files.pythonhosted.org/packages/35/12/35b694bfa58050607eedc80a68f64e6195c738249101a0dcbed0657147e7/pyinstrument-5.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c95adf98a920f2039eb0065966f980452a7af794bab387e9bfe8af3c681affa0", size = 148053, upload-time = "2025-08-12T11:34:45.589Z" },
{ url = "https://files.pythonhosted.org/packages/5d/4a/338b891f9119cf747153301d5d095942f378032309cd385e53857d03c2d2/pyinstrument-5.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bcb46ca8596b375c27850d4d06a1ce94ed78074774d35cbed3ccd28b663c5ba6", size = 146817, upload-time = "2025-08-12T11:34:46.668Z" },
{ url = "https://files.pythonhosted.org/packages/3b/cc/186cdb048fee445bbf9bd18819287a61b57b66ec68cfc47bc3c1e38b1ae6/pyinstrument-5.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3fc16597d26b24a46bf3455686300c0b8a3eb565ebc82396f402c031dccc0145", size = 146914, upload-time = "2025-08-12T11:34:47.878Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d1/533309830dd356d43e54d7feebaffab357f08568972285c609e98f7e6119/pyinstrument-5.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5aa135b4bd9667ddcb25fa582f4db77c5117ef207cb10ae901a8e4c5d5cde0e0", size = 146533, upload-time = "2025-08-12T11:34:49.015Z" },
{ url = "https://files.pythonhosted.org/packages/b8/df/2a656d6b1bd68ecfbb73c557906274a40ec7219dd92980fc1324997cf93e/pyinstrument-5.1.1-cp312-cp312-win32.whl", hash = "sha256:d15e37f8074b3043fca7aa985cb2079d2c221ccb0d27f059451ede800c801645", size = 124286, upload-time = "2025-08-12T11:34:50.285Z" },
{ url = "https://files.pythonhosted.org/packages/51/af/144d331cc9734e9141ac1a75f3ce904074ebc93dfe43cab44049ba8c8c28/pyinstrument-5.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c27d5cef0e809f213e5a94143c397d948650f5142c91dcce3611f584779183e", size = 125032, upload-time = "2025-08-12T11:34:51.369Z" },
{ url = "https://files.pythonhosted.org/packages/36/d4/b94f47aa7d301f6cdf5924bb75caacd0d0a1852bd4e876e3a64fc5798dad/pyinstrument-5.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:45af421c60c943a7f1619afabeba4951d4cc16b4206490d7d5b7ef5a4e2dfd42", size = 130315, upload-time = "2025-08-12T11:34:52.91Z" },
{ url = "https://files.pythonhosted.org/packages/1e/42/1bc2f28e139f69a0918d5d5dc1d59e65c640d4da9dd153fa48c2a8a87dd9/pyinstrument-5.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2603db3d745a65de66c96929ab9b0fcce050511eb24e32856ea2458785b8917f", size = 122805, upload-time = "2025-08-12T11:34:54.201Z" },
{ url = "https://files.pythonhosted.org/packages/a8/85/2f0c9115cd8a01e0a18d0650d9f3f20ff71e8ca17bd4af60dd3a0cb76f8a/pyinstrument-5.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe32492100efaa1b0a488c237fe420fdaf141646733a31a97f96c4e1fa6bbf8", size = 148210, upload-time = "2025-08-12T11:34:55.662Z" },
{ url = "https://files.pythonhosted.org/packages/86/62/3c73a63e6913378cc7e9ffb5af1e50836511eee83b7c7bf252fad7ec24e4/pyinstrument-5.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:999b5373f8b1e846357923063ae5c9275ad8a85ed4e0a42960a349288d1f5007", size = 146995, upload-time = "2025-08-12T11:34:57.133Z" },
{ url = "https://files.pythonhosted.org/packages/ab/8b/d21f4b6d8849881e9572967818e3e6d2dcb212e7dfa89e4e356d359db32b/pyinstrument-5.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58a2f69052178ec624e4df0cf546eda48b3a381572ac1cb3272b4c163888af9d", size = 147029, upload-time = "2025-08-12T11:34:58.255Z" },
{ url = "https://files.pythonhosted.org/packages/8a/4d/1e43cecf2bcf4a3dd1100f4fc7a3da6438a65d0b95ca7b8ab5d094ea7c0b/pyinstrument-5.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9bbc00d2e258edbefeb39b61ad4636099b08acd1effdd40d76883a13e7bf5a", size = 146668, upload-time = "2025-08-12T11:34:59.401Z" },
{ url = "https://files.pythonhosted.org/packages/34/48/00322b48e7adb665d04303b487454eb0c13a76ec0af8da20f452098fcc12/pyinstrument-5.1.1-cp313-cp313-win32.whl", hash = "sha256:cf2d8933e2aeaa02d4cb6279d83ef11ee882fb243fff96e3378153a730aadd6e", size = 124288, upload-time = "2025-08-12T11:35:00.514Z" },
{ url = "https://files.pythonhosted.org/packages/f5/14/d56515a110f74799aefc7489c1578ce4d99a4d731309559a427f954e7abc/pyinstrument-5.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:2402683a92617066b13a6d48f904396dcd15938016875b392534df027660eed4", size = 125041, upload-time = "2025-08-12T11:35:01.913Z" },
{ url = "https://files.pythonhosted.org/packages/18/2b/e4bdcabb5ae67de2ec3fa1f6e4eb4ae707b0bf460f895d4594792cdc919b/pyinstrument-5.1.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:688acba1c00cad73e43254e610f8e384a53ced3b0dbb5268fb44636e2b99663e", size = 130358, upload-time = "2025-08-12T11:35:03.569Z" }, { url = "https://files.pythonhosted.org/packages/18/2b/e4bdcabb5ae67de2ec3fa1f6e4eb4ae707b0bf460f895d4594792cdc919b/pyinstrument-5.1.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:688acba1c00cad73e43254e610f8e384a53ced3b0dbb5268fb44636e2b99663e", size = 130358, upload-time = "2025-08-12T11:35:03.569Z" },
{ url = "https://files.pythonhosted.org/packages/20/36/616f8db63997c096d3fb65e657cdf5bd2a63b53ed24a14750770dc500979/pyinstrument-5.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:116f5ad8cec4d6f5626305d7c1a104f5845a084bfb4b192d231eb8c41ea81f9a", size = 122827, upload-time = "2025-08-12T11:35:04.661Z" }, { url = "https://files.pythonhosted.org/packages/20/36/616f8db63997c096d3fb65e657cdf5bd2a63b53ed24a14750770dc500979/pyinstrument-5.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:116f5ad8cec4d6f5626305d7c1a104f5845a084bfb4b192d231eb8c41ea81f9a", size = 122827, upload-time = "2025-08-12T11:35:04.661Z" },
{ url = "https://files.pythonhosted.org/packages/af/7a/4f5d2bbc7c2466d46eb5ff47c6e667464eead47140e01a64be45215a59d4/pyinstrument-5.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d139d12a637001d3884344330054ce8335b2c8165dc3dd239726e1b358576bd", size = 147947, upload-time = "2025-08-12T11:35:05.786Z" }, { url = "https://files.pythonhosted.org/packages/af/7a/4f5d2bbc7c2466d46eb5ff47c6e667464eead47140e01a64be45215a59d4/pyinstrument-5.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d139d12a637001d3884344330054ce8335b2c8165dc3dd239726e1b358576bd", size = 147947, upload-time = "2025-08-12T11:35:05.786Z" },
@@ -302,23 +176,37 @@ wheels = [
[[package]] [[package]]
name = "pytaiko" name = "pytaiko"
version = "0.1.0" version = "0.9.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "moviepy" }, { name = "moviepy" },
{ name = "pyinstrument" },
{ name = "raylib-sdl" }, { name = "raylib-sdl" },
{ name = "tomlkit" }, { name = "tomlkit" },
] ]
[package.dev-dependencies]
dev = [
{ name = "nuitka" },
{ name = "pyinstrument" },
{ name = "ruff" },
{ name = "vulture" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "moviepy", specifier = ">=2.1.2" }, { name = "moviepy", specifier = ">=2.1.2" },
{ name = "pyinstrument", specifier = ">=5.1.1" }, { name = "raylib-sdl", path = "raylib_sdl-5.5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" },
{ name = "raylib-sdl", specifier = ">=5.5.0.2" },
{ name = "tomlkit", specifier = ">=0.13.3" }, { name = "tomlkit", specifier = ">=0.13.3" },
] ]
[package.metadata.requires-dev]
dev = [
{ name = "nuitka", specifier = ">=2.8.4" },
{ name = "pyinstrument", specifier = ">=5.1.1" },
{ name = "ruff", specifier = ">=0.14.2" },
{ name = "vulture", specifier = ">=2.14" },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.1.0" version = "1.1.0"
@@ -330,29 +218,42 @@ wheels = [
[[package]] [[package]]
name = "raylib-sdl" name = "raylib-sdl"
version = "5.5.0.2" version = "5.5.0.3"
source = { registry = "https://pypi.org/simple" } source = { path = "raylib_sdl-5.5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" }
dependencies = [ dependencies = [
{ name = "cffi" }, { name = "cffi" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/b6/24c241f86ed6101f277d600c8f69cd12368848f1e94ed82d21b4d9dfba11/raylib_sdl-5.5.0.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:265a312ea73d8987a4c58075db7f27e356e217d52b22383ccfacf07f6eeb3556", size = 2287593, upload-time = "2024-11-26T11:11:13.524Z" }, { filename = "raylib_sdl-5.5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fdd57c75ca760ceceea12f25ae6e22df1b3f11b322257a9272bbf25dea0bac93" },
{ url = "https://files.pythonhosted.org/packages/db/22/cada7093a7fc22ba8a68c032850175e7480b0e4149e782ee6093dca04006/raylib_sdl-5.5.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8e86aa9ee0190cf02ae44bace9dc87379d171141a62daddd963dea40d40dbca7", size = 1745212, upload-time = "2025-01-04T20:21:04.281Z" }, ]
{ url = "https://files.pythonhosted.org/packages/22/a2/387b20de1be19ece354a6c2cda30dc369948438dd824095b7da36ed974e7/raylib_sdl-5.5.0.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:3e2a91fef2525cc9a5a13fb3dbbd77e6c614b4d33f446c6047720516628b80d4", size = 2827982, upload-time = "2024-11-26T11:11:15.736Z" },
{ url = "https://files.pythonhosted.org/packages/1c/b4/f96cfb79981ac8e692975ae06ca224043d92b386b7f8a80cb515a85775c3/raylib_sdl-5.5.0.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5263010953358beffabca8f7f1dfedcccc0f931afb91cfb242fd7783c875d12c", size = 2967405, upload-time = "2024-11-26T11:11:18.039Z" }, [package.metadata]
{ url = "https://files.pythonhosted.org/packages/5b/6c/c26425f317392a98c414c1f14974f511b1abf7f36d54069c80026ce505a3/raylib_sdl-5.5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:4dcb099fa0bfa3048585d76445fe15cae3b2418c919f5777c54f429e1d3eb888", size = 1617306, upload-time = "2024-11-26T11:11:20.774Z" }, requires-dist = [{ name = "cffi", specifier = ">=2.0.0" }]
{ url = "https://files.pythonhosted.org/packages/f9/9d/8cd7466084f93b125c052f1825bc505d2822c19539dc5b3a2ac6a588fcaa/raylib_sdl-5.5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:48e374f5c20e618f4c7c5664ac9e4f39f53245ffb9b62d6d133b8676716363b0", size = 2290643, upload-time = "2024-11-26T11:11:23.67Z" },
{ url = "https://files.pythonhosted.org/packages/cf/b0/1891f9712bed1083f1360d7f57f042a48bed110b5d5224256a33eeb9f302/raylib_sdl-5.5.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6cd980a3fabdbe88a6449acf7ab24686854dce258dea9ff5b0eb46b7eae981c0", size = 1746303, upload-time = "2024-11-26T11:11:26.032Z" }, [[package]]
{ url = "https://files.pythonhosted.org/packages/24/de/6848b61e1df0eda838a61cb9e7060bd10d4af9616974c4360257c65ff8ff/raylib_sdl-5.5.0.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a8e0fd9399188943c81dadf60d3d13198da1f9e0d84cc2aefa9e0c0d5c6266d6", size = 2840025, upload-time = "2024-11-26T11:11:29.705Z" }, name = "ruff"
{ url = "https://files.pythonhosted.org/packages/5e/b8/d41eacab8666fb955877cb5e3d4afea7ba4d01f668d9e91aa1716c1230cb/raylib_sdl-5.5.0.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:92407aad5382958ec684c35d3a28c8fc091faca3b58fc5dd8b3495a1065520ff", size = 2985567, upload-time = "2024-11-26T11:11:32.887Z" }, version = "0.14.2"
{ url = "https://files.pythonhosted.org/packages/00/07/d9f26daeda03ccd2d5b637fc3da25d039b39c27f1c2dde0ae9eeb2c40861/raylib_sdl-5.5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ab31941d2247a043beb545f34ebef1844f1d9c5fac749032e991ee8c18439c5a", size = 1619555, upload-time = "2024-11-26T11:11:35.387Z" }, source = { registry = "https://pypi.org/simple" }
{ url = "https://files.pythonhosted.org/packages/06/9c/e3d596db50bf4fb35f39478f1f529446dded5a7142c0583dbd8b19d52fe4/raylib_sdl-5.5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bc6fe38110da1c98c341ede7064277ff79fb1b2a607787bd772974b4e4105d9a", size = 2290691, upload-time = "2024-11-26T11:11:38.045Z" }, sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663, upload-time = "2025-10-23T19:37:00.956Z" }
{ url = "https://files.pythonhosted.org/packages/d9/d3/70d418057bb162d04df5cc5249ff83c38d02b55dbfe9363334518f2276b6/raylib_sdl-5.5.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:696ff487d3688a6c36c05c0cfcad1cd891351268739e5dfc9037692e958caa56", size = 1746153, upload-time = "2024-11-26T11:11:41.109Z" }, wheels = [
{ url = "https://files.pythonhosted.org/packages/56/8b/ccea24888331953eb43f2aa869d0a757093ebe88036d5234d1e83ef1785e/raylib_sdl-5.5.0.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:10672ac9c3b3bac2da95d99b1e128b7ebded8aff4f3325063544ea55c0712a62", size = 2985152, upload-time = "2024-11-26T11:11:44.078Z" }, { url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390, upload-time = "2025-10-23T19:36:18.044Z" },
{ url = "https://files.pythonhosted.org/packages/15/d2/3a5e650d8c8dc3670a50ca000b6bed8f10f86872090eaa2a3450595f1528/raylib_sdl-5.5.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d7888a7ff42273a91b44520b209d24f20a9fd5eae0d9ac18851e52797c5d8b", size = 1619604, upload-time = "2024-11-26T11:11:46.22Z" }, { url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187, upload-time = "2025-10-23T19:36:22.664Z" },
{ url = "https://files.pythonhosted.org/packages/d9/2d/d531187e2c21d724fb07707dd71ac28d9e103d616ae595017f9b4679e8e2/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-macosx_10_13_x86_64.whl", hash = "sha256:51017cb725fce7da0b8f220839f6db91e20bbb3ba58e7debe3b1fd67f4468188", size = 1809261, upload-time = "2025-02-12T04:22:11.465Z" }, { url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177, upload-time = "2025-10-23T19:36:24.778Z" },
{ url = "https://files.pythonhosted.org/packages/f0/c5/81ec5b8e0c9b89e95b85d20fd4685368dabc0d7fedc8050efdfe3bbcc2f2/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.whl", hash = "sha256:9453c29fc28b08dcae8a6432aeb07f13fbf8504fb1351941df64b9fb8ba0bac3", size = 2180109, upload-time = "2025-02-12T04:22:14.472Z" }, { url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285, upload-time = "2025-10-23T19:36:26.979Z" },
{ url = "https://files.pythonhosted.org/packages/b0/4f/8ba71611c74d6e3ff5e95d2935c1f5f98fc61183ebf70d4dfb09547a5767/raylib_sdl-5.5.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fd3841ab8d8d6ca24e4fbffc9514c58c0cf5bd29e3f2406006eba872416325b3", size = 1499920, upload-time = "2025-02-12T04:22:16.996Z" }, { url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832, upload-time = "2025-10-23T19:36:29.192Z" },
{ url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995, upload-time = "2025-10-23T19:36:31.861Z" },
{ url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649, upload-time = "2025-10-23T19:36:33.915Z" },
{ url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182, upload-time = "2025-10-23T19:36:36.983Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516, upload-time = "2025-10-23T19:36:39.208Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690, upload-time = "2025-10-23T19:36:41.453Z" },
{ url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497, upload-time = "2025-10-23T19:36:43.61Z" },
{ url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116, upload-time = "2025-10-23T19:36:45.625Z" },
{ url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345, upload-time = "2025-10-23T19:36:47.618Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296, upload-time = "2025-10-23T19:36:49.789Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610, upload-time = "2025-10-23T19:36:51.882Z" },
{ url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318, upload-time = "2025-10-23T19:36:53.961Z" },
{ url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" },
{ url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" },
] ]
[[package]] [[package]]
@@ -375,3 +276,35 @@ sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
] ]
[[package]]
name = "vulture"
version = "2.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/25/925f35db758a0f9199113aaf61d703de891676b082bd7cf73ea01d6000f7/vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415", size = 58823, upload-time = "2024-12-08T17:39:43.319Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/56/0cc15b8ff2613c1d5c3dc1f3f576ede1c43868c1bc2e5ccaa2d4bcd7974d/vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9", size = 28915, upload-time = "2024-12-08T17:39:40.573Z" },
]
[[package]]
name = "zstandard"
version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
{ url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
{ url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
{ url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
{ url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
{ url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
{ url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
{ url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
{ url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
{ url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
]