Files
PyTaiko/libs/utils.py
Anthony Samms 1ef4d10ed6 minor fixes
2025-12-06 22:53:22 -05:00

447 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ctypes
import hashlib
import sys
import logging
import time
from libs.global_data import PlayerNum, global_data
from pathlib import Path
from typing import Optional
import pyray as ray
import raylib as rl
from raylib import (
SHADER_UNIFORM_FLOAT,
SHADER_UNIFORM_VEC2,
SHADER_UNIFORM_VEC4,
)
from libs.texture import TextureWrapper
logger = logging.getLogger(__name__)
def force_dedicated_gpu():
"""Force Windows to use dedicated GPU for this application"""
if sys.platform == "win32":
try:
# NVIDIA Optimus
nvapi = ctypes.windll.kernel32.LoadLibraryW("nvapi64.dll")
if nvapi:
ctypes.windll.kernel32.SetEnvironmentVariableW("SHIM_MCCOMPAT", "0x800000001")
except Exception as e:
logger.error(e)
def rounded(num: float) -> int:
"""Round a number to the nearest integer"""
sign = 1 if (num >= 0) else -1
num = abs(num)
result = int(num)
if (num - result >= 0.5):
result += 1
return sign * result
def get_current_ms() -> int:
"""Get the current time in milliseconds"""
return rounded(time.time() * 1000)
def strip_comments(code: str) -> str:
"""Strip comments from a string of code"""
result = ''
index = 0
for line in code.splitlines():
comment_index = line.find('//')
if comment_index == -1:
result += line
elif comment_index != 0 and not line[:comment_index].isspace():
result += line[:comment_index]
index += 1
return result
def is_input_key_pressed(keys: list[int], gamepad_buttons: list[int]):
if global_data.input_locked:
return False
for key in keys:
if rl.IsKeyPressed(key):
return True
if rl.IsGamepadAvailable(0):
for button in gamepad_buttons:
if rl.IsGamepadButtonPressed(0, button):
return True
return False
def is_l_don_pressed(player_num: PlayerNum = PlayerNum.ALL) -> bool:
"""Check if the left don button is pressed"""
if player_num == PlayerNum.ALL:
keys = global_data.config["keys_1p"]["left_don"] + global_data.config["keys_2p"]["left_don"]
elif player_num == PlayerNum.P1:
keys = global_data.config["keys_1p"]["left_don"]
elif player_num == PlayerNum.P2:
keys = global_data.config["keys_2p"]["left_don"]
else:
return False
gamepad_buttons = global_data.config["gamepad"]["left_don"]
return is_input_key_pressed(keys, gamepad_buttons)
def is_r_don_pressed(player_num: PlayerNum = PlayerNum.ALL) -> bool:
"""Check if the right don button is pressed"""
if player_num == PlayerNum.ALL:
keys = global_data.config["keys_1p"]["right_don"] + global_data.config["keys_2p"]["right_don"]
elif player_num == PlayerNum.P1:
keys = global_data.config["keys_1p"]["right_don"]
elif player_num == PlayerNum.P2:
keys = global_data.config["keys_2p"]["right_don"]
else:
return False
gamepad_buttons = global_data.config["gamepad"]["right_don"]
return is_input_key_pressed(keys, gamepad_buttons)
def is_l_kat_pressed(player_num: PlayerNum = PlayerNum.ALL) -> bool:
"""Check if the left kat button is pressed"""
if player_num == PlayerNum.ALL:
keys = global_data.config["keys_1p"]["left_kat"] + global_data.config["keys_2p"]["left_kat"]
elif player_num == PlayerNum.P1:
keys = global_data.config["keys_1p"]["left_kat"]
elif player_num == PlayerNum.P2:
keys = global_data.config["keys_2p"]["left_kat"]
else:
return False
gamepad_buttons = global_data.config["gamepad"]["left_kat"]
return is_input_key_pressed(keys, gamepad_buttons)
def is_r_kat_pressed(player_num: PlayerNum = PlayerNum.ALL) -> bool:
"""Check if the right kat button is pressed"""
if player_num == PlayerNum.ALL:
keys = global_data.config["keys_1p"]["right_kat"] + global_data.config["keys_2p"]["right_kat"]
elif player_num == PlayerNum.P1:
keys = global_data.config["keys_1p"]["right_kat"]
elif player_num == PlayerNum.P2:
keys = global_data.config["keys_2p"]["right_kat"]
else:
return False
gamepad_buttons = global_data.config["gamepad"]["right_kat"]
return is_input_key_pressed(keys, gamepad_buttons)
global_tex = TextureWrapper()
text_cache = set()
if not Path('cache/image').exists():
if not Path('cache').exists():
Path('cache').mkdir()
Path('cache/image').mkdir()
for file in Path('cache/image').iterdir():
text_cache.add(file.stem)
class OutlinedText:
"""Create an outlined text object."""
def __init__(self, text: str, font_size: int, color: ray.Color, outline_thickness=5.0, vertical=False):
"""
Create an outlined text object.
Args:
text (str): The text to be displayed.
font_size (int): The size of the font.
color (ray.Color): The color of the text.
outline_color (ray.Color): The color of the outline.
outline_thickness (float): The thickness of the outline.
vertical (bool): Whether the text is vertical or not.
"""
self.text = text
self.hash = self._hash_text(text, font_size, color, vertical)
self.outline_thickness = outline_thickness * global_tex.screen_scale
self.vertical = vertical
if self.hash in text_cache:
self.texture = ray.load_texture(f'cache/image/{self.hash}.png')
else:
self.font = self._load_font_for_text(text)
if vertical:
self.texture = self._create_text_vertical(text, font_size, color, ray.BLANK, self.font)
else:
self.texture = self._create_text_horizontal(text, font_size, color, ray.BLANK, self.font)
ray.gen_texture_mipmaps(self.texture)
ray.set_texture_filter(self.texture, ray.TextureFilter.TEXTURE_FILTER_TRILINEAR)
outline_size = ray.ffi.new('float*', self.outline_thickness)
texture_size = ray.ffi.new("float[2]", [self.texture.width, self.texture.height])
self.shader = ray.load_shader('shader/outline.vs', 'shader/outline.fs')
self.outline_size_loc = ray.get_shader_location(self.shader, "outlineSize")
self.outline_color_loc = ray.get_shader_location(self.shader, "outlineColor")
self.texture_size_loc = ray.get_shader_location(self.shader, "textureSize")
self.alpha_loc = ray.get_shader_location(self.shader, "alpha")
ray.set_shader_value(self.shader, self.outline_size_loc, outline_size, SHADER_UNIFORM_FLOAT)
ray.set_shader_value(self.shader, self.texture_size_loc, texture_size, SHADER_UNIFORM_VEC2)
self.default_src = ray.Rectangle(0, 0, self.texture.width, self.texture.height)
def _hash_text(self, text: str, font_size: int, color: ray.Color, vertical: bool):
n = hashlib.sha256()
n.update(text.encode('utf-8'))
n.update(str(font_size).encode('utf-8'))
if isinstance(color, tuple):
n.update(str(color[0]).encode('utf-8'))
n.update(str(color[1]).encode('utf-8'))
n.update(str(color[2]).encode('utf-8'))
n.update(str(color[3]).encode('utf-8'))
else:
n.update(str(color.r).encode('utf-8'))
n.update(str(color.g).encode('utf-8'))
n.update(str(color.b).encode('utf-8'))
n.update(str(color.a).encode('utf-8'))
n.update(str(vertical).encode('utf-8'))
return n.hexdigest()
def _load_font_for_text(self, text: str) -> ray.Font:
reload_font = False
for character in text:
if character not in global_data.font_codepoints:
global_data.font_codepoints.add(character)
reload_font = True
if reload_font:
codepoint_count = ray.ffi.new('int *', 0)
codepoints = ray.load_codepoints(''.join(global_data.font_codepoints), codepoint_count)
global_data.font = ray.load_font_ex(str(Path('Graphics/Modified-DFPKanteiryu-XB.ttf')), 40, codepoints, len(global_data.font_codepoints))
logger.info(f"Reloaded font with {len(global_data.font_codepoints)} codepoints")
return global_data.font
def _create_text_vertical(self, text: str, font_size: int, color: ray.Color, bg_color: ray.Color, font: Optional[ray.Font]=None, padding: int=10):
rotate_chars = {'-', '', '|', '/', '\\', '', '', '~', '', '', '(', ')',
'', '', '[', ']', '', '', '', '', '', '', '', ':', ''}
side_punctuation = {'.', ',', '', '', "'", '"', '´', '`'}
horizontal_punct = {'?', '!', '', '', ''} # Characters that should be drawn horizontally when repeated
lowercase_kana = {
'', '','', '','', '','', '','', '',
'', '','', '','', '','', '','', '',
'', '','','','','','','','','','',
'','','','','','',''
}
# Group consecutive horizontal punctuation marks
def group_horizontal_sequences(text):
groups = []
i = 0
while i < len(text):
if text[i] in horizontal_punct:
# Start of a horizontal sequence
sequence = text[i]
j = i + 1
# Continue collecting consecutive horizontal punctuation
while j < len(text) and text[j] in horizontal_punct:
sequence += text[j]
j += 1
# Only treat as horizontal if there are 2 or more characters
if len(sequence) >= 2:
groups.append(('horizontal', sequence))
else:
groups.append(('single', sequence))
i = j
else:
groups.append(('single', text[i]))
i += 1
return groups
# Helper function to calculate adjusted character height
def get_char_height(char):
if char in side_punctuation:
return font_size // 4
elif char.islower() or char in lowercase_kana:
return font_size * 0.88
elif char.isspace():
return font_size * 0.6
else:
return font_size
grouped_text = group_horizontal_sequences(text)
# Calculate dimensions with proper height adjustments
max_char_width = 0
total_height = padding * 2
for group_type, content in grouped_text:
if group_type == 'horizontal':
# For horizontal sequences, measure the combined width
if font:
seq_size = ray.measure_text_ex(font, content, font_size, 0)
else:
seq_width = ray.measure_text(content, font_size)
seq_size = ray.Vector2(seq_width, font_size)
max_char_width = max(max_char_width, seq_size.x)
total_height += font_size # Horizontal sequences use full font_size
else:
# Single character
char = content
if font:
char_size = ray.measure_text_ex(font, char, font_size, 0)
else:
char_width = ray.measure_text(char, font_size)
char_size = ray.Vector2(char_width, font_size)
if char in rotate_chars:
effective_width = char_size.y
else:
effective_width = char_size.x
max_char_width = max(max_char_width, effective_width)
# Use the adjusted height instead of fixed font_size
total_height += get_char_height(char)
width = int(max_char_width + (padding * 2))
height = int(total_height) # Make sure it's an integer
image = ray.gen_image_color(width, height, bg_color)
curr_char_y = padding - font_size
for group_type, content in grouped_text:
if group_type == 'horizontal':
# Handle horizontal punctuation sequence
char_y = font_size
curr_char_y += char_y
if font:
seq_size = ray.measure_text_ex(font, content, font_size, 0)
seq_image = ray.image_text_ex(font, content, font_size, 0, color)
else:
seq_width = ray.measure_text(content, font_size)
seq_size = ray.Vector2(seq_width, font_size)
seq_image = ray.image_text(content, font_size, color)
# Center the horizontal sequence
char_x = width // 2 - seq_size.x // 2
ray.image_draw(image, seq_image,
ray.Rectangle(0, 0, seq_image.width, seq_image.height),
ray.Rectangle(char_x, curr_char_y, seq_image.width, seq_image.height),
ray.WHITE)
ray.unload_image(seq_image)
else:
# Handle single character (existing logic)
char = content
char_y = get_char_height(char) # Use the helper function
curr_char_y += char_y
if font:
char_size = ray.measure_text_ex(font, char, font_size, 0)
char_image = ray.image_text_ex(font, char, font_size, 0, color)
else:
char_width = ray.measure_text(char, font_size)
char_size = ray.Vector2(char_width, font_size)
char_image = ray.image_text(char, font_size, color)
if char in rotate_chars:
rotated_image = ray.gen_image_color(char_image.height, char_image.width, ray.BLANK)
for y in range(char_image.height):
for x in range(char_image.width):
src_color = ray.get_image_color(char_image, x, y)
new_x = char_image.height - 1 - y
new_y = x
ray.image_draw_pixel(rotated_image, new_x, new_y, src_color)
ray.unload_image(char_image)
char_image = rotated_image
effective_width = char_size.y
else:
effective_width = char_size.x
char_x = width // 2 - effective_width // 2
if char in side_punctuation:
char_x += font_size//3
ray.image_draw(image, char_image,
ray.Rectangle(0, 0, char_image.width, char_image.height),
ray.Rectangle(char_x, curr_char_y, char_image.width, char_image.height),
ray.WHITE)
ray.unload_image(char_image)
ray.export_image(image, f'cache/image/{self.hash}.png')
texture = ray.load_texture_from_image(image)
ray.unload_image(image)
return texture
def _create_text_horizontal(self, text: str, font_size: int, color: ray.Color, bg_color: ray.Color, font: Optional[ray.Font]=None, padding: int=10):
if font:
text_size = ray.measure_text_ex(font, text, font_size, 0)
total_width = text_size.x + (padding * 2)
total_height = text_size.y + (padding * 2)
else:
total_width = ray.measure_text(text, font_size) + (padding * 2)
total_height = font_size + (padding * 2)
image = ray.gen_image_color(int(total_width), int(total_height), bg_color)
if font:
text_image = ray.image_text_ex(font, text, font_size, 0, color)
else:
text_image = ray.image_text(text, font_size, color)
text_x = padding
text_y = padding
ray.image_draw(image, text_image,
ray.Rectangle(0, 0, text_image.width, text_image.height),
ray.Rectangle(text_x, text_y, text_image.width, text_image.height),
ray.WHITE)
ray.unload_image(text_image)
ray.export_image(image, f'cache/image/{self.hash}.png')
texture = ray.load_texture_from_image(image)
ray.unload_image(image)
return texture
def draw(self, outline_color: ray.Color=ray.BLANK, color: ray.Color=ray.WHITE, scale: float = 1.0, center: bool = False,
x: float = 0, y: float = 0, x2: float = 0, y2: float = 0,
origin: ray.Vector2 = ray.Vector2(0,0), rotation: float = 0, fade: float = 1.1) -> None:
"""
Wrapper function for raylib's draw_texture_pro().
Parameters:
outline_color (ray.Color): The color to outline the text.
color (ray.Color): The color to tint the text.
x (float): An x-value added to the top-left corner of the text.
y (float): The y-value added to the top-left corner of the text.
x2 (float): The x-value added to the bottom-right corner of the text.
y2 (float): The y-value added to the bottom-right corner of the text.
origin (ray.Vector2): The origin point of the text.
rotation (float): The rotation angle of the text.
fade (float): The fade factor to apply to the text.
"""
if isinstance(outline_color, tuple):
outline_color_alloc = ray.ffi.new("float[4]", [
outline_color[0] / 255.0,
outline_color[1] / 255.0,
outline_color[2] / 255.0,
outline_color[3] / 255.0
])
else:
outline_color_alloc = ray.ffi.new("float[4]", [
outline_color.r / 255.0,
outline_color.g / 255.0,
outline_color.b / 255.0,
outline_color.a / 255.0
])
ray.set_shader_value(self.shader, self.outline_color_loc, outline_color_alloc, SHADER_UNIFORM_VEC4)
if isinstance(color, tuple):
alpha_value = ray.ffi.new('float*', min(fade * 255, color[3]) / 255.0)
else:
alpha_value = ray.ffi.new('float*', min(fade * 255, color.a) / 255.0)
if fade != 1.1:
final_color = ray.fade(color, fade)
else:
final_color = color
ray.set_shader_value(self.shader, self.alpha_loc, alpha_value, SHADER_UNIFORM_FLOAT)
if not self.vertical:
offset = (10 * global_tex.screen_scale)-10
else:
offset = 0
dest_rect = ray.Rectangle(x, y+offset, self.texture.width+x2, self.texture.height+y2)
if self.outline_thickness > 0:
ray.begin_shader_mode(self.shader)
ray.draw_texture_pro(self.texture, self.default_src, dest_rect, origin, rotation, final_color)
if self.outline_thickness > 0:
ray.end_shader_mode()
def unload(self):
"""
Unload the outlined text object.
Args:
None
"""
ray.unload_shader(self.shader)
ray.unload_texture(self.texture)