diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 0000000..14d4220 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,34 @@ +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Install splashkit + run: | + sudo apt update + curl -s https://raw.githubusercontent.com/splashkit/skm/master/install-scripts/skm-install.sh | bash + ~/.splashkit/skm linux install + ~/.splashkit/skm global install + + - name: Install dependencies + run: sudo apt-get install -y dotnet-host g++-arm-linux-gnueabihf + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Clone flipper repo + uses: actions/checkout@v4 + + - name: Run flipper + run: | + mkdir -p build + cd build + python ../flipper.py --path .. + + - name: Upload game archive + uses: actions/upload-artifact@v4 + with: + path: splashkit-games-*.tar.gz diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e9d2a1 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Flipper + +The splashkit arcade machine games repository. + +## Adding a game + +Adding a game is as easy as creating a new [toml](https://toml.io) file which +contains some details about your game. An example of a basic game is below. + +```toml +[meta] +name = "FooBar" +description = "Foobar is an amazing strategy game based on fizzbuzz" +language = "cpp" + +[git] +repo = "https://github.com/thoth-tech/foobar.git" +``` + +The toml file is broken down into 2 parts meta and git. + +### meta + +The meta section contains metadata about the game. It's fields are as follows. + +- `name` is the name of the game +- `description` is a short description of the game +- `language` the language which the game is programmed in. Possible values are + `cpp` for C++ or `cs` for C#. Python and pascal support coming soon. + +### git + +The git section contains information about the github repository containing the +game's source code. + +- `repo` is the url of the game's github repository + +## Full reference + +The above example is only the minimum required to package a game. Below shows +all the possible configuration values, however everything which is not stated +above is considered optional. + +```toml +[meta] +name = "" +description = "" +language = "" + +[git] +repo = "" +branch = "" # a tag can also be specified +directory = "" + +# See https://github.com/thoth-tech/ArcadeMenu/blob/master/GAMELISTS.md +[emulationstation] +image = "" +thumbnail = "" +rating = 0.0 +releasedate = 1979-05-27T07:32:00 +developer = "" +publisher = "" +genre = "" +players = 0 +``` + +## FAQ + +> Why toml? + +A toml parser included in Python's standard library, where as a yaml parser +isn't, and one of my requirements was that the build script should have no +dependencies other then python and it's standard library. + +## Usage + +Flipper is designed to be ran in a clean 'build' directory (similar to cmake) +and therefore takes the path to the repository as a positional argument. This +would also allow one to seperate the games repository from the flipper source +if flipper is being used on more repositories then just this one. + +A typical usage would look like the following + +``` +$ mkdir build +$ cd build +$ ../flipper.py .. +``` + +## Licensing + +By using this project to package your game, you are agreeing that a copy of the +source code and a binary build of the game will be freely redistributed. + +Please consider licensing your game using an open source license such as GPL, +MIT, Apache or BSD and including a copy of said licence in your game +repository. This way we can freely use your game and avoid copyright law +issues. + +This project and all the game packaging files within (not the contents of the +games themselves) are licensed under the MIT license unless otherwise stated. +See LICENSE.txt for more information. diff --git a/asteroids.toml b/asteroids.toml new file mode 100644 index 0000000..b96f7d9 --- /dev/null +++ b/asteroids.toml @@ -0,0 +1,16 @@ +[meta] +name = "Asteroids" +language = "cs" +description = """ +Asteroids is a top-down single player or co-op shooter that has \ +you, the player, flying around and surviving an onslaught of rocks, and if you \ +survive long enough, even bosses!""" + +[git] +repo = "https://github.com/thoth-tech/Asteroids.git" + +[emulationstation] +image = "doco/images/img3.png" +genre = "Multidirectional shooter" +developer = "Thoth Tech" +rating = 4.5 diff --git a/flipper.py b/flipper.py new file mode 100755 index 0000000..0b84fce --- /dev/null +++ b/flipper.py @@ -0,0 +1,243 @@ +#!/usr/bin/python3 + +import logging +import argparse +import os +import subprocess +import textwrap +import tomllib +import xml.etree.ElementTree as xml + +from datetime import datetime + +ARCHIVE_PATH = f"../splashkit-games-{datetime.now():%Y%m%d-%H%M%S}.tar.gz" + +HOME_PATH = "~" +GAMES_PATH = "Games" # relative to HOME_PATH +SYSTEM_PATH = os.path.join(GAMES_PATH, "LaunchScripts") + +CPP_LINK_SPLASHKIT = "-lSplashKit" + +# The verbose flag sets this to None so that stdout will be shown on stdout +STDOUT = subprocess.DEVNULL + +# The verbose flag sets this to logging.DEBUG +LOG_LEVEL = logging.INFO + +args = None +log = logging.getLogger("flipper") + + +class Game: + def __init__(self, config): + config = tomllib.load(fp) + + self.meta = config["meta"] + self.git = config["git"] + self.es = config.get("emulationstation", {}) + + self.log = logging.getLogger(self.meta["name"]) + self.cloned = False + + def clone(self): + """Clone the game's source code""" + clone_path = os.path.join(GAMES_PATH, self.meta["name"]) + + if os.path.isdir(clone_path): + self.log.warning("Game directory already exists, skipping cloning") + self.cloned = True + return + + cmd = ["git", "clone"] + cmd.append("--depth=1") + + if "branch" in self.git.keys(): + cmd += ["--branch", self.git["branch"]] + + cmd += [self.git["repo"], clone_path] + + self.log.info(f"Cloning...") + self.log.debug(" ".join(cmd)) + + clone = subprocess.run(cmd, stdout=STDOUT) + + if clone.returncode != 0: + self.log.critical("Game failed to clone") + exit(clone.returncode) + + self.cloned = True + + def build(self): + """Build the game + + For C++ and C# this creates a bin directory inside the cloned source + containing the final executable + """ + if not self.cloned: + self.clone() + + build_path = os.path.join( + GAMES_PATH, self.meta["name"], self.git.get("directory", "") + ) + + # Create a path to put the compiled binary + os.makedirs(os.path.join(build_path, "bin"), exist_ok=True) + + cmd = [] + + if self.meta["language"] == "cs": + cmd += ["dotnet", "publish"] + cmd += ["--configuration", "release"] + + if args.cs_runtime is not None: + cmd += ["--runtime", args.cs_runtime] + + cmd += ["-o", "bin"] + elif self.meta["language"] == "cpp": + cmd.append(args.cpp_prefix + args.cpp) + cmd += [ + source for source in os.listdir(build_path) if source.endswith(".cpp") + ] + cmd.append(CPP_LINK_SPLASHKIT) + cmd += ["-o", "bin/" + self.meta["name"]] + else: + self.log.critical( + f"Unable to build, unknown language {self.meta['language']}" + ) + exit(1) + + self.log.info(f"Building {self.meta['name']}...") + self.log.debug(" ".join(cmd)) + + build = subprocess.run(cmd, cwd=build_path, stdout=STDOUT) + + if build.returncode != 0: + self.log.critical("Game failed to build") + exit(build.returncode) + + def generate_run_script(self): + """Generate a run script for the game""" + script_path = os.path.join(SYSTEM_PATH, self.meta["name"] + ".sh") + self.log.info(f"Creating run script at {script_path}") + + script = "" + + if self.meta["language"] == "cs" or self.meta["language"] == "cpp": + script = f"""\ + #!/bin/sh + {os.path.join(HOME_PATH, GAMES_PATH, self.meta['name'], self.git.get('directory', ''), 'bin', self.meta['name'])} + """ + else: + self.log.error( + f"Unable to create run script, unknown language {self.meta['language']}" + ) + + os.makedirs(SYSTEM_PATH, exist_ok=True) + + script = textwrap.dedent(script) + self.log.debug(script) + + with open(script_path, "w+") as fp: + fp.write(script) + os.chmod(fp.name, 0o755) + + def es_config(self, gamelist): + """Append the game's emulation station configuration + + This assumes that the gameList root tag already exist in the given + element tree + + See https://github.com/thoth-tech/ArcadeMenu/blob/master/GAMELISTS.md + for the file format + """ + game = xml.SubElement(gamelist, "game") + + path = xml.SubElement(game, "path") + path.text = os.path.join(HOME_PATH, SYSTEM_PATH, self.meta["name"] + ".sh") + + name = xml.SubElement(game, "name") + name.text = self.meta["name"] + + self.log.info(f"Generating gamelist configuration for {self.meta['name']}") + + if (description := self.meta.get("description")) is not None: + self.log.debug("Adding description tag") + desc = xml.SubElement(game, "desc") + desc.text = description + else: + self.log.warning("Game doesn't have a description") + + if (image := self.es.get("image")) is not None: + self.log.debug("Adding image tag") + desc = xml.SubElement(game, "image") + desc.text = os.path.join(self.git.get("directory", ""), image) + else: + self.log.warning("Game doesn't have title image") + + for tag, val in self.es.items(): + if tag == "image": + continue + + self.log.debug(f"Adding {tag} tag") + element = xml.SubElement(game, tag) + element.text = str(val) + + +def create_archive(): + log.info(f"Creating {ARCHIVE_PATH}") + cmd = ["tar", "czvf", ARCHIVE_PATH, "."] + log.debug(" ".join(cmd)) + + tar = subprocess.run(cmd, stdout=STDOUT) + + if tar.returncode != 0: + log.critical("Archive creation failed, run with --verbose for more information") + exit(tar.returncode) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="splashkit arcade package manager") + + parser.add_argument( + "--cs-runtime", help="dotnet runtime architecture", default=None + ) + parser.add_argument("--cpp-prefix", help="cpp compiler prefix", default="") + parser.add_argument("--cpp", help="cpp compiler path", default="g++") + parser.add_argument( + "--verbose", help="increase output verbosity", action="store_true" + ) + parser.add_argument("repo", help="flipper repository path") + + args = parser.parse_args() + + if args.verbose: + LOG_LEVEL = logging.DEBUG + STDOUT = None + + logging.basicConfig(level=LOG_LEVEL) + + games = [] + + for file in os.listdir(args.repo): + if not file.endswith(".toml"): + continue + + with open(os.repo.join(args.repo, file), "rb") as fp: + games.append(Game(fp)) + + for game in games: + game.build() + + for game in games: + game.generate_run_script() + + gamelist = xml.Element("gameList") + for game in games: + game.es_config(gamelist) + + with open(os.repo.join(SYSTEM_PATH, "gamelist.xml"), "wb+") as fp: + tree = xml.ElementTree(gamelist) + xml.indent(tree) + tree.write(fp) + + create_archive() diff --git a/foobar.toml.example b/foobar.toml.example new file mode 100644 index 0000000..9cbe126 --- /dev/null +++ b/foobar.toml.example @@ -0,0 +1,7 @@ +[meta] +name = "FooBar" +description = "Foobar is an amazing strategy game based on fizzbuzz" +language = "cpp" + +[git] +repo = "https://github.com/thoth-tech/foobar.git" diff --git a/full.toml.example b/full.toml.example new file mode 100644 index 0000000..4531d1d --- /dev/null +++ b/full.toml.example @@ -0,0 +1,21 @@ +[meta] +name = "" +description = "" +language = "" + +[git] +repo = "" +branch = "" +directory = "" + +# See https://github.com/thoth-tech/ArcadeMenu/blob/master/GAMELISTS.md +[emulationstation] +image = "" +thumbnail = "" +rating = 0.0 +releasedate = 1979-05-27T07:32:00 +developer = "" +publisher = "" +genre = "" +players = 0 + diff --git a/pingpong.toml b/pingpong.toml new file mode 100644 index 0000000..c3af9fc --- /dev/null +++ b/pingpong.toml @@ -0,0 +1,13 @@ +[meta] +name = "Pingpong" +language = "cpp" +description = "The classic ping pong game" + +[git] +repo = "https://github.com/thoth-tech/arcade-games.git" +directory = "games/Pingpong" + +[emulationstation] +image = "images/Pingpong-config-image.png" +genre = "Sports" +developer = "Thoth Tech"