Packaging Ports for PortMaster¶
To release a port on PortMaster there are some guidelines that need to be followed.
Port structure¶
Ports live in the ports top level directory, each in its own sub-directory
named after the port. Every port needs a port.json, a screenshot.{jpg,png},
a README.md, a gameinfo.xml, a launch script and a port directory. A
cover.{jpg,png} is optional.
The launch script can have capital letters and spaces, like Port Name.sh, and
must end in .sh. The port directory must match the name of the directory
containing it. Some legacy ports break these rules, but new ports are not
accepted unless they follow the current convention.
Script names and port directories must be unique across the whole project. Checks run on submission to make sure of it.
portname/
├── port.json
├── README.md
├── screenshot.jpg
├── gameinfo.xml
├── cover.jpg (optional)
├── Port Name.sh
└── portname/
├── licenses/
│ └── LICENSE files
└── <port files here>
The portname itself has its own rules:
- It must start with a lowercase letter (a-z) or a number (0-9).
- After that you can use lowercase letters (a-z), numbers (0-9), periods (.) or underscores (_).
- There is no length limit, but keep it short.
- It must not clash with any existing port.
port.json¶
This is what PortMaster itself reads, and it holds all the pertinent info for the port. The JSON Generator will build one for you.
Make sure to select the correct architecture. If the game uses a runtime such as Godot, Mono or Java, no arch needs to be entered.
Example, from 2048:
{
"version": 2,
"name": "2048.zip",
"items": [
"2048.sh",
"2048"
],
"items_opt": null,
"attr": {
"title": "2048",
"desc": "The 2048 puzzle game",
"inst": "Ready to run.",
"genres": [
"puzzle"
],
"porter": [
"Christian_Haitian"
],
"image": {},
"rtr": true,
"runtime": null,
"reqs": [],
"arch": [
"aarch64",
"armhf"
]
}
}
README.md¶
This provides the additional info shown for the port on the wiki. The README Generator will build one for you.
Always include a thank you to the developer or creator. Without these people we would not be here.
Example:
## Notes
Thanks to the [Alien Blaster Team](https://www.schwardtnet.de/alienblaster/) for
creating this game and making it available for free!
## Controls
| Button | Action |
|--|--|
|A| Special Weapon|
|B| Main Weapon|
|X| Swap Weapon|
|Y| Swap Special Weapon |
|R1| Key "1" |
## Compile
```
wget http://www.schwardtnet.de/alienblaster/archives/alienblaster-1.1.0.tgz
cd alienblaster-1.1.0
make
```
screenshot.png¶
Used in the PortMaster GUI and on the wiki, so it needs to show gameplay or the
port's main function. It has to be exactly 640x480, as either .jpg or .png.
The Screenshot Resizer resizes an image of
any size to a 640x480 screenshot.png.
gameinfo.xml and cover.png¶
PortMaster installs metadata, including a cover, into EmulationStation when a
port is installed. That comes from a gameinfo.xml and a cover file.
The cover should show gameplay in addition to other media such as box art or a logo. If no cover is supplied, PortMaster falls back to the screenshot.
The Gameinfo Generator creates and edits
gameinfo.xml, and the Cover Generator builds
a cover.png from your artwork.
A filled out gameinfo.xml looks like this:
<?xml version="1.0" encoding="utf-8"?>
<gameList>
<game>
<path>./Angband.sh</path>
<name>Angband</name>
<desc>Angband is a free, single-player dungeon exploration game.
You play an adventurer seeking riches, fighting monsters, and preparing for a final battle with Morgoth, the Lord of Darkness.</desc>
<releasedate>20230819T000000</releasedate>
<developer>Angband Development Team</developer>
<publisher>Angband Development Team</publisher>
<genre>RPG</genre>
<image>./angband/cover.png</image>
</game>
</gameList>
Licenses¶
Add license files for all sources, libraries and assets you used, in a
licenses subfolder. For example:
- The game project's own license, if it's an open source game
- gptokeyb
- sdl1.2 compat
- gl4es
- box86 / box64
- any
.solibraries you shipped
You can usually find these either in the source folder you compiled from, or on
your build system under /usr/share/doc/<package>/copyright.
The launch script¶
Example scripts
Ready-made scripts for each engine are on the Script Templates page.
Below is a launch script with every section annotated, explaining what each part does.
#!/bin/bash
# Below we assign the source of the control folder (which is the PortMaster folder) based on the distro:
XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
if [ -d "/opt/system/Tools/PortMaster/" ]; then
controlfolder="/opt/system/Tools/PortMaster"
elif [ -d "/opt/tools/PortMaster/" ]; then
controlfolder="/opt/tools/PortMaster"
elif [ -d "$XDG_DATA_HOME/PortMaster/" ]; then
controlfolder="$XDG_DATA_HOME/PortMaster"
else
controlfolder="/roms/ports/PortMaster"
fi
source $controlfolder/control.txt # We source the control.txt file contents here
# The $ESUDO, $directory, $param_device and necessary sdl configuration controller configurations will be sourced from the control.txt file shown [here]
# If a Port is built for armhf architecture only (Need for Speed 2 for example) we set this flag so that some environment condition variables are set in the CFWs mod files.
# Example "https://github.com/PortsMaster/PortMaster-GUI/blob/main/PortMaster/mod_JELOS.txt"
export PORT_32BIT="Y" # If using a 32 bit port, else comment it out.
# We source custom mod files from the portmaster folder example mod_jelos.txt which containts pipewire fixes
[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt"
# We pull the controller configs like the correct SDL2 Gamecontrollerdb GUID from the get_controls function from the control.txt file here
get_controls
# We switch to the port's directory location below & set the variable for the gamedir and a configuration dir easier handling below
GAMEDIR=/$directory/ports/portfolder/
CONFDIR="$GAMEDIR/conf/"
# Ensure the conf directory exists
mkdir -p "$GAMEDIR/conf"
# Switch to the game directory
cd $GAMEDIR
# Log the execution of the script, the script overwrites itself on each launch
> "$GAMEDIR/log.txt" && exec > >(tee "$GAMEDIR/log.txt") 2>&1
# Some ports like to create save files or settings files in the user's home folder or other locations. We map these config folders so we can either preconfigure games and or have the savefiles in one place.
# You can either use XDG variables to redirect the Ports to our gamefolder if the port supports it:
# Set the XDG environment variables for config & savefiles
export XDG_DATA_HOME="$CONFDIR"
# OR
# Use bind_directories to reroute that to a location within the ports folder.
bind_directories ~/.portfolder $GAMEDIR/conf/.portfolder
# Port specific additional libraries should be included within the port's directory in a separate subfolder named libs.aarch64, libs.armhf or libs.x64
export LD_LIBRARY_PATH="$GAMEDIR/libs.${DEVICE_ARCH}:$LD_LIBRARY_PATH"
# Provide appropriate controller configuration if it recognizes SDL controller input
export SDL_GAMECONTROLLERCONFIG="$sdl_controllerconfig"
# If a port uses GL4ES (libgl.so.1) a folder named gl4es.aarch64 etc. needs to be created with the libgl.so.1 file in it. This makes sure that each cfw and device get the correct GL4ES export.
if [ -f "${controlfolder}/libgl_${CFW_NAME}.txt" ]; then
source "${controlfolder}/libgl_${CFW_NAME}.txt"
else
source "${controlfolder}/libgl_default.txt"
fi
# We launch gptokeyb using this $GPTOKEYB variable as it will take care of sourcing the executable from the central location,
# assign the appropriate exit hotkey dependent on the device (ex. select + start for most devices and minus + start for the
# rgb10) and assign the appropriate method for killing an executable dependent on the OS the port is run from.
# With -c we assign a custom mapping file else gptokeyb will only run as a tool to kill the process.
# For $ANALOG_STICKS we have the ability to supply multiple gptk files to support 1 and 2 analogue stick devices in different ways.
# For a proper documentation how gptokeyb works: [Link](https://github.com/PortsMaster/gptokeyb)
$GPTOKEYB "portexecutable.${DEVICE_ARCH}" -c "./portname.gptk.$ANALOG_STICKS" &
# Do some platform specific stuff right before the port is launched but after GPTOKEYB is run.
pm_platform_helper "$GAMEDIR/portexecutable.${DEVICE_ARCH}"
# Now we launch the port's executable with multiarch support. Make sure to rename your file according to the architecture you built for. E.g. portexecutable.aarch64
./portexecutable.${DEVICE_ARCH}
# Cleanup any running gptokeyb instances, and any platform specific stuff.
pm_finish
Functions and error handling¶
Some games need installation or patching on first run. Use functions to keep the launch script organised, and error handling inside them to keep it stable.
# Functions
install() {
pm_message "Performing first-run setup..."
# Purge unneeded files
rm -rf assets/*.exe assets/*.dll assets/.gitkeep
# Rename data.win
pm_message "Moving game files..."
mv "./assets/data.win" "./game.droid" || return 1
mv assets/* ./
rmdir assets
# Do localization fonts and xdelta patch if low ram
if [ $DEVICE_RAM -lt 2 ]; then
rm -rf "$GAMEDIR/localization_fonts.csv"
mv patch/localization_fonts.csv ./
find $GAMEDIR -type f -iname "*.ttf" ! -iname "Commodore Rounded v1-1.ttf" ! -iname "small_pixel.ttf" -delete
apply_patch && rm -rf "$GAMEDIR/patch" # Only remove if function is successful
fi
}
apply_patch() {
pm_message "Applying patch..."
if [ -f "$controlfolder/xdelta3" ]; then
error=$("$controlfolder/xdelta3" -d -s "$GAMEDIR/game.droid" "$GAMEDIR/patch/iosas.xdelta" "$GAMEDIR/game2.droid" 2>&1)
if [ $? -eq 0 ]; then
rm -rf "$GAMEDIR/game.droid"
mv "$GAMEDIR/game2.droid" "$GAMEDIR/game.droid"
pm_message "Patch applied successfully."
else
pm_message "Failed to apply patch. Error: $error"
rm -f "$GAMEDIR/game2.droid"
return 1
fi
else
pm_message "Error: xdelta3 not found in $controlfolder. Try updating PortMaster."
return 1
fi
}
if [ ! -f "$GAMEDIR/game.droid" ] && [ ! -f "$GAMEDIR/.installed" ]; then
install && touch "$GAMEDIR/.installed" # Only touch if function is successful
fi
Several things to note here:
- The line moving
game.droidreturns1immediately if it fails, which stops the install function proceeding after a critical task didn't complete. apply_patchand the.csvfile are only used if the device has less than 2GB of RAM, using the$DEVICE_RAMvariable filled bycontrol.txt.- The
$GAMEDIR/patchdirectory is only removed ifapply_patchsucceeded, via&&. That lets the user correct mistakes during install without reinstalling the port. apply_patchis a nest of conditionals for error checking, and returns1if it failed.installonly runs once. On success a.installedfile is created, which prevents it running again.