initial commit

This commit is contained in:
beerpsi 2023-12-30 12:02:40 +07:00 committed by beerpsi@duck.com
commit bf14d32980
12 changed files with 1368 additions and 0 deletions

92
.gitignore vendored Normal file
View File

@ -0,0 +1,92 @@
# Created by https://www.toptal.com/developers/gitignore/api/clion+all
# Edit at https://www.toptal.com/developers/gitignore?templates=clion+all
### CLion+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### CLion+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
# End of https://www.toptal.com/developers/gitignore/api/clion+all

20
CMakeLists.txt Normal file
View File

@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.27)
project(chuniio_brokenithm)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
include_directories("${CMAKE_SOURCE_DIR}/include/")
set(CMAKE_CXX_STANDARD 17)
link_directories(src)
add_library(chuniio_brokenithm SHARED src/chuniio.c
src/chuniio.h
src/config.c
src/config.h
src/socket.h
src/struct.h
src/util/dprintf.c
src/util/dprintf.h)
set_target_properties(chuniio_brokenithm PROPERTIES PREFIX "")
set_target_properties(chuniio_brokenithm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
target_link_libraries(chuniio_brokenithm ws2_32)

5
LICENSE Normal file
View File

@ -0,0 +1,5 @@
Copyright (C) 2023 by beerpsi beerpsi@noreply.gitea.tendokyu.moe
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

44
README.md Normal file
View File

@ -0,0 +1,44 @@
# chuniio-brokenithm
ChuniIO driver for [Brokenithm-Android](https://github.com/tindy2013/Brokenithm-Android)
without needing an external server.
It is recommended to use this with [Dniel97's segatools](https://gitea.tendokyu.moe/Dniel97/segatools/releases),
since it allows loading 32-bit chuniio DLLs without any messy hacks.
## Configuration
segatools.ini
```ini
[chuniio]
path=chuniio_brokenithm.dll
[io3]
; Test button virtual-key code. Default is the 1 key.
test=0x31
; Service button virtual-key code. Default is the 2 key.
service=0x32
; Keyboard button to increment coin counter. Default is the 3 key.
coin=0x33
; AIR sensor emulator. Default is the space key.
; If using individual-ray IR (see below), set this value to 0.
ir=0x20
[brokenithm]
; Use TCP instead of UDP for connections (default UDP)
tcp=1
; Port to accept connections on (default 52468)
port=52468
```
## Build instructions
```shell
mkdir cmake-build
cd cmake-build
cmake ..
ninja
ls chuniio_brokenithm.dll
```

667
src/chuniio.c Normal file
View File

@ -0,0 +1,667 @@
//
// Created by beerpsi on 12/30/2023.
//
#include "chuniio.h"
#include <stdio.h>
#include <inttypes.h>
#include <signal.h>
#include <time.h>
#include <process.h>
#include <assert.h>
#include "config.h"
#include "struct.h"
#include "util/dprintf.h"
//region Brokenithm
struct IPCMemoryInfo* chuni_io_file_mapping;
const char *memFileName = "Local\\BROKENITHM_SHARED_BUFFER";
char remote_address[BUFSIZ];
uint16_t remote_port = 52468;
uint16_t server_port = 52468;
bool tcp_mode = false;
volatile sig_atomic_t EXIT_FLAG = false, CONNECTED = false;
uint32_t last_input_packet_id = 0;
enum {
CARD_AIME,
CARD_FELICA,
};
enum {
FUNCTION_COIN = 1,
FUNCTION_CARD
};
typedef struct {
SOCKET sHost;
struct IPCMemoryInfo* memory;
} thread_args;
void socket_set_timeout(SOCKET sHost, int timeout) {
setsockopt(sHost, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(int));
setsockopt(sHost, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(int));
}
int socket_bind(SOCKET sHost, unsigned long addr, uint16_t port) {
struct sockaddr_in srcaddr = {};
memset(&srcaddr, 0, sizeof(srcaddr));
srcaddr.sin_family = AF_INET;
srcaddr.sin_addr.s_addr = addr;
srcaddr.sin_port = htons(port);
return bind(sHost, (struct sockaddr*)&srcaddr, sizeof(srcaddr));
}
int socket_send_to(SOCKET sHost, const struct sockaddr_in* addr, const char* buf, int len) {
return sendto(sHost, buf, len, 0, (struct sockaddr*)&addr, sizeof(&addr));
}
void print_err(const char* fmt, ...) {
time_t lt = time(NULL);
struct tm *local = localtime(&lt);
char tmpbuf[32];
strftime(tmpbuf, 32, "%Y-%m-%d %H:%M:%S", local);
dprintf("brokenithm_server: [%s] ", tmpbuf);
va_list ap;
va_start(ap, fmt);
dprintfv(fmt, ap);
va_end(ap);
}
void get_socks_address(const struct PacketConnect* pkt, char* address, int address_len, uint16_t *port) {
if (!pkt || !address || !port) {
return;
}
*port = ntohs(pkt->port);
switch (pkt->addrType) {
case 1:
inet_ntop(AF_INET, pkt->addr.addr4.addr, address, address_len);
break;
case 2:
inet_ntop(AF_INET6, pkt->addr.addr6, address, address_len);
break;
default:
return;
}
}
void update_packet_id(uint32_t new_packet_id)
{
if (last_input_packet_id > new_packet_id) {
print_err("[WARN] Packet #%" PRIu32 " came too late\n", new_packet_id);
} else if (new_packet_id > last_input_packet_id + 1) {
print_err("[WARN] Packets between #%" PRIu32 " and #%" PRIu32 " total %" PRIu32 " packet(s) are missing, probably too late or dropped\n",
last_input_packet_id, new_packet_id, new_packet_id - last_input_packet_id - 1);
} else if (new_packet_id == last_input_packet_id) {
print_err("[WARN] Packet #%" PRIu32 " duplicated\n", new_packet_id);
}
last_input_packet_id = new_packet_id;
}
void dump_bytes(const void *ptr, size_t nbytes, bool hex_string)
{
const uint8_t *bytes;
uint8_t c;
size_t i;
size_t j;
if (nbytes == 0) {
dprintf("\t--- Empty ---\n");
}
bytes = (const unsigned char*)ptr;
if (hex_string) {
for (i = 0 ; i < nbytes ; i++) {
dprintf("%02x", bytes[i]);
}
dprintf("\n");
return;
}
for (i = 0 ; i < nbytes ; i += 16) {
dprintf(" %08x:", (int) i);
for (j = 0 ; i + j < nbytes && j < 16 ; j++) {
dprintf(" %02x", bytes[i + j]);
}
while (j < 16) {
dprintf(" ");
j++;
}
dprintf(" ");
for (j = 0 ; i + j < nbytes && j < 16 ; j++) {
c = bytes[i + j];
if (c < 0x20 || c >= 0x7F) {
c = '.';
}
dprintf("%c", c);
}
dprintf("\n");
}
dprintf("\n");
}
void print_card_info(uint8_t card_type, uint8_t *card_id) {
switch (card_type) {
case CARD_AIME:
print_err("[INFO] Card type: AiMe, ID: ");
dump_bytes(card_id, 10, true);
break;
case CARD_FELICA:
print_err("[INFO] Card type: FeliCa, ID: ");
dump_bytes(card_id, 8, true);
break;
default:
break;
}
}
int make_ipv4_address(struct sockaddr_in* addr, char* host, uint16_t port) {
addr->sin_family = AF_INET;
addr->sin_port = htons(port);
return inet_pton(AF_INET, host, (struct in_addr *)&addr->sin_addr.s_addr);
}
uint8_t previous_led_status[3 * 32];
bool has_previous_led_status = false;
int skip_count = 0;
unsigned int __stdcall thread_led_broadcast(void *ctx) {
thread_args *args = (thread_args*)ctx;
SOCKET sHost = args->sHost;
struct IPCMemoryInfo* memory = args->memory;
struct sockaddr_in addr = {};
make_ipv4_address(&addr, remote_address, remote_port);
char send_buffer[4 + 3 * 32];
send_buffer[0] = 100;
send_buffer[1] = 'L';
send_buffer[2] = 'E';
send_buffer[3] = 'D';
uint8_t current_led_status[3 * 32];
while (!EXIT_FLAG) {
if (!CONNECTED) {
Sleep(50);
continue;
}
memcpy(current_led_status, memory->ledRgbData, 3 * 32);
bool same;
if (!has_previous_led_status) {
same = (memcmp(previous_led_status, current_led_status, 3 * 32) == 0);
} else {
same = false;
}
memcpy(previous_led_status, current_led_status, 3 * 32);
has_previous_led_status = true;
if (!same || ++skip_count > 50) {
memcpy(send_buffer + 4, current_led_status, 3 * 32);
if (socket_send_to(sHost, &addr, send_buffer, 100) < 0) {
print_err("[ERROR] Cannot send packet: error %lu\n", GetLastError());
if (tcp_mode) {
if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN) {
continue;
} else {
print_err("[INFO] Device disconnected!\n");
CONNECTED = false;
EXIT_FLAG = true;
break;
}
}
}
skip_count = 0;
}
Sleep(10);
}
return 0;
}
uint8_t last_card_id[10];
unsigned int __stdcall thread_input_recv(void *ctx) {
thread_args *args = (thread_args*)ctx;
SOCKET sHost = args->sHost;
struct IPCMemoryInfo* memory = args->memory;
char buffer[BUFSIZ];
struct sockaddr_in addr = {};
make_ipv4_address(&addr, remote_address, remote_port);
int recv_len, packet_len;
uint8_t real_len;
while (!EXIT_FLAG) {
if (!tcp_mode) {
/**
on UDP mode data is sent as packets, so just receive into a buffer big enough for 1 packet
each recvfrom call will only get 1 packet of data, the remaining data is discarded
**/
if ((recv_len = recvfrom(sHost, buffer, BUFSIZ - 1, 0, NULL, NULL)) == -1) {
continue;
}
real_len = (unsigned char)buffer[0];
if (real_len > recv_len) {
continue;
}
packet_len = real_len + 1;
} else {
/**
on TCP mode packets are length-prefixed, so we read in the first 4 bytes to
figure out how much we need to read, then read in the full data.
**/
recv_len = 0;
while (recv_len < 4) {
int read = recv(sHost, buffer + recv_len, 4 - recv_len, 0);
if (read == -1) {
if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN) {
continue;
} else {
print_err("[INFO] Device disconnected!\n");
CONNECTED = false;
EXIT_FLAG = true;
break;
}
}
recv_len = recv_len + read;
}
real_len = buffer[0];
packet_len = real_len + 1;
while (recv_len < packet_len) {
int read = recv(sHost, buffer + recv_len, packet_len - recv_len, 0);
if (read == -1) {
if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN) {
continue;
} else {
print_err("[INFO] Device disconnected!\n");
CONNECTED = false;
EXIT_FLAG = true;
break;
}
}
recv_len = recv_len + read;
}
}
if (packet_len >= sizeof(struct PacketInput) && buffer[1] == 'I' && buffer[2] == 'N' && buffer[3] == 'P') {
struct PacketInput* pkt = (struct PacketInput*)buffer;
memcpy(memory->airIoStatus, pkt->airIoStatus, sizeof(pkt->airIoStatus));
memcpy(memory->sliderIoStatus, pkt->sliderIoStatus, sizeof(pkt->sliderIoStatus));
memory->testBtn = pkt->testBtn;
memory->serviceBtn = pkt->serviceBtn;
update_packet_id(ntohl(pkt->packetId));
} else if (packet_len >= sizeof(struct PacketInputNoAir) && buffer[1] == 'I' && buffer[2] == 'P' && buffer[3] == 'T') { // without air
struct PacketInputNoAir* pkt = (struct PacketInputNoAir*)buffer;
memcpy(memory->sliderIoStatus, pkt->sliderIoStatus, sizeof(pkt->sliderIoStatus));
memory->testBtn = pkt->testBtn;
memory->serviceBtn = pkt->serviceBtn;
update_packet_id(ntohl(pkt->packetId));
} else if (packet_len >= sizeof(struct PacketFunction) && buffer[1] == 'F' && buffer[2] == 'N' && buffer[3] == 'C') {
struct PacketFunction* pkt = (struct PacketFunction*)buffer;
switch (pkt->funcBtn) {
case FUNCTION_COIN:
memory->coinInsertion = 1;
break;
case FUNCTION_CARD:
memory->cardRead = 1;
break;
}
} else if (packet_len >= sizeof(struct PacketConnect) && buffer[1] == 'C' && buffer[2] == 'O' && buffer[3] == 'N') {
struct PacketConnect* pkt = (struct PacketConnect*)buffer;
get_socks_address(pkt, remote_address, BUFSIZ - 1, &remote_port);
print_err("[INFO] Device %s:%d connected.\n", remote_address, remote_port);
last_input_packet_id = 0;
CONNECTED = true;
} else if (packet_len >= 4 && buffer[1] == 'D' && buffer[2] == 'I' && buffer[3] == 'S') {
CONNECTED = false;
if (tcp_mode) {
EXIT_FLAG = 1;
print_err("[INFO] Device disconnected!\n");
break;
}
if (strlen(remote_address)) {
print_err("[INFO] Device %s:%d disconnected.\n", remote_address, remote_port);
memset(remote_address, 0, BUFSIZ);
}
} else if (packet_len >= sizeof(struct PacketPing) && buffer[1] == 'P' && buffer[2] == 'I' && buffer[3] == 'N') {
if (!CONNECTED) {
continue;
}
char response[13];
memcpy(response, buffer, 12);
response[2] = 'O';
socket_send_to(sHost, &addr, response, 13);
} else if (packet_len >= sizeof(struct PacketCard) && buffer[1] == 'C' && buffer[2] == 'R' && buffer[3] == 'D') {
struct PacketCard* pkt = (struct PacketCard*)buffer;
if (pkt->remoteCardRead) {
if (memcmp(last_card_id, pkt->remoteCardId, 10) != 0) {
print_err("[INFO] Got remote card.\n");
print_card_info(pkt->remoteCardType, pkt->remoteCardId);
memcpy(last_card_id, pkt->remoteCardId, 10);
}
} else if (memory->remoteCardRead) {
print_err("[INFO] Remote card removed.\n");
memset(last_card_id, 0, 10);
}
memory->remoteCardRead = pkt->remoteCardRead;
memory->remoteCardType = pkt->remoteCardType;
memcpy(memory->remoteCardId, pkt->remoteCardId, 10);
}
}
return 0;
}
unsigned int __stdcall server_thread_proc(void* ctx) {
struct IPCMemoryInfo* memory = (struct IPCMemoryInfo*)ctx;
if (!tcp_mode) {
print_err("[INFO] Mode: UDP\n");
SOCKET sHost = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
socket_set_timeout(sHost, 2000);
socket_bind(sHost, htonl(INADDR_ANY), server_port);
print_err("[INFO] Waiting for device on port %d...\n", server_port);
thread_args args = { .memory = memory, .sHost = sHost };
HANDLE led_thread = (HANDLE)_beginthreadex(NULL, 0, thread_led_broadcast, &args, 0, NULL);
HANDLE input_thread = (HANDLE)_beginthreadex(NULL, 0, thread_input_recv, &args, 0, NULL);
WaitForSingleObject(led_thread, INFINITE);
WaitForSingleObject(input_thread, INFINITE);
CloseHandle(led_thread);
CloseHandle(input_thread);
} else {
print_err("[INFO] Mode: TCP\n");
SOCKET sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
socket_set_timeout(sHost, 50);
socket_bind(sHost, htonl(INADDR_ANY), server_port);
listen(sHost, 10);
#pragma clang diagnostic push
#pragma ide diagnostic ignored "EndlessLoop"
for (;;) {
print_err("[INFO] Waiting for device on port %d...\n", server_port);
struct sockaddr_in user_socket = {};
socklen_t sock_size = sizeof(struct sockaddr_in);
SOCKET acc_socket = accept(sHost, (struct sockaddr *)&user_socket, &sock_size);
char buffer[20] = {};
const char* user_address = inet_ntop(AF_INET, &user_socket.sin_addr, buffer, 20);
if (user_address != NULL) {
print_err("[INFO] Device %s:%d connected.\n", user_address, user_socket.sin_port);
}
CONNECTED = true;
EXIT_FLAG = false;
thread_args args = { .memory = memory, .sHost = acc_socket };
HANDLE led_thread = (HANDLE)_beginthreadex(NULL, 0, thread_led_broadcast, &args, 0, NULL);
HANDLE input_thread = (HANDLE)_beginthreadex(NULL, 0, thread_input_recv, &args, 0, NULL);
WaitForSingleObject(led_thread, INFINITE);
WaitForSingleObject(input_thread, INFINITE);
CloseHandle(led_thread);
CloseHandle(input_thread);
print_err("[INFO] Exiting gracefully...\n");
last_input_packet_id = 0;
EXIT_FLAG = true;
CONNECTED = false;
}
#pragma clang diagnostic pop
}
return 0;
}
HRESULT server_start() {
tcp_mode = GetPrivateProfileIntW(L"brokenithm", L"tcp", 0, L".\\segatools.ini") == 1;
server_port = GetPrivateProfileIntW(L"brokenithm", L"port", 52468, L".\\segatools.ini");
struct WSAData wsaData = {};
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
print_err("[ERROR] WSA startup failed!\n");
return E_FAIL;
}
HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, false, memFileName);
if (hMapFile == NULL) {
hMapFile = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 1024, memFileName);
if (hMapFile == NULL) {
print_err("[ERROR] CreateFileMapping failed! error: %lu\n", GetLastError());
return E_FAIL;
}
}
struct IPCMemoryInfo* memory = (struct IPCMemoryInfo*)MapViewOfFileEx(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 1024, NULL);
chuni_io_file_mapping = memory;
if (memory == NULL) {
print_err("[ERROR] Cannot get view of memory map! error: %lu\n", GetLastError());
return E_FAIL;
}
_beginthreadex(NULL, 0, server_thread_proc, memory, 0, NULL);
return S_OK;
}
//endregion
//region ChuniIO stuff
static unsigned int __stdcall chuni_io_slider_thread_proc(void *ctx);
static bool chuni_io_coin;
static uint16_t chuni_io_coins;
static uint8_t chuni_io_hand_pos;
static HANDLE chuni_io_slider_thread;
static bool chuni_io_slider_stop_flag;
static struct chuni_io_config chuni_io_cfg;
uint16_t chuni_io_get_api_version() {
return 0x0102;
}
HRESULT chuni_io_jvs_init() {
chuni_io_config_load(&chuni_io_cfg, L".\\segatools.ini");
HRESULT result = server_start();
if (result != S_OK) {
return result;
}
return S_OK;
}
void chuni_io_jvs_read_coin_counter(uint16_t *out) {
if (out == NULL) {
return;
}
if (chuni_io_file_mapping && chuni_io_file_mapping->coinInsertion) {
chuni_io_coins++;
chuni_io_file_mapping->coinInsertion = 0;
} else {
if (GetAsyncKeyState(chuni_io_cfg.vk_coin)) {
if (!chuni_io_coin) {
chuni_io_coin = true;
chuni_io_coins++;
}
} else {
chuni_io_coin = false;
}
}
*out = chuni_io_coins;
}
void chuni_io_jvs_poll(uint8_t *opbtn, uint8_t *beams) {
size_t i;
if ((chuni_io_file_mapping && chuni_io_file_mapping->testBtn) || GetAsyncKeyState(chuni_io_cfg.vk_test)) {
*opbtn |= CHUNI_IO_OPBTN_TEST; /* Test */
}
if ((chuni_io_file_mapping && chuni_io_file_mapping->serviceBtn) || GetAsyncKeyState(chuni_io_cfg.vk_service)) {
*opbtn |= CHUNI_IO_OPBTN_SERVICE; /* Service */
}
if (GetAsyncKeyState(chuni_io_cfg.vk_ir_emu)) {
if (chuni_io_hand_pos < 6) {
chuni_io_hand_pos++;
}
} else {
if (chuni_io_hand_pos > 0) {
chuni_io_hand_pos--;
}
}
for (i = 0 ; i < 6 ; i++) {
if (chuni_io_hand_pos > i) {
*beams |= (1 << i);
}
}
// IR format is beams[5:0] = {b5,b6,b3,b4,b1,b2};
for (i = 0 ; i < 3 ; i++) {
if (chuni_io_file_mapping && chuni_io_file_mapping->airIoStatus[i*2])
*beams |= (1 << (i*2+1));
if (chuni_io_file_mapping && chuni_io_file_mapping->airIoStatus[i*2+1])
*beams |= (1 << (i*2));
}
}
HRESULT chuni_io_slider_init() {
return S_OK;
}
void chuni_io_slider_start(void* callback) {
if (chuni_io_slider_thread != NULL) {
return;
}
chuni_io_slider_thread = (HANDLE) _beginthreadex(
NULL,
0,
chuni_io_slider_thread_proc,
callback,
0,
NULL);
}
void chuni_io_slider_stop(void) {
if (chuni_io_slider_thread == NULL) {
return;
}
chuni_io_slider_stop_flag = true;
WaitForSingleObject(chuni_io_slider_thread, INFINITE);
CloseHandle(chuni_io_slider_thread);
chuni_io_slider_thread = NULL;
chuni_io_slider_stop_flag = false;
}
void chuni_io_slider_set_leds(const uint8_t *rgb) {
if (chuni_io_file_mapping) {
memcpy(chuni_io_file_mapping->ledRgbData, rgb, 32 * 3);
}
}
HRESULT chuni_io_led_init(void) {
return S_OK;
}
void chuni_io_led_set_colors(uint8_t board, uint8_t *rgb) {}
static unsigned int __stdcall chuni_io_slider_thread_proc(void *ctx)
{
chuni_io_slider_callback_t callback;
uint8_t pressure[32];
callback = (chuni_io_slider_callback_t)ctx;
#pragma clang diagnostic push
#pragma ide diagnostic ignored "LoopDoesntUseConditionVariableInspection"
while (!chuni_io_slider_stop_flag) {
if (chuni_io_file_mapping) {
memcpy(pressure, chuni_io_file_mapping->sliderIoStatus, 32);
}
callback(pressure);
Sleep(1);
}
#pragma clang diagnostic pop
return 0;
}
//endregion
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
return TRUE;
}

