An overlook on JVS I/O emulation and implementation

Contains a decent amount of code, better seen in the original blog.

Introduction

The only downside might be piracy and bootlegging, and though it’s always been a problem in the arcade scene, this time could go stronger since PCs, as simple as it is to develop for, the same could be said for cracking protections and decrypting information. These form part of the layer that prohibits us from running the software on normal PCs, in conjunction with the I/O devices.

Some history

The story dates back to 2009–2011 (I can’t remember exactly), when data dumps of most of the Type X games, some of X2, and all the eX-BOARD titles were released in arcade forums. This data was unprotected, which means that no security devices or checks were needed for the games to work. I can’t recall if the emulator for I/O was also released at the same time, but for sure shortly after one appeared. This first loader did just that, emulate the I/O device, and thus removing the final wall that prevented the software to be run without displaying an I/O ERROR screen.

Some time after, Romhack reworked this emulator into the open-source Type X I/O emulator, ttx_monitor, and later built on top of it the eX-BOARD variant, xb_monitor. He later also did a Cave-PC variant, cv_monitor, but to my knowledge it hasn’t been open-sourced. The Romhack emulators became the default to use during a long time, until more recently other capable alternatives appeared, like JConfig and TeknoParrot, and despite these emulate much more I/O devices of other machines as well, the Type X and eX-BOARD cores are all based on Romhack’s emulators. Now even myself entered the trend, and made enhanced versions of both ttx_monitor and xb_monitor, TTX-Monitor+ and XB-Monitor+ respectively.

What to expect

Since I don’t own any arcades, some basic terminology such as JAMMA was unknown to me, and still kind of is, so my only supports are the original JVS documentation and Romhack’s code, besides some self-play with the software which uses those devices. Still, I think it was enough to understand how things work, hence the high-level remark. That being said, this write might be only enjoyed by casual people that aren’t on topic, and don’t be surprised if any inaccuracies present during the lecture.

Protection at its finest

Chaos Breaker cloned hard disk drive image

The first has a Windows XP Embedded install, a Type X loader/launcher and a virtual image disk with the game data.

The root of the C: partition, the TypeXsys folder contents and last the encrypted disk image inside the data folder with the game data. Also, yes.txt

The loader does hardware checks (dongle, disk drive, partitions) and if everything is okay, a key is retrieved, which decrypts the image file, thus being able to run the game. Once the game is executed, it checks for a valid JVS I/O device in the COM2 port. If present, the game continues normally reaching full execution, and if not, the I/O board error will show up.

I/O error screens from Trouble Witches and GigaWing Generations, respectively

There’s also a second partition, which stores all the game configuration and data. The only reason I can think of this is that the first partition might be read-only, or at least nothing is written over there.

A sole partition just for this

In the case of eX-BOARD games, those were delivered in IDE cartridges, being by itself enough protection. Or that’s why Examu thought at least, being cracked in short time. In lack of more documentation about the protection, looking at xb_monitor code we see a hook in the library IpgExKey.dll, function _GetKeyLicense@0:

BOOL APIENTRY HookFunctions()
// eX-BOARD software function hooks.
HGetKeyLicense = HookIt("IpgExKey.dll", "_GetKeyLicense@0", Hook_GetKeyLicense);

INT APIENTRY Hook_GetKeyLicense(VOID)
return 1;

Function hook in XB-Monitor+

Loading the DLL in Ghidra, we can see all exported functions, GetKeyLicense being one of them:

I think it’s safe to assume from its name that it handles the protection check, so making it always return true is enough to bypass it. Now with the decrypted data and protection cracked, the only barrier left is the JVS I/O device.

Communication Device

Schematics of the Type X, JVS main board and JVS I/O board connected to the child I/O board

The software then reads the input data from the COM2 port device. The data is transferred in packets through the JVS protocol:

JVS protocol package structure

