--- /dev/null
+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;
+
--- /dev/null
+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)
+
--- /dev/null
+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)
+
--- /dev/null
+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]
+
--- /dev/null
+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)
+
--- /dev/null
+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)
+
--- /dev/null
+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
+
--- /dev/null
+from .Support import Support
+from .SupportDos import SupportDos
+from .SupportWin31 import SupportWin31
+from .SupportGog import SupportGog
--- /dev/null
+from .Library import Library
+from .Game import Game
+from .Builder import Builder
+from .Support import Support
--- /dev/null
+#!/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()
+