172
src/chuniio.h Normal file
View File

@ -0,0 +1,172 @@
//
// Created by beerpsi on 12/30/2023.
//
#ifndef CHUNIIO_BROKENITHM_CHUNIIO_H
#define CHUNIIO_BROKENITHM_CHUNIIO_H
#include "socket.h"
#include <windows.h>
#include <stdbool.h>
#include <stdint.h>
enum {
CHUNI_IO_OPBTN_TEST = 0x01,
CHUNI_IO_OPBTN_SERVICE = 0x02,
CHUNI_IO_OPBTN_COIN = 0x04,
};
/* Get the version of the Chunithm IO API that this DLL supports. This
function should return a positive 16-bit integer, where the high byte is
the major version and the low byte is the minor version (as defined by the
Semantic Versioning standard).
The latest API version as of this writing is 0x0101. */
uint16_t chuni_io_get_api_version();
/* Initialize JVS-based input. This function will be called before any other
chuni_io_jvs_*() function calls. Errors returned from this function will
manifest as a disconnected JVS bus.
All subsequent calls may originate from arbitrary threads and some may
overlap with each other. Ensuring synchronization inside your IO DLL is
your responsibility.
Minimum API version: 0x0100 */
HRESULT chuni_io_jvs_init();
/* Poll JVS input.
opbtn returns the cabinet test/service state, where bit 0 is Test and Bit 1
is Service.
beam returns the IR beams that are currently broken, where bit 0 is the
lowest IR beam and bit 5 is the highest IR beam, for a total of six beams.
Both bit masks are active-high.
Note that you cannot instantly break the entire IR grid in a single frame to
simulate hand movement; this will be judged as a miss. You need to simulate
a gradual raising and lowering of the hands. Consult the proof-of-concept
implementation for details.
NOTE: Previous releases of Segatools mapped the IR beam inputs incorrectly.
Please ensure that you advertise an API version of at least 0x0101 so that
the correct mapping can be used.
Minimum API version: 0x0100
Latest API version: 0x0101 */
void chuni_io_jvs_poll(uint8_t *opbtn, uint8_t *beams);
/* Read the current state of the coin counter. This value should be incremented
for every coin detected by the coin acceptor mechanism. This count does not
need to persist beyond the lifetime of the process.
Minimum API version: 0x0100 */
void chuni_io_jvs_read_coin_counter(uint16_t *total);
/* Initialize touch slider emulation. This function will be called before any
other chuni_io_slider_*() function calls.
All subsequent calls may originate from arbitrary threads and some may
overlap with each other. Ensuring synchronization inside your IO DLL is
your responsibility.
Minimum API version: 0x0100 */
HRESULT chuni_io_slider_init(void);
/* Chunithm touch slider layout:
^^^ Toward screen ^^^
----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
31 | 29 | 27 | 25 | 23 | 21 | 19 | 17 | 15 | 13 | 11 | 9 | 7 | 5 | 3 | 1 |
----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
32 | 30 | 28 | 26 | 24 | 22 | 20 | 18 | 16 | 14 | 12 | 10 | 8 | 6 | 4 | 2 |
----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
There are a total of 32 regions on the touch slider. Each region can return
an 8-bit pressure value. The operator menu allows the operator to adjust the
pressure level at which a region is considered to be pressed; the factory
default value for this setting is 20. */
/* Callback function supplied to your IO DLL. This must be called with a
pointer to a 32-byte array of pressure values, one byte per slider cell.
See above for layout and pressure threshold information.
The callback will copy the pressure state data out of your buffer before
returning. The pointer will not be retained. */
typedef void (*chuni_io_slider_callback_t)(const uint8_t *state);
/* Start polling the slider. Your DLL must start a polling thread and call the
supplied function periodically from that thread with new input state. The
update interval is up to you, but if your input device doesn't have any
preferred interval then 1 kHz is a reasonable maximum frequency.
Note that you do have to have to call the callback "occasionally" even if
nothing is changing, otherwise the game will raise a comm timeout error.
Minimum API version: 0x0100 */
void chuni_io_slider_start(void *callback);
/* Stop polling the slider. You must cease to invoke the input callback before
returning from this function.
This *will* be called in the course of regular operation. For example,
every time you go into the operator menu the slider and all of the other I/O
on the cabinet gets restarted.
Following on from the above, the slider polling loop *will* be restarted
after being stopped in the course of regular operation. Do not permanently
tear down your input driver in response to this function call.
Minimum API version: 0x0100 */
void chuni_io_slider_stop(void);
/* Update the RGB lighting on the slider. A pointer to an array of 32 * 3 = 96
bytes is supplied. The illuminated areas on the touch slider are some
combination of rectangular regions and dividing lines between these regions
but the exact mapping of this lighting control buffer is still TBD.
Minimum API version: 0x0100 */
void chuni_io_slider_set_leds(const uint8_t *rgb);
/* Initialize LED emulation. This function will be called before any
other chuni_io_led_*() function calls.
All subsequent calls may originate from arbitrary threads and some may
overlap with each other. Ensuring synchronization inside your IO DLL is
your responsibility.
Minimum API version: 0x0102 */
HRESULT chuni_io_led_init(void);
/* Update the RGB LEDs. rgb is a pointer to an array of up to 63 * 3 = 189 bytes.
Chunithm uses two chains/boards with WS2811 protocol (each logical led corresponds to 3 physical leds).
board 0 is on the left side and board 1 on the right side of the cab
left side has 5*10 rgb values for the billboard, followed by 3 rgb values for the air tower
right side has 6*10 rgb values for the billboard, followed by 3 rgb values for the air tower
Each rgb value is comprised of 3 bytes in R,G,B order
NOTE: billboard strips have alternating direction (bottom to top, top to bottom, ...)
Minimum API version: 0x0102 */
void chuni_io_led_set_colors(uint8_t board, uint8_t *rgb);
#endif //CHUNIIO_BROKENITHM_CHUNIIO_H

