From: Niki Roo Date: Sun, 23 Mar 2025 17:29:51 +0000 (+0100) Subject: inital commit X-Git-Url: http://git.nikiroo.be/?a=commitdiff_plain;h=2545f82bc004465e07c1552a7f91dcc7b6b2c5b1;p=gamiki.git inital commit --- 2545f82bc004465e07c1552a7f91dcc7b6b2c5b1 diff --git a/Gamiki/Builder.py b/Gamiki/Builder.py new file mode 100644 index 0000000..9644bb6 --- /dev/null +++ b/Gamiki/Builder.py @@ -0,0 +1,99 @@ +from os import listdir +from os.path import isfile, isdir, join, expanduser, realpath, exists + +from . import Library, Game +from .Support import Support, SupportDos, SupportWin31, SupportGog + +class Builder: + """Manage all the resources needed to run games.""" + def __init__(self): + self.libraries : list[Library] = [] + self.games : list[Game] = [] + + supports = [] + supports.append(SupportGog()) + supports.append(SupportWin31()) # prio over DOS + supports.append(SupportDos()) + + targets = [] + if (exists(expanduser("~/.games.cfg"))): + with open(file, encoding="utf-8") as data: + ln = data.strip() + if (not ln.startswith("#")): + targets.append(ln) + else: + targets.append(expanduser("~/Games")) + + for target in targets: + self.load_libs(supports, target) + + for lib in self.libraries: + for game in lib: + self.games.append(game) + + def load_libs(self, supports: list[Support], dir: str): + for path in listdir(dir): + fullpath = join(dir, path) + if (not isdir(realpath(fullpath))): + continue + + try: + self.libraries.append(Library(fullpath, supports)) + except FileNotFoundError: + pass + + def list(self): + i = 1 + for game in self.games: + print("{0:6d}".format(i), "", game.name) + i += 1 + + def list_tag(self): + i = 1 + for game in self.games: + print("{0:6d}".format(i), "", game.src + ":", game.name) + i += 1 + + def list_tags(self): + i = 1 + for game in self.games: + print( + "{0:6d}".format(i), "", game.name, + "(" + ";".join(game.tags) +")") + i += 1 + + def find(self, key: str) -> Game: + game: Game = None + + try: + i = int(key) + game = self.games[i-1] + except ValueError: + pass + except IndexError as orig: + raise ValueError("Game index does not exist: " + key) + + # TODO: allow regex + + if game == None: + for g in self.games: + if (g.code == key): + if (game != None): + raise ValueError( + "Multiple codes match, please specify: " + key + ) + game = g + break + + if game == None: + for g in self.games: + if (g.name == key): + if (game != None): + raise ValueError( + "Multiple names match, please specify: " + key + ) + game = g + break + + return game; + diff --git a/Gamiki/Game.py b/Gamiki/Game.py new file mode 100644 index 0000000..babe18d --- /dev/null +++ b/Gamiki/Game.py @@ -0,0 +1,49 @@ +from os.path import basename as basename + +class Game(dict[str, str]): + """Game object generated from a directory.""" + def __init__(self, library, dir: str): + # TODO: doc: dir = full path + self.dir = dir.removesuffix("/") + self.name = basename(self.dir) + self.code = basename(self.dir) + self.src = "" + self.tags = [] + self.desc = "" + self.support = None + self.library = library + + self._read_info(self.dir + "/gameinfo.txt") + + if ("Name" in self): + self.name = self["Name"] + if ("Code" in self): + self.code = self["Code"] + if ("Tags" in self): + self.tags = [ tag.strip() for tag in self["Tags"].split(";") ] + if (self.tags): + self.src = self.tags[0] + if ("Desc" in self): + self.desc = self["Desc"].replace("\\n", "\n").strip() + + def _read_info(self, file: str): + try: + with open(file, encoding="utf-8") as data: + for ln in data: + ln = ln.strip() + tab = ln.split("=", maxsplit=1) + if (len(ln) and ln[0] != '#' and len(tab) == 2): + self[tab[0]] = tab[1] + except FileNotFoundError: + pass + + + def set_support(self, support): + self.support = support + + def start(self, params: list[str] = []): + if (self.support == None): + raise RuntimeError("Unsupported game was called: " + game.name) + + self.support.start(self, params) + diff --git a/Gamiki/Library.py b/Gamiki/Library.py new file mode 100644 index 0000000..47b1228 --- /dev/null +++ b/Gamiki/Library.py @@ -0,0 +1,34 @@ +from os import listdir +from os.path import isdir, join, realpath, exists + +from .Support import Support +from .Game import Game + +class Library(list[Game]): + """Manage a library (a folder) of Games.""" + + def __init__(self, dir: str, supports: list[Support]): + self.name = '' # TODO + self.dir = dir + self.supports = supports + self.preferredSupport = None + # TODO: dir = fullpath + + config = join(self.dir, "games.cfg") + if (not exists(config)): + raise FileNotFoundError("Library is not configured (games.cfg)") + + # TODO: get name from config? + + for path in listdir(self.dir): + fullpath = join(self.dir, path) + if (not isdir(realpath(fullpath))): + continue + game = Game(self, fullpath) + for support in self.supports: + if (support.supports(game)): + game.set_support(support) + break; + if (game.support != None): + self.append(game) + diff --git a/Gamiki/Support/Support.py b/Gamiki/Support/Support.py new file mode 100644 index 0000000..f22ecf6 --- /dev/null +++ b/Gamiki/Support/Support.py @@ -0,0 +1,29 @@ +from subprocess import run +from shutil import which +#from typing import Any, Self + +from ..Game import Game + +class Support: + programs: dict[str, str] = None + + """Can detect and start games.""" + def __init__(self): + pass + + def supports(self, game: Game) -> bool: + return False + + def start(self, game: Game, params: list[str] = []): + if (not self.supports(game)): + raise RuntimeError("Unsupported game was called: " + game.name) + + def program(name: str) -> str: + if (Support.programs == None): + Support.programs = {} + + if (name not in Support.programs): + Support.programs[name] = which(name) + + return Support.programs[name] + diff --git a/Gamiki/Support/SupportDos.py b/Gamiki/Support/SupportDos.py new file mode 100644 index 0000000..e89954a --- /dev/null +++ b/Gamiki/Support/SupportDos.py @@ -0,0 +1,43 @@ +from os.path import exists, realpath +from subprocess import run +from shutil import which + +from . import Support +from .. import Game + +class SupportDos(Support): + """Supports DOS games via DosBox.""" + def __init__(self): + pass + + def supports(self, game: Game): + return (exists(game.dir + "/start.conf")) + + def start(self, game: Game, params: list[str] = []): + dir = realpath(game.dir) + link_dir = realpath(self.get_link_dir(game)) + + if (Support.program("app.sh") != None): + cmd = [ + Support.program("app.sh"), + "--wait", "dosbox", "--link", link_dir + ] + elif (Support.program("dosbox") != None): + cmd = [ Support.program("dosbox") ] + else: + raise RuntimeError("DosBox not found") + + if (exists(dir + "/base.conf")): + cmd.append("-conf") + cmd.append(dir + "/base.conf") + cmd.append("-conf") + cmd.append(dir + "/start.conf") + + print("Running", game.name) + rep = run(cmd, cwd=dir) + if (rep.returncode != 0): + print("\nError when running", game.name, ": RC", rep.returncode) + + def get_link_dir(self, game: Game) -> str: + return (game.dir) + diff --git a/Gamiki/Support/SupportGog.py b/Gamiki/Support/SupportGog.py new file mode 100644 index 0000000..cacf212 --- /dev/null +++ b/Gamiki/Support/SupportGog.py @@ -0,0 +1,61 @@ +from random import random +from math import floor +from os.path import exists, realpath +from os import environ +from pathlib import Path +from subprocess import run +from shutil import which + +from . import Support +from .. import Game + +class SupportGog(Support): + """Supports GoG games via the 'games' docker or natively.""" + def __init__(self): + pass + + def supports(self, game: Game): + return (exists(game.dir + "/gameinfo")) + + def start(self, game: Game, params: list[str] = []): + dir = realpath(game.dir) + + if (not exists(dir + "/start.sh")): + raise RuntimeError("GoG game without a start.sh script") + + env = environ.copy() + + ffile = None + if (Support.program("launch.sh") != None): + ffile = Path( + "/tmp/shared/.game-" + + "{0:6d}".format(floor(random() * 1000000)) + + ".sh" + ) + with open(ffile, 'w', encoding="utf-8") as data: + data.write("#!/bin/sh\n") + data.write("cd '" + dir + "'\n") + data.write("./start.sh\n") + ffile.chmod(0o777) + env["OPTS"] = "--entrypoint=" + ffile.as_posix() + cmd = [ + Support.program("launch.sh"), + "games", "--link", dir + ] + else: + cmd = [ dir + "/start.sh" ] + + if (exists(dir + "/base.conf")): + cmd.append("-conf") + cmd.append(dir + "/base.conf") + cmd.append("-conf") + cmd.append(dir + "/start.conf") + + # GoG launcher already does this: + # print("Running", game.name) + rep = run(cmd, cwd=dir, env=env) + if (ffile != ""): + ffile.unlink() + if (rep.returncode != 0): + print("\nError when running", game.name, ": RC", rep.returncode) + diff --git a/Gamiki/Support/SupportWin31.py b/Gamiki/Support/SupportWin31.py new file mode 100644 index 0000000..9a85c05 --- /dev/null +++ b/Gamiki/Support/SupportWin31.py @@ -0,0 +1,19 @@ +from os.path import exists, realpath +from subprocess import run +from shutil import which + +from . import Support, SupportDos +from .. import Game + +class SupportWin31(SupportDos): + """Supports Windows 3.1 games via DosBox.""" + + # TODO: must be tried befoer SupportDos + + def supports(self, game: Game): + return (exists(game.dir + "/start.conf") + and exists(game.dir + "/C/WINDOWS")) + + def get_link_dir(self, game: Game) -> str: + return game.library.dir + diff --git a/Gamiki/Support/__init__.py b/Gamiki/Support/__init__.py new file mode 100644 index 0000000..7e87dbd --- /dev/null +++ b/Gamiki/Support/__init__.py @@ -0,0 +1,4 @@ +from .Support import Support +from .SupportDos import SupportDos +from .SupportWin31 import SupportWin31 +from .SupportGog import SupportGog diff --git a/Gamiki/__init__.py b/Gamiki/__init__.py new file mode 100644 index 0000000..db34ecc --- /dev/null +++ b/Gamiki/__init__.py @@ -0,0 +1,4 @@ +from .Library import Library +from .Game import Game +from .Builder import Builder +from .Support import Support diff --git a/Main.py b/Main.py new file mode 100755 index 0000000..e4472ef --- /dev/null +++ b/Main.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +import argparse +from os.path import basename +from sys import stderr + +from Gamiki import Builder, Library + +parser = argparse.ArgumentParser( + description="Game launcher and simple management system." +) + +# Global actions +global_actions = parser.add_argument_group('global actions') +global_actions.add_argument("--list", + help="list all known games by temporary number and name", + action="store_true" +) +global_actions.add_argument("--list-tag", + help="list all known games and their source tag", + action="store_true" +) +global_actions.add_argument("--list-tags", + help="list all known games and their tags", + action="store_true" +) + +# Select a game +game_selection = parser.add_argument_group('game selection') +game_selection.add_argument("GAME", + help="Start a game (code, name or index -- may use RegEx)", + nargs='?', default="" +) + +# Start a game or options on a game +game_actions = parser.add_argument_group('actions on a GAME') +game_actions.add_argument("--info", + help="get information about a specific game", + metavar="GAME" +) +game_actions.add_argument("--info-name", + help="get the name of the game", + metavar="GAME" +) +game_actions.add_argument("--info-code", + help="get the code (short name) of the game", + metavar="GAME" +) +game_actions.add_argument("--info-desc", + help="get the description of the game", + metavar="GAME" +) +game_actions.add_argument("--info-tags", + help="get all the tags of the game, one per line", + metavar="GAME" +) +game_actions.add_argument("--info-icon", + help="get the icon of the game", + metavar="GAME" +) + +args = parser.parse_args() + +builder = Builder() +if (args.list): + builder.list() + exit(0) +elif (args.list_tag): + builder.list_tag() + exit(0) +elif (args.list_tags): + builder.list_tags() + exit(0) + +g = "" +if (args.info): + g = args.info +elif (args.info_name): + g = args.info_name +elif (args.info_code): + g = args.info_code +elif (args.info_desc): + g = args.info_desc +elif (args.info_tags): + g = args.info_tags +elif (args.info_icon): + g = args.info_icon +else: + g = args.GAME + +if (g == ""): + parser.print_usage() + print( + basename(__file__) + ": error:", + "global action or GAME required", + file=stderr + ) + exit(1) + +try: + game = builder.find(g) +except ValueError as err: + print(err, file=stderr) + exit(3) + +if (game == None): + print("Game not found:", g, file=stderr) + exit(2) + +if (args.info): + for (k, v) in game.items(): + print(k + " =", v) +elif (args.info_name): + print(game.name) +elif (args.info_code): + print(game.code) +elif (args.info_desc): + print(game.desc) +elif (args.info_tags): + for tag in game.tags: + print(tag) +elif (args.info_icon): + if ("Icon" in game): + print(game["Icon"]) +else: + game.start() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29