This is a loader for various arcade games developed by 573. The project is using CMake as it's build system, with a custom build script for making packages ready for distribution and to keep it easy for people not knowing how to use CMake.
We're currently using Arch Linux for building the binaries. You'll need:
- MinGW-64 packages (can be found in the AUR)
- bash
- git
- zip
- upx (optional)
For any other GNU/Linux distributions (or Windows lol), you're on your own.
To build the project, run:
$ ./build_all.sh
You can tweak some settings at the beginning of the build script. You might want to modify the paths of the toolchains if yours differ. It's also possible to disable UPX compression and source distribution for example.
If you're not using an IDE and want to run the build script from command line manually each time you make a change, you can set CLEAN_BUILD to 0 so it will only compile what's needed. When modifying the resources you should build from scratch, since CMake isn't able to track changes of those files (e.g. changelog.txt, licenses.txt).
You can put your custom build script under "build_all.local.sh" if you don't want git to track the changes you made to the build settings.
To get proper stackstraces, you need to build with the DEBUG setting enabled. This will set the CMake build type to debug and the script will try to make use of cv2pdb. Check external/cv2pdb/README.md for details on how to set it up. It is required to use cv2pdb to generate PDB output, because at time of writing, MinGW still didn't support Microsoft's proprietary file format.
SpiceTools is providing a TCP/JSON based API. You can enable it with the
-api [PORT]
option. It's recommended to also set a password with
-apipass [PASS]
.
The protocol is meant to be simple, easy to use and with few overhead.
To connect to the API, just open a TCP connection to the host computer
at the specified port.
To make a request you write the UTF-8 encoded JSON contents to the
socket. To mark the end of the request, you need to terminate the JSON
string with 0x00
. The server returns its answer on the same way,
UTF-8 encoded JSON terminated with NULL.
To save space, by default the JSONs are without whitespace. To enable
pretty printing, use -apipretty
.
If you want to test the API server without running a game, you can
run it headless via -apidebug
.
If a password is specified, both request and response are encrypted
using RC4 with the key being the password encoded in UTF-8.
While only providing weak security when the password is static,
it's simple to implement and requires no authentication protocol.
The password is also able to change dynamically, see
control.session_refresh()
for details.
If you don't want to worry about all the details, you can just use one of the supplied libraries (check /api/resources). There's also a little remote control GUI available for python.
SpiceTools opens a WebSocket server on the specified port plus one. That means if the API is on 1337, the WebSocket server will be on 1338. The protocol is the very same as for the normal API, but instead of directly sending the data over TCP you need to send binary datapackets. The included dart spiceapi library also has a WebSocket implementation.
This example inserts the a card into P1's reader slot. If you need more examples, you can check the example code.
{
"id": 1,
"module": "card",
"function": "insert",
"params": [0, "E004000000000000"]
}
{
"id": 1,
"errors": [],
"data": []
}
For the sake of simplifying this documentation, I will describe functions
as if they were called via a normal programming language. That means,
besides module
/function
needing to be set accordingly, the params
field
must contain all the parameters described below. Some of them are optional.
The returning data will always be in the data
field of the response.
Errors will be reported as strings in the errors
field. Sometimes, you can
receive more than one error. The error messages may be changed/improved in the
future, so you shouldn't rely on them. If the error list is empty, the function
executed successfully.
The id
of the response will always match the id
of the request. The API
server itself doesn't expect any logic of your ID choices, however you can use
them to make sure your answers aren't arriving out of order. Currently it
doesn't matter since the TCP protocol doesn't allow for out of order data,
however this may change when/if support for UDP is being introduced. The only
restriction is that the ID has to be a valid 64-bit unsigned integer.
- insert(index: uint, card_id: hex)
- inserts a card which gets read by the emulated card readers for the game
- index has to be either 0 (for P1) or 1 (for P2)
- card_id has to be a valid 16-character hex string
- get()
- returns the amount of unprocessed coins in queue
- not equal to the amount of coins shown ingame since the game may drain it
- set(amount: int)
- sets the amount of coins in queue for the game to process
- insert(amount: int)
- adds the amount to the coins in queue
- amount is optional and defaults to 1
- blocker_get()
- returns the current coin blocker state (false: open, true: closed)
- avs()
- returns a dict including the AVS model, dest, spec, rev and ext
- launcher()
- returns a dict including the version, compile date/time, system time and the arguments used to start the process
- memory()
- returns a dict including total, total used and bytes used by the process for both physical and virtual RAM
For all functions in this module, the keypad parameter must be either 0 (for P1) or 1 (for P2). Accepted keypad characters are "0" to "9" for the single digit numbers, "A" for the double zero and "D" for the decimal key.
- write(keypad: uint, input: str)
- writes all characters in input sequentially to the keypad
- this should be used for things like PIN input
- set(keypad: uint, key: char, ...)
- clears all overrides, then applies each given key as override
- if a key is overridden, it shows up as pressed in-game
- get(keypad: uint)
- returns all pressed keys of the specified keypad
All of those three modules have equally named methods for you to call.
- read()
- returns an array of state objects containing name, state and a bool
- the bool indicates if the object is active (e.g. the button was overridden)
- write([name: str, state: float], ...)
- applies the states as an override value
- the device binding via the config will be ignored while an override is set
- if an override value is set, the object will be marked as active
- write_reset([name: str], ...)
- removes the override value from the objects specified by name
- if no names were passed, all overrides will be removed
- read()
- returns an array of state objects containing id, x and y
- write([id: uint, x: int, y: int], ...)
- adds the given touch points having an unknown ID
- overrides old touch points when the ID matches
- write_reset(id: uint, ...)
- removes the touch points matching one of the given IDs
This module only functions if a password is being used to communicate, with the single exception of session_refresh().
- raise(signal: str)
- raises a signal to the current process
- signal is a string and must be one of the following values: "SIGABRT", "SIGFPE", "SIGILL", "SIGINT", "SIGSEGV", "SIGTERM"
- the signal will be raised while the message is being processed
- exit(code: int)
- code is optional and defaults to 0
- the process will end while the message is being processed
- restart()
- spawns a new instance with the same arguments as passed to the main executable
- session_refresh()
- generates a new secure password and applies it after the response
- can be used to increase the security by using the password as some kind of session token
- recommended to call directly after connecting and once in a while for persistent connections
- shutdown()
- tries to force shutdown the computer
- reboot()
- tries to force reboot the computer
This module only functions if a password is being used to communicate.
All offsets are specified in file offsets of the corresponding dll_name
,
which also means that your hex edits are applicable directly.
- write(dll_name: str, data: hex, offset: uint)
- writes the bytes in data to the specified offset
- read(dll_name: str, offset: uint, size: uint)
- reads bytes starting from offset and returns it
- signature(dll_name: str, signature: hex, replacement: hex, offset: uint,
usage: uint)
- tries to find the signature in the module's memory
- unknown bytes are masked out via "??" (e.g. "75??90????74")
- masking out bytes works for both the signature and the replacement
- offset is the byte difference between the offset of the found signature and the offset where the replacement data gets written to
- usage is the number of the result being used for replacement, starting at 0
- returns the offset where the replacement data gets written to
- the replacement string can be empty, so you can only get the address of the signature while not actually replacing anything
- ticker_get()
- returns a string of the characters displayed on the 16 segment display
- ticker_set(text: str)
- sets the contents of the 16 segment display and disables writes from game
- ticker_reset()
- re-enables writes from game
- info()
- returns information about the serial LCD controller some games use
Unless otherwise noted, all files are licensed under the GPLv3. See the LICENSE file for the full license text. Files not originating from this project should be placed in "external", they each have their own license.
- If you want to distribute the binaries, you'll have to include the sources, otherwise you're violating the GPL.
- If you want to merge some of this project's code into similar software, that software has to be put under the GPL as well. You'll probably have to modify a lot anyways, so you can just use the source as some kind of documentation.