80
src/config.c Normal file
View File

@ -0,0 +1,80 @@
#include <windows.h>
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "config.h"
static const int chuni_io_default_cells[] = {
'L', 'L', 'L', 'L',
'K', 'K', 'K', 'K',
'J', 'J', 'J', 'J',
'H', 'H', 'H', 'H',
'G', 'G', 'G', 'G',
'F', 'F', 'F', 'F',
'D', 'D', 'D', 'D',
'S', 'S', 'S', 'S',
};
static const int chuni_io_default_ir[] = {
'4', '5', '6', '7', '8', '9'
};
void chuni_io_config_load(
struct chuni_io_config *cfg,
const wchar_t *filename)
{
wchar_t key[16];
int i;
wchar_t port_input[6];
assert(cfg != NULL);
assert(filename != NULL);
// Technically it's io4 but leave this for compatibility with old configs.
cfg->vk_test = GetPrivateProfileIntW(L"io3", L"test", '1', filename);
cfg->vk_service = GetPrivateProfileIntW(L"io3", L"service", '2', filename);
cfg->vk_coin = GetPrivateProfileIntW(L"io3", L"coin", '3', filename);
cfg->vk_ir_emu = GetPrivateProfileIntW(L"io3", L"ir", VK_SPACE, filename);
for (i = 0 ; i < 6 ; i++) {
swprintf_s(key, _countof(key), L"ir%i", i + 1);
cfg->vk_ir[i] = GetPrivateProfileIntW(
L"ir",
key,
chuni_io_default_ir[i],
filename);
}
for (i = 0 ; i < 32 ; i++) {
swprintf_s(key, _countof(key), L"cell%i", i + 1);
cfg->vk_cell[i] = GetPrivateProfileIntW(
L"slider",
key,
chuni_io_default_cells[i],
filename);
}
cfg->led_output_pipe = GetPrivateProfileIntW(L"led", L"cabLedOutputPipe", 1, filename);
cfg->led_output_serial = GetPrivateProfileIntW(L"led", L"cabLedOutputSerial", 0, filename);
cfg->slider_led_output_pipe = GetPrivateProfileIntW(L"led", L"controllerLedOutputPipe", 1, filename);
cfg->slider_led_output_serial = GetPrivateProfileIntW(L"led", L"controllerLedOutputSerial", 0, filename);
cfg->led_serial_baud = GetPrivateProfileIntW(L"led", L"serialBaud", 921600, filename);
GetPrivateProfileStringW(
L"led",
L"serialPort",
L"COM5",
port_input,
6,
filename);
// Sanitize the output path. If it's a serial COM port, it needs to be prefixed
// with `\\.\`.
wcsncpy(cfg->led_serial_port, L"\\\\.\\", 4);
wcsncat_s(cfg->led_serial_port, 11, port_input, 6);
}