To put it simply, the Sync Code determines the start of a valid JVS packet, which always has the value of 0xE0. The Node indicates the destination address, or the slave device/node of destination. The Byte determines the size of the rest of the packet, including the checksum, and this sum helps to identify if a packet is corrupt or not. The Data is, precisely, the data, which is formed by a command and arguments. There’s a large list of commands for different functions. In emulation, most of the commands are hardcoded for the initialization of the I/O board, so the one that really matters to us is the command 0x20, SWINP, or more friendly, Switch Inputs:

Command SWIMP byte data for normal inputs (there’s also for mahjong panels and dual sticks)

So, how do we emulate this process?

I/O Emulation

BOOL APIENTRY HookFunctions() {
// Communications devices function hooks.
HOOK("kernel32.dll", ClearCommError, H, LP, Hook);
HOOK("kernel32.dll", CloseHandle, H, LP, Hook);
HOOK("kernel32.dll", EscapeCommFunction, H, LP, Hook);
HOOK("kernel32.dll", GetCommModemStatus, H, LP, Hook);
HOOK("kernel32.dll", GetCommState, H, LP, Hook);
HOOK("kernel32.dll", GetCommTimeouts, H, LP, Hook);
HOOK("kernel32.dll", SetCommMask, H, LP, Hook);
HOOK("kernel32.dll", SetCommState, H, LP, Hook);
HOOK("kernel32.dll", SetCommTimeouts, H, LP, Hook);
HOOK("kernel32.dll", SetupComm, H, LP, Hook);
}

In concept is simple, we create a stream of data that will simulate the JVS transfer structure, build the correct packets and return a valid reply. Basically, we feed the data that the stream wants to hear. The interesting part is the request of command 0x20, when the second part of the emulation comes to play.

Besides the fake COM device, we also need a true input layer, which then we can link with the former and thus generating the input signals through a virtual JVS packet. For this, we create two DInput devices (although any input API would work, we use DInput since the games by themselves also use it, so we need it anyways): a fake one, which is hooked into the game so it doesn’t detect any inputs, and a real one, that will read our inputs from the selected device. This way, we cancel any input read from the game, and we inject our inputs into the JVS stream, which then will be read by the game.

// Prevents the games of having access to input devices.
HRESULT APIENTRY Fake_DirectInput8Create(HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID* ppvOut, LPUNKNOWN punkOuter) {
// Flag to make a true DInput device after the fake one was already created.
if (DIMagicCall)
// Passthrough to create a normal DInput device
return FDirectInput8Create(hinst, dwVersion, riidltf, ppvOut, punkOuter);
else {
*ppvOut = (LPVOID)pFakeInterface;
punkOuter = NULL;
// This device returns null when GetState() is called, thus no inputs are registered.
return DI_OK;
}
}

The DInput initialization behaves as normal, the devices are enumerated, acquire the one we want and finally create a polling thread. When we press a key/button on the device, it will set a flag in an input state array, which is read by the JVS stream polling.

// Check for a joystick command.
if (IS_JOY_OBJECT(InValue)) {
// Check joystick axes and buttons.
}
// Check for keyboard commands.
else {
int Button = GET_JOY_BUT(InValue);
StateTable[i] = JoyState[JoyNumber].rgbButtons[Button] & 0x80 ? 1 : 0;
}

Check if the polled key is pressed and set the corresponding flag in the array

// Controller status. Command SWINP.
case 0x20: {
// Push to byte 0.
JVS.bPush(InputMgr.GetState(TEST_MODE) ? 0x80 : 0);
// Push to bytes 1 and 2.
JVS.bPush(InInfo.Xp1HiByte());
JVS.bPush(InInfo.Xp1LoByte());
JVS.bPush(InInfo.Xp2HiByte());
JVS.bPush(InInfo.Xp2LoByte());
break;
}

Then the JVS polling will get the state of the input table and process it

When the fake JVS detects the flag, it sets the bit in the corresponding byte of the Input Switch data block:

