From a6915a95f8bb67af289deae7948a0f46a9b90902 Mon Sep 17 00:00:00 2001 From: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:41:08 +0200 Subject: [PATCH] ekt: implement y3ws --- .gitignore | 1 + common/y3io/impl/websockets/config.c | 39 +++ common/y3io/impl/websockets/config.h | 19 ++ common/y3io/impl/websockets/y3ws.c | 351 +++++++++++++++++++++++++++ common/y3io/impl/websockets/y3ws.h | 4 + common/y3io/meson.build | 10 +- doc/y3ws.txt | 55 +++++ meson.build | 1 + subprojects/cwinwebsocket.wrap | 5 + 9 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 common/y3io/impl/websockets/config.c create mode 100644 common/y3io/impl/websockets/config.h create mode 100644 common/y3io/impl/websockets/y3ws.c create mode 100644 common/y3io/impl/websockets/y3ws.h create mode 100644 doc/y3ws.txt create mode 100644 subprojects/cwinwebsocket.wrap diff --git a/.gitignore b/.gitignore index f5d9008..8e442f9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ build/ # External dependencies subprojects/capnhook +subprojects/cwinwebsocket # For enabling debug logging on local builds MesonLocalOptions.mk diff --git a/common/y3io/impl/websockets/config.c b/common/y3io/impl/websockets/config.c new file mode 100644 index 0000000..4f7a963 --- /dev/null +++ b/common/y3io/impl/websockets/config.c @@ -0,0 +1,39 @@ +#include + +#include +#include +#include +#include + +#include "config.h" + +void y3ws_config_load(struct y3ws_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->enable = GetPrivateProfileIntW(L"y3ws", L"enable", 1, filename); + cfg->debug = GetPrivateProfileIntW(L"y3ws", L"debug", 0, filename); + + cfg->port = GetPrivateProfileIntW(L"y3ws", L"port", 3497, filename); + + wchar_t tmpstr[16]; + + GetPrivateProfileStringW( + L"y3ws", + L"gameId", + L"SDEY", + tmpstr, + _countof(tmpstr), + filename); + + wcstombs(cfg->game_id, tmpstr, sizeof(cfg->game_id)); + + GetPrivateProfileStringW( + L"y3ws", + L"cardDirectory", + L"DEVICE\\print", + cfg->card_path, + _countof(cfg->card_path), + filename); +} diff --git a/common/y3io/impl/websockets/config.h b/common/y3io/impl/websockets/config.h new file mode 100644 index 0000000..91ad18f --- /dev/null +++ b/common/y3io/impl/websockets/config.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include "hooklib/dvd.h" +#include "hooklib/touch.h" +#include "hooklib/printer_chc.h" +#include "hooklib/printer_cx.h" + +struct y3ws_config { + bool enable; + bool debug; + + uint16_t port; + char game_id[5]; + wchar_t card_path[MAX_PATH]; +}; + +void y3ws_config_load(struct y3ws_config *cfg, const wchar_t *filename); \ No newline at end of file diff --git a/common/y3io/impl/websockets/y3ws.c b/common/y3io/impl/websockets/y3ws.c new file mode 100644 index 0000000..49374e2 --- /dev/null +++ b/common/y3io/impl/websockets/y3ws.c @@ -0,0 +1,351 @@ +#include +#include +#include +#include +#include + +#include "3rdparty/cjson/cJSON.h" + +#include "config.h" +#include "y3ws.h" + +#include + +#include "hooklib/y3.h" + +#include "winwebsocket.h" +#include "lib/base64.h" + +#include "util/dprintf.h" +#include "util/env.h" + +static void onopen(struct wws_connection*); +static void onclose(struct wws_connection*); +static void onmessage(struct wws_connection*, const char*, size_t); +static void onlog(const char*, ...); + +#define PROTOCOL_VERSION 1 +#define GAME_MAX_CARDS 32 +#define MAX_CARDS 500 +#define CARD_ID_LEN 5 +#define OUTPUT_BUFFER_SIZE (1024 * 1024 * 5) + +static struct y3ws_config cfg; + +static bool is_initialized = false; +static struct CardInfo card_info[GAME_MAX_CARDS]; +static int card_info_size = 0; +static CRITICAL_SECTION card_info_lock; + +#pragma region y3-dll functions +uint16_t y3_io_get_api_version() { + return 0x0100; +} + +HRESULT y3_io_init() { + if (is_initialized) { + return S_FALSE; + } + + y3ws_config_load(&cfg, get_config_path()); + + memset(card_info, 0, sizeof(card_info)); + InitializeCriticalSection(&card_info_lock); + + wws_set_callbacks(onopen, onclose, onmessage, onlog); + wws_set_verbose(cfg.debug); + + HRESULT hr = wws_start(cfg.port); + + if (SUCCEEDED(hr)) { + dprintf("Y3WS: Started server on port %d\n", cfg.port); + if (cfg.debug) { + dprintf("Y3WS: WS debug logging is on\n"); + } + is_initialized = true; + } else { + dprintf("Y3WS: Error starting server: %lx\n", hr); + } + + return hr; +} + +HRESULT y3_io_close() { + + HRESULT hr = wws_stop(); + + DeleteCriticalSection(&card_info_lock); + + is_initialized = false; + + return hr; +} + +HRESULT y3_io_get_cards(struct CardInfo* pCardInfo, int* numCards) { + EnterCriticalSection(&card_info_lock); + for (int i = 0; i < card_info_size; i++) { + memcpy(&pCardInfo[i], &card_info[i], sizeof(struct CardInfo)); + } + *numCards = card_info_size; + LeaveCriticalSection(&card_info_lock); + return S_OK; +} +#pragma endregion + +#pragma region y3ws functions +static void y3ws_make_error_packet(const char* message, char* output_data, size_t* output_size) { + dprintf("Y3WS: Error: %s\n", message); + *output_size = sprintf_s((char*)output_data, *output_size, "{\"version\":%d, \"success\":false,\"error\":\"%s\"}", PROTOCOL_VERSION, message); +} + +static void y3ws_make_success_packet(char* output_data, size_t* output_size) { + *output_size = sprintf_s((char*)output_data, *output_size, "{\"version\":%d, \"success\":true}", PROTOCOL_VERSION); +} + +static void y3ws_read_cards(char* output_data, size_t* output_size) { + WIN32_FIND_DATAW ffd; + + wchar_t path[MAX_PATH]; + swprintf(path, MAX_PATH, L"%ls\\*", cfg.card_path); + + HANDLE hFind = FindFirstFileW(path, &ffd); + if (INVALID_HANDLE_VALUE == hFind) { + dprintf("Y3WS: Failed to access directory: %ls (%ld)\n", path, GetLastError()); + y3ws_make_error_packet("Could not read from card directory", output_data, output_size); + return; + } + + cJSON* response = cJSON_CreateObject(); + cJSON* pversion = cJSON_CreateNumber(PROTOCOL_VERSION); + cJSON_AddItemToObject(response, "version", pversion); + cJSON* success = cJSON_CreateBool(true); + cJSON_AddItemToObject(response, "success", success); + cJSON* cards = cJSON_CreateArray(); + cJSON_AddItemToObject(response, "cards", cards); + + int count = 0; + do { + if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + continue; + } + + swprintf(path, MAX_PATH, L"%ls\\%ls", cfg.card_path, ffd.cFileName); + + HANDLE hImage = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hImage == INVALID_HANDLE_VALUE) { + dprintf("Y3WS: Error opening %ls: %ld\n", path, GetLastError()); + continue; + } + DWORD ret = SetFilePointer(hImage, -CARD_ID_LEN, NULL, FILE_END); + if (ret == INVALID_SET_FILE_POINTER) { + dprintf("Y3WS: Error seeking in %ls: %ld\n", path, GetLastError()); + continue; + } + uint8_t buf[CARD_ID_LEN]; + DWORD bytesRead = 0; + if (!ReadFile(hImage, &buf, CARD_ID_LEN, &bytesRead, NULL) || bytesRead != CARD_ID_LEN) { + dprintf("Y3WS: Error reading card ID from %ls: %ld\n", path, GetLastError()); + continue; + } + + cJSON* card = cJSON_CreateObject(); + cJSON_AddItemToArray(cards, card); + + // cJSON isn't wide... + char fpatha[MAX_PATH]; + size_t fpatha_len = 0; + wcstombs_s(&fpatha_len, fpatha, MAX_PATH, ffd.cFileName, MAX_PATH); + fpatha[fpatha_len] = 0; + + // ReSharper disable once CppRedundantCastExpression + cJSON* path_str = cJSON_CreateString(fpatha); + cJSON_AddItemToObject(card, "path", path_str); + cJSON* card_id = cJSON_CreateNumber((double)((uint64_t)buf[0] >> 32 | buf[1] >> 24 | buf[2] >> 16 | buf[3] >> 8 | buf[4])); + cJSON_AddItemToObject(card, "card_id", card_id); + + } while (FindNextFileW(hFind, &ffd) != 0 && count++ < MAX_CARDS); + + cJSON_PrintPreallocated(response, output_data, (int)*output_size, false); + *output_size = strlen(output_data); + + dprintf("Y3WS: Sent %d card(s) to the client\n", count); + + cJSON_Delete(response); + FindClose(hFind); +} + +static void y3ws_read_card_image(char* file_name, char* output_data, size_t* output_size) { + wchar_t path[MAX_PATH]; + swprintf(path, MAX_PATH, L"%ls\\%s", cfg.card_path, file_name); + + HANDLE hImage = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hImage == INVALID_HANDLE_VALUE) { + dprintf("Y3WS: Failed to access file: %ls (%ld)\n", path, GetLastError()); + y3ws_make_error_packet("Could not read file", output_data, output_size); + return; + } + + DWORD size = GetFileSize(hImage, NULL); + if (size == INVALID_FILE_SIZE) { + dprintf("Y3WS: Failed to access file size: %ls (%ld)\n", path, GetLastError()); + y3ws_make_error_packet("Could not read file", output_data, output_size); + return; + } + + uint8_t* buf = malloc(size); + size_t b64size = ((size * 4) / 3) + (size / 96) + 6; + char* b64buf = malloc(b64size); + if (buf == NULL) { + dprintf("Y3WS: Allocation error for file: %ls (%ld)\n", path, GetLastError()); + y3ws_make_error_packet("Could not read file", output_data, output_size); + goto end; + } + + DWORD bytesRead = 0; + if (!ReadFile(hImage, buf, size, &bytesRead, NULL) || bytesRead != size) { + dprintf("Y3WS: File read failed: %ls (%ld)\n", path, GetLastError()); + y3ws_make_error_packet("Could not read file", output_data, output_size); + goto end; + } + + b64size = base64_encode(buf, size, b64buf); + + if (b64size + 100 > *output_size) { + dprintf("Y3WS: Encoded file size exceeds buffer: %ls (%lld / %lld)\n", path, b64size, *output_size); + y3ws_make_error_packet("File too large", output_data, output_size); + } + + *output_size = sprintf_s((char*)output_data, *output_size, "{\"version\":%d, \"success\":true,\"data\":\"%s\"}", PROTOCOL_VERSION, (char*)b64buf); +end: + free(buf); + free(b64buf); +} + +static void y3ws_set_cards_from_json(const cJSON* cards, char* output_data, size_t* output_size) { + + const cJSON* card = NULL; + card_info_size = 0; + + EnterCriticalSection(&card_info_lock); + memset(card_info, 0, sizeof(card_info)); + cJSON_ArrayForEach(card, cards) { + cJSON* x = cJSON_GetObjectItemCaseSensitive(card, "x"); + cJSON* y = cJSON_GetObjectItemCaseSensitive(card, "y"); + cJSON* r = cJSON_GetObjectItemCaseSensitive(card, "rotation"); + cJSON* id = cJSON_GetObjectItemCaseSensitive(card, "card_id"); + + if (cJSON_IsNumber(x) && cJSON_IsNumber(y) && cJSON_IsNumber(r) && cJSON_IsNumber(id)) { + card_info[card_info_size].eCardStatus = VALID; + card_info[card_info_size].fX = (float)x->valuedouble; + card_info[card_info_size].fY = (float)y->valuedouble; + card_info[card_info_size].fAngle = (float)r->valuedouble; + card_info[card_info_size].uID = id->valueint; + } else { + dprintf("Y3WS: Invalid object in card array at index %d\n", card_info_size); + } + + if (++card_info_size >= GAME_MAX_CARDS) { + dprintf("Y3WS: too many cards specified, truncating!\n"); + break; + } + + } + LeaveCriticalSection(&card_info_lock); + y3ws_make_success_packet(output_data, output_size); +} + +static void y3ws_handle(const char* input_data, uint32_t input_length, char* output_data, size_t* output_size) { + cJSON* json = cJSON_ParseWithLength(input_data, input_length); + if (json == NULL) { + const char *error_ptr = cJSON_GetErrorPtr(); + + dprintf("Y3WS: Invalid JSON received!\n"); + dprintf("Y3WS: Message was: %s\n", input_data); + dprintf("Y3WS: Error at: %s\n", error_ptr); + } + + const cJSON* ver = cJSON_GetObjectItemCaseSensitive(json, "version"); + const cJSON* cmd = cJSON_GetObjectItemCaseSensitive(json, "command"); + + if (!cJSON_IsNumber(ver) || ver->valueint <= 0) { + y3ws_make_error_packet("Missing version attribute", output_data, output_size); + goto end; + } + if (!cJSON_IsString(cmd)) { + y3ws_make_error_packet("Missing command attribute", output_data, output_size); + goto end; + } + if (ver->valueint != PROTOCOL_VERSION) { + y3ws_make_error_packet("Incompatible version", output_data, output_size); + goto end; + } + + if (strcmp(cmd->valuestring, "ping") == 0) { + y3ws_make_success_packet(output_data, output_size); + } else if (strcmp(cmd->valuestring, "get_game_id") == 0) { + *output_size = sprintf_s((char*)output_data, *output_size, "{\"version\":%d, \"success\":true,\"game_id\":\"%s\"}", PROTOCOL_VERSION, cfg.game_id); + } else if (strcmp(cmd->valuestring, "get_cards") == 0) { + y3ws_read_cards(output_data, output_size); + } else if (strcmp(cmd->valuestring, "get_card_image") == 0) { + const cJSON* path = cJSON_GetObjectItemCaseSensitive(json, "path"); + if (cJSON_IsString(path)) { + y3ws_read_card_image(path->valuestring, output_data, output_size); + } else { + y3ws_make_error_packet("Missing attribute", output_data, output_size); + } + } else if (strcmp(cmd->valuestring, "set_field") == 0) { + const cJSON* cards = cJSON_GetObjectItemCaseSensitive(json, "cards"); + if (cJSON_IsArray(cards)) { + y3ws_set_cards_from_json(cards, output_data, output_size); + } else { + y3ws_make_error_packet("Missing attribute", output_data, output_size); + } + } else { + y3ws_make_error_packet("Unknown command", output_data, output_size); + } + +end: + cJSON_Delete(json); +} +#pragma endregion + +#pragma region websocket callbacks + +static void onopen(struct wws_connection* client) { + dprintf("Y3WS: Connection opened, addr: %s\n", client->ip_str); +} + +static void onclose(struct wws_connection* client) { + dprintf("Y3WS: Connection closed, addr: %s\n", client->ip_str); +} + +static void onmessage(struct wws_connection* client, const char* msg, size_t size) { + if (cfg.debug) { + dprintf("Y3WS: Message: %.*s\n", (int)size, msg); + } + char* out_buf = malloc(OUTPUT_BUFFER_SIZE); + if (out_buf == NULL) { + dprintf("Y3WS: out of memory for allocating response buffer\n"); + client->is_connected = false; + return; + } + size_t out_size = OUTPUT_BUFFER_SIZE; + + y3ws_handle(msg, size, out_buf, &out_size); + HRESULT hr = wws_send(client, out_buf, out_size); + if (!SUCCEEDED(hr)) { + dprintf("Y3WS: Error sending message: %lx\n", hr); + } + + free(out_buf); +} + +static void onlog(const char* format, ...) { + va_list args; + va_start (args, format); + dprintf("Websocket: "); + dprintfv(format, args); + va_end (args); +} + +#pragma endregion \ No newline at end of file diff --git a/common/y3io/impl/websockets/y3ws.h b/common/y3io/impl/websockets/y3ws.h new file mode 100644 index 0000000..0b88e80 --- /dev/null +++ b/common/y3io/impl/websockets/y3ws.h @@ -0,0 +1,4 @@ +#pragma once + +#include +#include \ No newline at end of file diff --git a/common/y3io/meson.build b/common/y3io/meson.build index 3650512..c998949 100644 --- a/common/y3io/meson.build +++ b/common/y3io/meson.build @@ -6,12 +6,20 @@ y3io_lib = static_library( dependencies : [ capnhook.get_variable('hook_dep'), capnhook.get_variable('hooklib_dep'), + cwinwebsocket.get_variable('cws_dep'), ], link_with : [ util_lib, ], sources : [ - 'impl/dummy/y3io.c', + 'impl/websockets/config.c', + 'impl/websockets/config.h', + 'impl/websockets/y3ws.c', + 'impl/websockets/y3ws.h', + 'impl/websockets/3rdparty/cjson/cJSON.c', + 'impl/websockets/3rdparty/cjson/cJSON.h', + 'impl/websockets/3rdparty/cjson/cJSON_Utils.c', + 'impl/websockets/3rdparty/cjson/cJSON_Utils.h', 'impl/y3io.h', ], ) diff --git a/doc/y3ws.txt b/doc/y3ws.txt new file mode 100644 index 0000000..4e09801 --- /dev/null +++ b/doc/y3ws.txt @@ -0,0 +1,55 @@ +Y3WS websocket protocol for card I/O for the taisen series by Haruka: + +All packets are JSON and, unless otherwise specified contain these fields: + +* Requests: +{"version": 1, "command": "..."} + +* Responses: +{"version": 1, "success": , "error": } + +Commands: + +- ping + +Does nothing except answering with a success response. + + +- get_game_id + +Extra response fields: + - game_id: + +Returns the current game ID that is being run (SDDD, SDGY). + + +- get_cards + +Extra response fields: + - array of objects + - card_id: + - path: + +Returns all cards that the player has in possession. + + +- get_card_image + +Extra request fields: + - path: +Extra response fields: + - data: + +Returns the card image for the given path (format TBA). + + +- set_field + +Extra request fields: + - array of objects + - card_id: + - x: + - y: + - rotation: + +Sets the given cards onto the given positions on the field. Always replaces the entire board. diff --git a/meson.build b/meson.build index baec3c5..646d0c8 100644 --- a/meson.build +++ b/meson.build @@ -97,6 +97,7 @@ bcrypt_lib = cc.find_library('bcrypt') inc = include_directories('common', 'games') capnhook = subproject('capnhook') +cwinwebsocket = subproject('cwinwebsocket') subdir('common/amex') subdir('common/iccard') diff --git a/subprojects/cwinwebsocket.wrap b/subprojects/cwinwebsocket.wrap new file mode 100644 index 0000000..b23eb43 --- /dev/null +++ b/subprojects/cwinwebsocket.wrap @@ -0,0 +1,5 @@ +[wrap-git] +directory = cwinwebsocket +url = https://github.com/akechi-haruka/cwinwebsocket +revision = HEAD +