38
src/config.h Normal file
View File

@ -0,0 +1,38 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
struct chuni_io_config {
uint8_t vk_test;
uint8_t vk_service;
uint8_t vk_coin;
uint8_t vk_ir_emu;
uint8_t vk_ir[6];
uint8_t vk_cell[32];
// Which ways to output LED information are enabled
bool led_output_pipe;
bool led_output_serial;
bool slider_led_output_pipe;
bool slider_led_output_serial;
// The name of a COM port to output LED data on, in serial mode
wchar_t led_serial_port[12];
int32_t led_serial_baud;
};
void chuni_io_config_load(
struct chuni_io_config *cfg,
const wchar_t *filename);
#ifdef __cplusplus
}
#endif

44
src/socket.h Normal file
View File

@ -0,0 +1,44 @@
//
// Created by beerpsi on 12/29/2023.
//
#ifndef CHUNIIO_BROKENITHM_SOCKET_H
#define CHUNIIO_BROKENITHM_SOCKET_H
#ifdef _WIN32
#ifndef WINVER
#define WINVER 0x0501
#endif // WINVER
#include <ws2tcpip.h>
#include <winsock2.h>
#else
//translate windows functions to linux functions
#include <unistd.h>
#include <string.h>
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#define closesocket close
#define SOCKADDR_IN sockaddr_in
#define ZeroMemory(d,l) memset((d), 0, (l))
#define ioctlsocket ioctl
#ifndef SA_INTERRUPT
#define SA_INTERRUPT 0 //ignore this setting
#endif
#define SD_BOTH SHUT_RDWR
#ifndef __hpux
#include <sys/select.h>
#endif /* __hpux */
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <netdb.h>
#include <signal.h>
#include <unistd.h>
typedef sockaddr *LPSOCKADDR;
#endif // _WIN32
#endif //CHUNIIO_BROKENITHM_SOCKET_H