BYTE Xp1HiByte() {
BYTE Byte = 0;
if (InputMgr.GetState(P1_START))
Byte |= 0x80;
if (InputMgr.GetState(P1_SERVICE))
Byte |= 0x40;
if (InputMgr.GetState(P1_UP))
Byte |= 0x20;
if (InputMgr.GetState(P1_DOWN))
Byte |= 0x10;
if (InputMgr.GetState(P1_RIGHT))
Byte |= 0x04;
if (InputMgr.GetState(P1_LEFT))
Byte |= 0x08;
if (InputMgr.GetState(P1_BUTTON_1))
Byte |= 0x02;
if (InputMgr.GetState(P1_BUTTON_2))
Byte |= 0x01;
return Byte;
}

BYTE Xp1LoByte() {
BYTE Byte = 0;
if (InputMgr.GetState(P1_BUTTON_3))
Byte |= 0x80;
if (InputMgr.GetState(P1_BUTTON_4))
Byte |= 0x40;
if (InputMgr.GetState(P1_BUTTON_5))
Byte |= 0x20;
if (InputMgr.GetState(P1_BUTTON_6))
Byte |= 0x10;
return Byte;
}

Setting up the first 2 bytes, which belong to P1’s inputs.

Finally, the packet is sent and the function returns, the reply is buffered and then it’s interpreted by the game as a legit JVS I/O button press.

Excellent, now what

But why not use the modern alternatives then? Because, for whatever reason, none of them have support for Examu’s eX-BOARD titles, and by them I mean JConfig, because TeknoParrot seems to support the platform, but I don’t really like the software itself so I prefer to avoid it, even if that means to develop my own solution.

Here’s where my enhanced version of the only open source solution available, xb_monitor, comes to play, trying to put the old loader up to modern alternatives. And since I was already there, I also decided to apply the same treatment to ttx_loader, because why not (and well, both share most of the code anyways), though later I’d find a good use for it. But before going through those new features, it’s important to remark that both projects have seen a big overhaul of the codebase, in such a way that I feel is much better to read and understand, so if you want to see the inner works, might want to check TTX-Monitor+ and XB-Monitor+ over the originals.

QOL Enhancements

#define DEADZONE 500 /*(MAX_AXIS_VAL / DEADZONE_DIV)*/

The new deadzone value (500), with the old implementation commented out (10)

In the function for input pooling, only the left axis (AxisL) and buttons were checked, very limited. Support for the right axis (AxisR), triggers (AxisZ) and POVs were added, in addition to a PovAsAxis option, which allows to use POVs together with the left axis mapped inputs (like the Analog button on DualShock controllers).

// Axis definitions.
#define AXIS_X 1
#define AXIS_Y 2
#define AXIS_Z 3
#define AXIS_RX 4
#define AXIS_RY 5
#define POVN 10
// POVs definitions.
#define POV_CENTER -1
#define POV_UP 0
#define POV_UP_RIGHT 4500
#define POV_RIGHT 9000
#define POV_RIGHT_DOWN 13500
#define POV_DOWN 18000
#define POV_DOWN_LEFT 22500
#define POV_LEFT 27000
#define POV_LEFT_UP 31500

// Polling of joystick axes and POVs.
switch (GET_JOY_AXIS(InValue)) {
case AXIS_X: {
if (IS_NEGATIVE_AXIS(InValue)) {
if ((JoyState[JoyNumber].lX < -DEADZONE) || (mTable[CONFIG_POVASAXIS] && ((Dir == POV_LEFT) || (Dir == POV_DOWN_LEFT) || (Dir == POV_LEFT_UP))))
StateTable[i] = 1;
}
else {
if ((JoyState[JoyNumber].lX > DEADZONE) || (mTable[CONFIG_POVASAXIS] && ((Dir == POV_RIGHT) || (Dir == POV_UP_RIGHT) || (Dir == POV_RIGHT_DOWN))))
StateTable[i] = 1;
}
break;
}
case POVN: {
// To avoid problems, mapped POVs are disabled and
// forced to work as axis if PovAsAxis option is enabled.
if ((JoyState[JoyNumber].rgdwPOV[0] != -1) && !mTable[CONFIG_POVASAXIS]) {
if (GET_JOY_RANGE(InValue) == POV_UP && ((Dir == POV_UP) || (Dir == POV_UP_RIGHT) || (Dir == POV_LEFT_UP)))
StateTable[i] = 1;
if (GET_JOY_RANGE(InValue) == POV_RIGHT && ((Dir == POV_RIGHT) || (Dir == POV_UP_RIGHT) || (Dir == POV_RIGHT_DOWN)))
StateTable[i] = 1;
if (GET_JOY_RANGE(InValue) == POV_DOWN && ((Dir == POV_DOWN) || (Dir == POV_RIGHT_DOWN) || (Dir == POV_DOWN_LEFT)))
StateTable[i] = 1;
if (GET_JOY_RANGE(InValue) == POV_LEFT && ((Dir == POV_LEFT) || (Dir == POV_DOWN_LEFT) || (Dir == POV_LEFT_UP)))
StateTable[i] = 1;
}
break;
}
}

