Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
db3106b
Add readme
hugglesfox Jul 31, 2024
bc9b80e
Add the example game foobar
hugglesfox Jul 31, 2024
f5c863d
Change to using a toml based format
hugglesfox Aug 20, 2024
96f62ff
merge branch and tag
hugglesfox Sep 11, 2024
26f80c6
Add clone and build logic
hugglesfox Sep 11, 2024
263fd85
WIP add asteroids
hugglesfox Sep 17, 2024
9667813
Finish initial implementation with asteroids
hugglesfox Sep 18, 2024
7020d6b
Add logging
hugglesfox Sep 18, 2024
f58770a
Add dxballgame
hugglesfox Sep 18, 2024
4658013
Add ping pong
hugglesfox Sep 18, 2024
f7cd53b
Add github actions
hugglesfox Sep 18, 2024
dfc85d1
Fix run statements
hugglesfox Sep 18, 2024
4a2ee46
Fix skm path
hugglesfox Sep 18, 2024
822134c
Configuring CI/CD scripts is annoying
hugglesfox Sep 18, 2024
0d9daa2
Update apt before installing
hugglesfox Sep 18, 2024
224f87c
Fix flipper path
hugglesfox Sep 18, 2024
bfb141c
Add apt repo update
hugglesfox Sep 18, 2024
8fca475
Fix arch expression
hugglesfox Sep 18, 2024
f0545a6
Change flipper clone path
hugglesfox Sep 18, 2024
e047fd6
Add libjsoncpp-dev dependency
hugglesfox Sep 18, 2024
bb3e03c
Remove dxballgame
hugglesfox Sep 18, 2024
7443392
Fix C++ library link name
hugglesfox Sep 18, 2024
352576a
Remove json dependency
hugglesfox Sep 18, 2024
b4fb56d
switch to g++-arm-linux-gnueabihf
hugglesfox Sep 18, 2024
db381b2
Add archive upload
hugglesfox Sep 18, 2024
63efed7
Remove arm build for now
hugglesfox Sep 18, 2024
226f636
Add return code handling for clone, build and archive
hugglesfox Sep 18, 2024
c9f636e
Fix archive path
hugglesfox Sep 18, 2024
18b242d
Fix docstring
hugglesfox Sep 18, 2024
ebb8ef0
Clean up logging text
hugglesfox Sep 18, 2024
774f0ac
Make path a positional argument
hugglesfox Sep 27, 2024
aa9ee95
Add flipper usage instructions
hugglesfox Sep 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
@@ -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
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions asteroids.toml
Original file line number Diff line number Diff line change
@@ -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
243 changes: 243 additions & 0 deletions flipper.py
Original file line number Diff line number Diff line change
@@ -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()
Loading