diff options
Diffstat (limited to 'pygame/__init__.py')
-rwxr-xr-x | pygame/__init__.py | 695 |
1 files changed, 695 insertions, 0 deletions
diff --git a/pygame/__init__.py b/pygame/__init__.py new file mode 100755 index 0000000..b6ec930 --- /dev/null +++ b/pygame/__init__.py @@ -0,0 +1,695 @@ +#!/usr/bin/python3 + +from dataclasses import dataclass +from functools import lru_cache, partial +from pathlib import Path +import subprocess +import selectors + +#import pygame_sdl2 as pygame +import pygame +import yaml + +from .profiledb import ProfileDB + +from gamechest.cliactions import run +from gamechest.gamedb import GameDB +from gamechest.statusdb import StatusDB +from gamechest.gameconfig import config +from gamechest.runners.install import Install +from gamechest.runners.remove import Remove + + +# 16:9 ratio +#WINDOW_RESOLUTION = (1280, 720) +#WINDOW_RESOLUTION = (800, 600) +WINDOW_RESOLUTION = (1024, 576) + +if pygame.ver[0] != '2': + raise ValueError(f'pygame2 required, got {pygame.ver}') + + +class Event(Exception): pass +class QuitEvent(Event): pass + + +@lru_cache(maxsize=40) +def get_text_surface(text, font, color): + new_surface = font.render(text, Parameters.font_antialias, color) + print('cache miss for', text) + return new_surface + + +def calculate_new_size(size_max, size): + width_max, height_max = size_max + width, height = size + + if width > width_max: + ratio = width_max / width + width = width_max + height = height * ratio + + if height > height_max: + ratio = height_max / height + height = height_max + width = width * ratio + + return width, height + + +@lru_cache(maxsize=40) +def get_image_surface(path, resize=None, alpha=False): + print('cache miss for', path) + image = pygame.image.load(str(path)) + if alpha: + image = image.convert_alpha() + else: + image = image.convert() + if resize: + new_size = calculate_new_size(resize, (image.get_width(), + image.get_height())) + image = pygame.transform.scale(image, new_size) + print('new_size', new_size) + return image + + +@dataclass +class AppState: + menu_selected_index: int = 0 + + +class Parameters: + key_delay = 330 + key_interval = 1000//25 + font_antialias = True + + + + +class GuiApp: + + def __init__(self): + pygame.init() + pygame.key.set_repeat(Parameters.key_delay, Parameters.key_interval) + #print(pygame.display.list_modes()) + self.window = pygame.display.set_mode(WINDOW_RESOLUTION) + self.clock = pygame.time.Clock() + self.cycles_per_second = 60 + self.menu_font = pygame.font.Font(pygame.font.get_default_font(), 16) + self.menu_color = (0, 0, 200) + self.render_timer = 0 + self.render_timeout = 1000//30 # 30 fps + + def process_input(self, last_frame_time, event=None): + if event is None: + event = pygame.event.poll() + if event.type == pygame.KEYDOWN: + print('event', event, 'key name', pygame.key.name(event.key)) + if event.key in (pygame.K_q, pygame.K_ESCAPE): + raise QuitEvent() + if event.type == pygame.QUIT: + raise QuitEvent() + + def update(self, last_frame_time): + pass + + def render(self, last_frame_time): + self.render_timer += last_frame_time + if self.render_timer < self.render_timeout: + return + self.render_timer = 0 + + self.window.fill((0, 0, 0)) + + # Initial y + y = 50 + x = 50 + + self.window.blit(surface, (x, y)) + + pygame.display.update() + + + def loop(self): + last_frame_time = 1 # equivalent to 0 but avoid div0 + last_render_time = 1 + try: + while 1: + self.process_input(last_frame_time) + self.update(last_frame_time) + self.render(last_frame_time) + last_frame_time = self.clock.tick(self.cycles_per_second) + except QuitEvent: + pass + + print('quitting') + pygame.quit() + + +class ProfileDataObserver: + + def event_current_profile_changed(self, new_profile): + pass + + +class ObservedSubject: + def __init__(self): + self.observers = [] + + def add_observer(self, observer): + self.observers.append(observer) + + def del_observer(self, observer): + self.observers.remove(observer) + + def notify_event(self, event_name, *args, **kws): + for observer in self.observers: + getattr(observer, event_name)(*args, **kws) + + +class ProfileData(ObservedSubject): + + def __init__(self): + super().__init__() + self.profiledb = ProfileDB() + self.profiles_len = len(self.profiledb.get_profiles()) + self.current_profile_index = 0 + configured_default_profile = config.get_profile_id() + for index, profile in enumerate(self.profiledb.get_profiles()): + if profile['name'] == configured_default_profile: + self.current_profile_index = index + + def change_active_profile(self, new_index): + if new_index < 0: + new_index = 0 + elif new_index >= self.profiles_len: + new_index = self.profiles_len-1 + if self.current_profile_index != new_index: + self.current_profile_index = new_index + self.notify_event('event_current_profile_changed', + self.get_current_profile()) + + def change_profile_increase(self): + self.change_active_profile(self.current_profile_index +1) + + def change_profile_decrease(self): + self.change_active_profile(self.current_profile_index -1) + + def get_current_profile(self): + return self.profiledb.get_profiles()[self.current_profile_index]['name'] + + def get_current_profile_avatar(self): + return self.profiledb.get_profiles()[self.current_profile_index].get('avatar', None) + + def get_current_display(self): + return self.profiledb.get_profiles()[self.current_profile_index]['display'] + + +class ProfileRenderer(ProfileDataObserver): + + def __init__(self, profiledata): + self.profiledata = profiledata + self.surface_to_render = None + profiledata.add_observer(self) + self.font_filename = pygame.font.match_font('bitstreamverasans') + print('ProfileRenderer font:', self.font_filename) + self.font = pygame.font.Font(self.font_filename, 24) + self.font_fg_color = (0, 200, 200) + self.avatar_surface = None + self.avatar_size = (100, 100) + self.image_box = get_image_surface(config.get_data_d() / 'asset/box.png', resize=(170, 170), alpha=True) + self.load_avatar() + + def load_avatar(self): + self.avatar_surface = None + if path := self.profiledata.get_current_profile_avatar(): + path = config.get_data_d() / path + if path.exists(): + self.avatar_surface = get_image_surface(path, + self.avatar_size) + else: + print('no avatar found at', path) + + def event_current_profile_changed(self, new_profile): + self.surface_to_render = None + self.load_avatar() + print('changing rendering profile to', new_profile) + + def render(self, surface, pos): + if self.surface_to_render is None: + profile_name = self.profiledata.get_current_profile() + new_surface = get_text_surface( + self.profiledata.get_current_display(), + self.font, self.font_fg_color) + #new_surface = self.font.render(self.profiledata.get_current_display(), Parameters.font_antialias, self.font_fg_color) + self.surface_to_render = new_surface + avatar_pos = (pos[0] + 15, pos[1] + 15) + #text_pos = (avatar_pos[0] + self.avatar_size[0] + 5, avatar_pos[1]) + text_pos = (avatar_pos[0], avatar_pos[1] + self.avatar_size[1]) + surface.blit(self.image_box, pos) + if self.avatar_surface: + surface.blit(self.avatar_surface, avatar_pos) + surface.blit(self.surface_to_render, text_pos) + + + +class GameData(ObservedSubject): + + def __init__(self): + super().__init__() + self.game_db = GameDB() + self.games_idtitles = list(self.game_db.get_idtitles()) + self.games_len = len(self.games_idtitles) + self.game_index = 0 + + def change_active_game(self, new_index): + if new_index < 0: + new_index = 0 + elif new_index >= self.games_len: + new_index = self.games_len-1 + if self.game_index != new_index: + self.game_index = new_index + self.notify_event('event_current_game_changed', + self.get_current_gameinfo()) + + #def get_idtitles(self): + # return self.games_idtitles + + def get_games_subset(self): + low_index = max(self.game_index-3, 0) + high_index = low_index + 6 + for index, item in enumerate(self.games_idtitles[low_index:high_index], start=low_index): + selected = 1 if self.game_index == index else 0 + game_info = self.game_db.get_info(item[0]) + current_icon = game_info.get( + 'icon', None) + if current_icon is not None: + current_icon = config.get_data_d() / current_icon + yield selected, item[0], item[1], current_icon, game_info + + def get_current_gameinfo(self): + return self.game_db.get_info(self.get_current_id()) + + def get_current_id(self): + current_id = self.games_idtitles[self.game_index][0] + return current_id + + def get_current_title(self): + current_title = self.games_idtitles[self.game_index][1] + return current_title + + def change_game_increase(self): + self.change_active_game(self.game_index +1) + + def change_game_decrease(self): + self.change_active_game(self.game_index -1) + +class GameDataObserver: + + def event_current_game_changed(self, new_game_info): pass + +class GameRenderer(GameDataObserver): + + def __init__(self, gamedata, status_db): + self.gamedata = gamedata + self.status_db = status_db + gamedata.add_observer(self) + self.font_filename = pygame.font.match_font('bitstreamverasans') + print('GameRenderer font:', self.font_filename) + self.font = pygame.font.Font(self.font_filename, 16) + self.font_selected = pygame.font.Font(self.font_filename, 24) + self.font_fg_color = (90, 90, 200) + #self.image_right_arrow = get_image_surface(config.get_data_d() / 'asset/arrow-right.png', + # resize=(16, 16), + # alpha=True) + self.image_right_arrow = get_image_surface(config.get_data_d() / 'asset/bulb.png', + resize=(16, 16), + alpha=True) + self.image_box = get_image_surface(config.get_data_d() / 'asset/box.png', resize=(400, 400), alpha=True) + self.image_box.set_alpha(220) + + def event_current_game_changed(self, new_game_info): + print('game changed, new game:', new_game_info['title']) + + def render(self, dest_surface, pos): + x = pos[0] + y = pos[1] + y_pad = 32 + surface_height = None + #selected_item_y = y + #low_index = max(selected_index-3, 0) + #high_index = low_index + 6 + dest_surface.blit(self.image_box, pos) + x += 15 + y += 45 + for selected, game_id, game_title, game_icon, game_info in self.gamedata.get_games_subset(): + text = game_title + if not self.status_db.is_installed(game_info): + text = f'[u] {text}' + else: + text = f'[i] {text}' + arrow_width = self.image_right_arrow.get_width() + text_x = x + y_pad + surface = get_text_surface(text, + self.font_selected if selected else + self.font, + self.font_fg_color) + if game_icon and game_icon.exists(): + icon_surface = get_image_surface(game_icon, + resize=(y_pad, y_pad), + alpha=True) + icon_pos = (x + arrow_width, + y + (surface.get_height() + - icon_surface.get_height())/2) + + dest_surface.blit(icon_surface, icon_pos) + dest_surface.blit(surface, (text_x + + arrow_width + 5, y)) + if selected: + surface_height = surface.get_height() + #selected_item_y = y + dest_surface.blit(self.image_right_arrow, (x, y + (surface_height - self.image_right_arrow.get_height())/2)) + y += surface.get_height() + y_pad + + +class GameCoverRenderer(GameDataObserver): + + def __init__(self, gamedata): + self.gamedata = gamedata + gamedata.add_observer(self) + self.current_game_info = self.gamedata.get_current_gameinfo() + self.cover_shift = (0, 0) + self.default_cover_surface = get_image_surface(config.get_data_d() / 'asset/default_cover.jpeg', + resize=WINDOW_RESOLUTION) + self.default_cover_shift = ( + (WINDOW_RESOLUTION[0] - self.default_cover_surface.get_width())/2, + (WINDOW_RESOLUTION[1] - self.default_cover_surface.get_height())/2, + ) + self.load_cover() + + def event_current_game_changed(self, new_game_info): + print('game changed, cover:', new_game_info.get('cover', None)) + self.current_game_info = new_game_info + self.load_cover() + + def load_cover(self): + self.cover_surface = None + path = self.current_game_info.get('cover', None) + if path is None: + self.cover_surface = self.default_cover_surface + self.cover_shift = self.default_cover_shift + return + path = config.get_data_d() / Path(path) + if path.exists(): + self.cover_surface = get_image_surface(path, + resize=WINDOW_RESOLUTION) + self.cover_shift = ( + (WINDOW_RESOLUTION[0] - self.cover_surface.get_width())/2, + (WINDOW_RESOLUTION[1] - self.cover_surface.get_height())/2, + ) + else: + self.cover_surface = self.default_cover_surface + self.cover_shift = self.default_cover_shift + + def render(self, dest_surface, pos): + if self.cover_surface is None: + return + pos = ( + pos[0] + self.cover_shift[0], + pos[1] + self.cover_shift[1], + ) + dest_surface.blit(self.cover_surface, pos) + + +class ProgressBarRenderer: + + def __init__(self): + self.progress = .0 + self.back_color = 0xEEEEEEF0 + self.front_color = 0x0000FF00 + + def set_progress(self, progress): + 'progress is between .0 and 1.0' + self.progress = progress + + def render(self, dest_surface, pos): + size = (100, 10) + background_rect = pygame.Rect(pos[0], pos[1], size[0], size[1]) + inside_rect = pygame.Rect(pos[0]+2, pos[1]+2, int(96*self.progress), 6) + + dest_surface.lock() + pygame.draw.rect(dest_surface, self.back_color, background_rect, + border_radius=5) + pygame.draw.rect(dest_surface, self.front_color, inside_rect, + border_radius=5) + dest_surface.unlock() + + +class DoubleProgressBarRenderer: + def __init__(self): + self.progress_bar_global = ProgressBarRenderer() + self.progress_bar_step = ProgressBarRenderer() + self.title = "" + self.title_surface = None + self.font_filename = pygame.font.match_font('bitstreamverasans') + print('GameRenderer font:', self.font_filename) + self.font = pygame.font.Font(self.font_filename, 10) + self.font_color = 0xFFFFFFFF + self.set_title("") + + def set_progress(self, global_progress, step_progress): + self.progress_bar_global.set_progress(global_progress) + self.progress_bar_step.set_progress(step_progress) + + def set_title(self, title): + self.title = title or "" + self.title_surface = get_text_surface(title, self.font, self.font_color) + + def render(self, dest_surface, pos): + x = pos[0] + y = pos[1] + rect = pygame.Rect(x, y, 104, 10+10+10+2+2+2+2) + x += 2 + y += 2 + pygame.draw.rect(dest_surface, 0x00000000, rect, border_radius=3) + if self.title_surface: + dest_surface.blit(self.title_surface, (x, y)) + #y += self.title_surface.get_height() + 2 + y += 10 + 2 + self.progress_bar_global.render(dest_surface, (x, y)) + self.progress_bar_step.render(dest_surface, (x, y+12)) + + + +class GuiAppSub1(GuiApp): + + def __init__(self): + super().__init__() + self.input_timer = 0 + self.input_timeout = 1000//10 + self.state = AppState() + self.base_window_fill_color = (0, 0, 0) + self.user_move_vector = (0, 0) + self.game_db = GameDB() + #self.menu_font_cache = ({}, []) + #self.menu_font_cache_max = 40 + #self.last_movement_update_time = self.clock. + self.joysticks = {} + self.joysticks_axis_threshold = 0 + self.profile_data = ProfileData() + self.profile_renderer = ProfileRenderer(self.profile_data) + self.game_data = GameData() + self.status_db = StatusDB() + self.game_renderer = GameRenderer(self.game_data, self.status_db) + self.game_cover_renderer = GameCoverRenderer(self.game_data) + self.action = None + self.runner = None + self.progress_bar = DoubleProgressBarRenderer() + self.selector = selectors.DefaultSelector() + self.delay_change = None + + def process_input(self, last_frame_time): + + movement_up_keys = ( + pygame.K_UP, + pygame.K_k, + ) + movement_down_keys = ( + pygame.K_DOWN, + pygame.K_j, + ) + axis_change = 0 + for event in pygame.event.get(): + go_super = 1 + x, y = self.user_move_vector + if event.type == pygame.KEYDOWN: + if event.key in (*movement_up_keys, *movement_down_keys): + y = 1 if event.key in movement_down_keys else -1 + go_super = 0 + elif event.key in (pygame.K_LEFT, pygame.K_RIGHT): + x = -1 if event.key == pygame.K_LEFT else 1 + go_super = 0 + elif event.type == pygame.KEYUP: + x, y = self.user_move_vector + if event.key in (*movement_up_keys, *movement_down_keys): + y = 0 + go_super = 0 + elif event.key in (pygame.K_LEFT, pygame.K_RIGHT): + x = 0 + go_super = 0 + elif event.key == pygame.K_RETURN: + self.action = 'run' + elif event.key == pygame.K_i: + self.action = 'install' + elif event.key == pygame.K_d: + self.action = 'remove' + elif event.type in (pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP): + go_super = 0 + factor = 1 if pygame.JOYBUTTONDOWN else 0 + joystick = self.joysticks[event.instance_id] + print('gamepad event', event) + #if event.button == 0: + #y += 1 * factor + if event.type == pygame.JOYBUTTONDOWN: + joystick.rumble(0.3, 0.8, 40) + if event.button == 1: + raise QuitEvent() + elif event.button == 2: + self.action = 'run' + #if event.button == 1: + #y -= 1 * factor + elif event.type == pygame.JOYAXISMOTION: + go_super = 0 + if event.axis == 1: # left stick up-down axis + new_axis = self.joysticks_axis_threshold + #print('motion event', event) + if event.value > 0.70: + new_axis = 1 # down activated + elif event.value < 0.5 and event.value > -0.5: + new_axis = 0 # down and up deactivated + elif event.value < -0.70: + new_axis = -1 # up activated + if new_axis != self.joysticks_axis_threshold: + axis_change = 1 + self.joysticks_axis_threshold = new_axis + print('threshold', self.joysticks_axis_threshold) + elif event.type == pygame.JOYDEVICEADDED: + # This event will be generated when the program starts for every + # joystick, filling up the list without needing to create them manually. + joy = pygame.joystick.Joystick(event.device_index) + self.joysticks[joy.get_instance_id()] = joy + print(f"Gamepad {joy.get_instance_id()} connected") + elif event.type == pygame.JOYDEVICEREMOVED: + del self.joysticks[event.instance_id] + print(f"Gamepad {event.instance_id} disconnected") + + if go_super == 0: + if axis_change: + self.user_move_vector = (x, self.joysticks_axis_threshold) + else: + self.user_move_vector = (x, y) + #print('new move vector:', self.user_move_vector) + else: + super().process_input(last_frame_time, event) + + def update(self, last_frame_time): + #self.input_timer += last_frame_time + #if self.input_timer < self.input_timeout: + # return + #self.input_timer = 0 + + if self.user_move_vector[1] < 0: + self.game_data.change_game_decrease() + elif self.user_move_vector[1] > 0: + self.game_data.change_game_increase() + + if self.user_move_vector[0] < 0: + self.profile_data.change_profile_decrease() + elif self.user_move_vector[0] > 0: + self.profile_data.change_profile_increase() + + if action := self.action: + self.action = None + profile_id = self.profile_renderer.profiledata.get_current_profile() + game_id = self.game_data.get_current_id() + game_info = self.game_data.get_current_gameinfo() + print('current id', game_id) + print('profile id', profile_id) + if not self.runner: + # action not already running, else skip action + if action == 'run': + if not self.status_db.is_installed(game_info): + print('Game', game_id, 'is not installed, aborting.', file=sys.stderr) + else: + command = self.game_db.get_game_command(profile_id, game_id) + subprocess.run(command) + elif action == 'install': + #subprocess.run([ + # 'gamechest', + # 'install', + # game_id]) + remote_basedir = config.get_remote_basedir() + source = f'{remote_basedir}/{game_info["package_name"]}' + dest = config.get_games_install_basedir() / game_info['id'] + dest.mkdir(parents=True, exist_ok=True) + self.runner = Install(source, dest) + self.selector.register(self.runner.get_read_fd(), + selectors.EVENT_READ) + self.delay_change = partial(self.status_db.set_installed, game_info) + self.progress_bar.set_title("installing...") + elif action == 'remove': + remote_basedir = config.get_remote_basedir() + path = ( + config.get_games_install_basedir() + / game_id + ) + #subprocess.run([ + # 'gamechest', + # 'remove', + # game_id]) + self.runner = Remove(path) + self.selector.register(self.runner.get_read_fd(), + selectors.EVENT_READ) + self.delay_change = partial(self.status_db.set_uninstalled, game_info) + self.progress_bar.set_title("removing...") + + if self.runner: + if self.runner.poll() is None: + if self.selector.select(0): + progress = self.runner.progress_read() + self.progress_bar.set_progress(progress.step / progress.steps_count, progress.percent / 100) + else: + self.progress_bar.set_progress(1, 1) + self.progress_bar.set_title("done") + self.runner = None + if self.delay_change is not None: + self.delay_change() + self.delay_change = None + + + self.user_move_vector = (0, 0) + + def render(self, last_frame_time): + self.window.fill(self.base_window_fill_color) + + self.game_cover_renderer.render(self.window, (0, 0)) + + # Initial y + y = 50 + x = 50 + 64 + 5 + self.game_renderer.render(self.window, (x, y)) + + + fps_surface = get_text_surface(f'{self.clock.get_fps():02.0f}', + self.menu_font, self.menu_color) + fps_pos = (self.window.get_width() - fps_surface.get_width(), 0) + self.window.blit(fps_surface, fps_pos) + + self.profile_renderer.render(self.window, (700, 50)) + self.progress_bar.render(self.window, (500, 50)) + + pygame.display.update() + + +app = GuiAppSub1() +app.loop() |