Extract from the input polling function.

Now that we’re done with controls, I’ll touch in some features that I thought were unnecessary to include: the logging functions and the DirectDraw wrapper. The former is still present code-wise, and is available as a debug tool just for development, instead of always being on and creating logs that most users don’t care at all. While the DirectDraw wrapper has been removed, the Direct3D 9 was required for XB-Monitor+, but has been reduced to a single purpose: fix the window drawing in Arcana Heart 3. The original implementation was more complex, but for this I just forced fullscreen mode @640x480, like the rest of eX-BOARD games do.

HRESULT HookIDirect3D9::CreateDevice(LPVOID _this, UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS* pPresentationParameters, IDirect3DDevice9** ppReturnedDeviceInterface) {
pPresentationParameters->Windowed = FALSE;
pPresentationParameters->BackBufferWidth = 640;
pPresentationParameters->BackBufferHeight = 480;
return pD3D->CreateDevice(Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, ppReturnedDeviceInterface);
}

The important part of the wrapper

This decision was in favor of the use of external wrappers, like the excellent dgVoodoo, which not only could solve incompatibilities in modern systems, but also enhance the visuals. Other loaders like JConfig have limited wrappers bundled to have a more out-of-the-box experience, but under mine those don’t work pretty well, or lack features. Regardless, a nice thing to have.

Last but not least, a SavePatch feature. As shown at the beginning of the article, most games stored their options and score data in a different partition. Their behaviour didn’t change and they will attempt to save there. The problems are, not everyone has a second partition with the specified drive letter, and we don’t want our data to be all over the place. To fix this, we redirect all the file and directory operations to a specified save folder in the root directory of the application.

For eX-BOARD games is simple, since the data isn’t stored in the hard drive, but rather in volatile memory (I don’t know specifically where or in which form), so we create a virtual SRAM file, which then is loaded in memory. xb_monitor already placed the SRAM binary data in the sv folder, a structure that’s used in JConfig and binary patches, and will be carried over XB-Monitor+ and TTX-Monitor+ as well.

VOID SaveSRAM() {
FILE* Stream = NULL;
Stream = fopen(SRAM_NAME, "wb");
if (!Stream)
return;
fwrite(SRAM, 1, SRAM_SIZE, Stream);
fclose(Stream);
}

But Type X is a different beast, that looks simple at first, but its implementation gets complicated. In theory we hook our own CreateDirectory and CreateFile system functions (both ANSI and wide character variants) and call it a day, but when we deal with subdirectories shit gets annoying. With the current implementation, I’ve got all games saving in the save directory, but some like The King Of Fighters ‘98, Gouketsuji Ichizoku Senzo Kuyou and Trouble Witches won’t read it back. While it might be fixable, perhaps with a different algorithm, it wasn’t worth the hassle, considering that other loaders probably have game-specific patches (certainly TeknoParrot), while I aim for a more dynamic approach.

using namespace std::literals;

// Beautiful recursion. Necessary for games which create subfolders for savedata.
void CreateFolderA(CHAR* SaveFolder, CHAR* SaveSubFolderC) {
if (strcmp(SaveFolder, SaveSubFolderC) != 0) {
CHAR SaveSubFolder[MAX_PATH];
strcpy(SaveSubFolder, SaveSubFolderC);
strrchr(SaveSubFolderC, '\\')[0] = '\0';
CreateFolderA(SaveFolder, SaveSubFolderC);
HCreateDirectoryA(SaveSubFolder, nullptr);
}
else
HCreateDirectoryA(SaveFolder, nullptr);
}