91
src/struct.h Normal file
View File

@ -0,0 +1,91 @@
//
// Created by beerpsi on 12/29/2023.
//
#ifndef CHUNIIO_BROKENITHM_STRUCT_H
#define CHUNIIO_BROKENITHM_STRUCT_H
#include <stdint.h>
;
#pragma pack(push)
#pragma pack(1)
struct IPCMemoryInfo
{
uint8_t airIoStatus[6];
uint8_t sliderIoStatus[32];
uint8_t ledRgbData[32 * 3];
uint8_t testBtn;
uint8_t serviceBtn;
uint8_t coinInsertion;
uint8_t cardRead;
uint8_t remoteCardRead;
uint8_t remoteCardType;
uint8_t remoteCardId[10];
};
struct PacketInput
{
uint8_t packetSize;
uint8_t packetName[3];
uint32_t packetId;
uint8_t airIoStatus[6];
uint8_t sliderIoStatus[32];
uint8_t testBtn;
uint8_t serviceBtn;
};
struct PacketInputNoAir
{
uint8_t packetSize;
uint8_t packetName[3];
uint32_t packetId;
uint8_t sliderIoStatus[32];
uint8_t testBtn;
uint8_t serviceBtn;
};
struct PacketFunction
{
uint8_t packetSize;
uint8_t packetName[3];
uint8_t funcBtn;
};
struct PacketConnect
{
uint8_t packetSize;
uint8_t packetName[3];
uint8_t addrType;
uint16_t port;
union
{
struct
{
uint8_t addr[4];
uint8_t padding[12];
} addr4;
uint8_t addr6[16];
} addr;
};
struct PacketCard
{
uint8_t packetSize;
uint8_t packetName[3];
uint8_t remoteCardRead;
uint8_t remoteCardType;
uint8_t remoteCardId[10];
};
struct PacketPing
{
uint8_t packetSize;
uint8_t packetName[3];
uint64_t remotePingTime;
};
#pragma pack(pop)
#endif //CHUNIIO_BROKENITHM_STRUCT_H

