import importlib.util import argparse import logging import tomllib import pygame import attr import sys import os # TODO: Auto install game requirements # TODO: If a game is specified, only install those requirements logging.basicConfig(level=logging.DEBUG) log = logging.getLogger('PainJamLauncher') # Store the current working directory # This is used to revert the cd change originalCWD = os.getcwd() # Parse command line arguments parser = argparse.ArgumentParser(prog='Pain Jam launcher') parser.add_argument('-sw', '--width', type=int, default=1280) parser.add_argument('-sh', '--height', type=int, default=720) parser.add_argument('-fps', '--fpsCap', type=int, default=60) parser.add_argument('-f', '--fullscreen', action='store_true') parser.add_argument('gameID', nargs='?', type=str, default=None) args = parser.parse_args() @attr.s class Game: id: str = attr.ib() name: str = attr.ib() description: str = attr.ib() path: str = attr.ib() games = {} # Scan for games for path in os.listdir('../'): gamePath = os.path.join('../', path) # Skip anything that isn't a directory if not os.path.isdir(gamePath): continue # Get the games toml path tomlPath = os.path.join(gamePath, 'game.toml') # Skip all directories that don't have a game.toml file if not os.path.isfile(tomlPath): continue with open(tomlPath, 'r') as f: toml = tomllib.loads(f.read()) gameID = toml['id'] game = Game(gameID, toml['name'], toml['description'], gamePath) games.update({gameID: game}) log.debug(f'Loaded game {gameID}') run = True pygame.init() # I'm sorry, I prefer object-oriented programming. So sue me. class Launcher: def __init__(self): # The primary display. self.screen = pygame.display.set_mode((args.width, args.height)) # The surface that the games render to. self.gameSurface = pygame.Surface(self.screen.size) # The current Game object. None means no game is running self.currentGame = None self.clock = pygame.time.Clock() # Is the launcher running (including if a game is running). self.running = True pygame.display.set_caption('Pain Jam Launcher') def loadGame(self, game): # Set the current working directory # to the base path of the game. os.chdir(game.path) # Load the games game.py file. spec = importlib.util.spec_from_file_location(game.id, os.path.join(game.path, 'game.py')) foo = importlib.util.module_from_spec(spec) spec.loader.exec_module(foo) # Initialise the games Game class, then set the # current game to that. self.currentGame = foo.Game(self.gameSurface) # Set the window title to the games name pygame.display.set_caption(game.name) def update(self): for event in pygame.event.get(): if self.gameRunning: self.currentGame.onEvent(event) # CTRL+ESC if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE and event.mod == 64): # Close the game. # TODO: Fade out game? self.currentGame.close() self.currentGame = None os.chdir(originalCWD) pygame.display.set_caption('Pain Jam Launcher') else: if event.type == pygame.QUIT: # Exit the launcher. self.running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_1: self.loadGame(games['russianRoulette']) if self.gameRunning: self.currentGame.update() self.screen.blit(self.currentGame.surf, (0, 0)) else: self.screen.fill((0, 0, 0)) pygame.display.update() self.clock.tick(args.fpsCap) @property def gameRunning(self): return self.currentGame != None launcher = Launcher() if args.gameID != None: launcher.loadGame(games[args.gameID]) while launcher.running: launcher.update()