BOOL APIENTRY Hook_CreateDirectoryA(LPCSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes)
{
if (mTable[CONFIG_SAVEPATCH]) {
// Assuming that no Type X game store data in the C: drive. Excludes relative paths.
if ((lpPathName[0] != 'C' && lpPathName[0] != 'c') && lpPathName[1] == ':') {
CHAR RootPath[MAX_PATH];
GetModuleFileNameA(GetModuleHandleA(nullptr), RootPath, _countof(RootPath));
strrchr(RootPath, '\\')[0] = '\0';
std::string SavePath = RootPath + "\\sv\\"s;
return HCreateDirectoryA(SavePath.c_str(), nullptr);
}
}
return HCreateDirectoryA(lpPathName, lpSecurityAttributes);
}

HANDLE APIENTRY Hook_CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile)
{
if (mTable[CONFIG_SAVEPATCH]) {
// Assuming that no Type X game store data in the C: drive. Excludes relative paths.
if ((lpFileName[0] != 'C' && lpFileName[0] != 'c') && lpFileName[1] == ':') {
// Get game working directory.
CHAR RootPath[MAX_PATH];
GetModuleFileNameA(GetModuleHandleA(nullptr), RootPath, _countof(RootPath));
// Strip executable filename from path.
strrchr(RootPath, '\\')[0] = '\0';
std::string FilePath = lpFileName;
// Forced to 3 to skip both slashes and backslashes.
std::string FileName = FilePath.substr(3);
// Get working directory lenght.
int PathLenght = 0;
for (int i = 0; i < MAX_PATH; i++)
if (RootPath[i] == '\0') {
PathLenght = i;
break;
}
// Exclude directory files. Avoids screwing up normal file operations.
if (strncmp(lpFileName, RootPath, PathLenght) != 0) {
std::string SavePath = RootPath + "\\sv\\"s;
std::string SaveFile = SavePath + FileName;
std::string SaveSubFolderS = SaveFile.substr(0, SaveFile.length() - (FileName.length() - FileName.rfind('\\')));
CHAR SaveFolder[MAX_PATH];
strcpy(SaveFolder, (SavePath.substr(0, SavePath.length() - 1)).c_str());
CHAR SaveSubFolderC[MAX_PATH];
strcpy(SaveSubFolderC, SaveSubFolderS.c_str());
CreateFolderA(SaveFolder, SaveSubFolderC);
return HCreateFileA(SaveFile.c_str(), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
}
}
}
return HCreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
}

System function hooks for directory and file redirection

Something important to point out is that we only redirect file operations that aren’t relative to the current path. This way we don’t fuck up with the game trying to read its data files.

Introducing mahjong input support

Apparently, while both use indeed JVS for I/O communication, it seems like a custom implementation, since JVS is flexible enough to do this. Why? I don’t know, they could’ve stuck with the standard (you know, the S part) mahjong panel handling. The people behind JConfig have been trying to figure this out, and told me that a different JVS dump is needed in order to provide the correct data the games are expecting. Until then, let’s go for a hacky solution.

Luckily for us, the developers left keyboard controls for debugging, or at least on THG5, which uses DirectInput. THGMP doesn’t, though it seems to map some keys, which aren’t activated for some reason. I’m still investigating this, so hopefully there’s a way to unlock the keyboard for THGMP. It should work until a proper way to emulate the mahjong JVS I/O is finally sorted out.

The only loader that was able to run THG5 with the debug controls is the original, the first released TypeX Loader. The reason for this is that every other loader creates a fake DirectInput device to prevent the game from taking inputs by itself, thus forcing only JVS inputs to be recognized. For mahjong titles, this hook was disabled, leaving the game to take the inputs, and despite the loader offering a mahjong panel input configuration, it was never implemented and never worked. We’ll be doing the first, and fix the later.

So, with this in mind, the idea is to give the fake feeling of proper emulation, by enabling the remapping of the keyboard keys to any device, including the keyboard itself. For this, a DirectInput wrapper is implemented, with some algorithm complexity. Basically, it takes care of all the input handling, and manages the original and user mapped keys. This way, we want to avoid any conflicts that might occur when overlapping the original and user configuration.

While it sounded simple at first, it was quite challenging to implement correctly. All the mahjong input configuration has been separated from the normal input, to make it easier to develop, understand and ultimately, use.

// Only limitation is that if a key is mapped to a pointer of another key, both of those
// can't be pressed at the same time. Example: 'A' is mapped to the 'A' key, and 'B' is
// mapped to '1'. Both can't be pressed at the same time because 'A' points to '1'.
void PollInputMulti(int ThreadNumber) {
for (;;) {
// +3 is the mahjong inputs offset.
if (InputMgr.GetState(ThreadNumber + 3)) {
// Avoid the thread to process a key already being processed by another.
if (isPressed[ThreadNumber] == 0) {
isPressed[ThreadNumber] = 1;
INPUT Input = { 0 };
Input.type = INPUT_KEYBOARD;
Input.ki.wScan = MapVirtualKey(LOBYTE(VI_CODES[ThreadNumber]), 0);
// Value needed for the releasing of pointed keys.
int isPointer = 0xFF;
// Check if the key pressed has a pointer key.
for (int k = M_START; k < M_END; k++)
if ((DIK_CODES[ThreadNumber] == iTable[k])) {
isPointer = k;
break;
}
if (isPointer != 0xFF)
isPressed[isPointer] = 2;
// SendInput loop.
while (InputMgr.GetState(ThreadNumber + 3)) {
Input.ki.dwFlags = KEYEVENTF_SCANCODE;
SendInput(1, &Input, sizeof(Input));
Sleep(10); // Pause necessary for the next key to be recognized.
Input.ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, &Input, sizeof(Input));
}
// Wait for another thread to process and reject the last SendInput,
// in case the sent key pointed to an also mapped key.
Sleep(50);
if (isPointer != 0xFF)
isPressed[isPointer] = 0;
isPressed[ThreadNumber] = 0;
}
}
Sleep(20); // Reduce the thread processing.
}
}