85
src/util/dprintf.c Normal file
View File

@ -0,0 +1,85 @@
#ifndef NDEBUG
#include <windows.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include "dprintf.h"
static long dbg_buf_lock_init;
static CRITICAL_SECTION dbg_buf_lock;
static char dbg_buf[16384];
static size_t dbg_buf_pos;
void dprintf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
dprintfv(fmt, ap);
va_end(ap);
}
void dprintfv(const char *fmt, va_list ap)
{
long init;
/* Static constructors in C are difficult to do in a way that works under
both GCC and MSVC, so we have to use atomic ops to ensure that the
buffer mutex is correctly initialized instead. */
do {
init = InterlockedCompareExchange(&dbg_buf_lock_init, 0, 1);
if (init == 0) {
/* We won the init race, global variable is now set to 1, other
threads will spin until it becomes -1. */
InitializeCriticalSection(&dbg_buf_lock);
dbg_buf_lock_init = -1;
init = -1;
}
} while (init >= 0);
EnterCriticalSection(&dbg_buf_lock);
dbg_buf_pos += vsnprintf_s(
dbg_buf + dbg_buf_pos,
sizeof(dbg_buf) - dbg_buf_pos,
sizeof(dbg_buf) - dbg_buf_pos - 1,
fmt,
ap);
if (dbg_buf_pos + 1 > sizeof(dbg_buf)) {
abort();
}
if (strchr(dbg_buf, '\n') != NULL) {
OutputDebugStringA(dbg_buf);
dbg_buf_pos = 0;
dbg_buf[0] = '\0';
}
LeaveCriticalSection(&dbg_buf_lock);
}
void dwprintf(const wchar_t *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
dwprintfv(fmt, ap);
va_end(ap);
}
void dwprintfv(const wchar_t *fmt, va_list ap)
{
wchar_t msg[512];
_vsnwprintf_s(msg, _countof(msg), _countof(msg) - 1, fmt, ap);
OutputDebugStringW(msg);
}
#endif

30
src/util/dprintf.h Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdarg.h>
#include <stddef.h>
#ifdef __GNUC__
#define DPRINTF_CHK __attribute__(( format(printf, 1, 2) ))
#else
#define DPRINTF_CHK
#endif
#ifndef NDEBUG
void dprintf(const char *fmt, ...) DPRINTF_CHK;
void dprintfv(const char *fmt, va_list ap);
void dwprintf(const wchar_t *fmt, ...);
void dwprintfv(const wchar_t *fmt, va_list ap);
#else
#define dprintf(...)
#define dprintfv(fmt, ap)
#define dwprintf(...)
#define dwprintfv(fmt, ap)
#endif
#ifdef __cplusplus
}
#endif