Multi-threaded version of the polling algorithm

For the input polling, a new thread is created for each button. At first I wanted to make the thread count arbitrary, but that wasn’t going anywhere. Also, single thread polling is available, which funnily enough fixes a problem of how inconsistently the game handles multiple input presses.

One final touch

Instead of updating the old UI, I went with a new .NET Windows Forms solution made from scratch. Well, kind of, since I had most of it done for another arcade-related project, including all the interface operations and controller integration, through the DirectInput API. When I first developed this, it took quite the time, considering that I had never worked with DirectX nor any input API before, but at the same time it helped me a lot to understand the whole input implementation in ttx_monitor and xb_monitor.

Also put my graphic designer skills to show with two brand new logos and icons, neat.

Might not be over yet

However, the game behaves differently than TGH5: TGHMP is a collection of the first 4 games in the series. Each game has its own executable on its own folder, including the test mode and the special menu to select the game. It turns out that game.exe is the process which handles the child process creation and the JVS communication, acting as a pipeline to the real game processes. For this reason, this game is a pain to debug, so it’s not very straightforward as it’d usually be. Still, I’ll give it a try.

It was a long journey that I don’t consider over yet, but I take the experience of refactoring another person’s project, implementing new core features, programming for the first time in C, and just learning more about how modern arcade machines work.

References

JVS I/O — PCB Otaku Wiki

JVS Protocol — OpenJVS

Taito Type X User Manual — TAITO

JAMMA Video Standard (The Third Edition) (JVS) — JAMMA

JAMMA Video Standard (JVS) Third Edition — Alex Marshall

PC Hardware in Arcades, an Analysis — Alex Marshall

Low-level lover in an high-level society. Software developer professionally (the degree says so), graphic designer by hobby. hipnosis183.github.io

Low-level lover in an high-level society. Software developer professionally (the degree says so), graphic designer by hobby. hipnosis183.github.io