From bb5be10adc81ccd91cd1f683401af3bb7d6dfd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Nov=C3=A1k?= Date: Wed, 7 May 2025 17:46:51 +0200 Subject: [PATCH] sse client to listen commands (from mapedit) --- game/globals.h | 2 +- game/skeldal.c | 68 +++++++++++++--- game/skeldal.h | 1 + platform/CMakeLists.txt | 24 +++--- platform/linux/app_start.cpp | 6 +- platform/mtqueue.cpp | 44 ++++++++++ platform/mtqueue.h | 32 ++++++++ platform/sse_receiver.cpp | 142 +++++++++++++++++++++++++++++++++ platform/sse_receiver.h | 30 +++++++ platform/windows/app_start.cpp | 16 ++-- 10 files changed, 336 insertions(+), 29 deletions(-) create mode 100644 platform/mtqueue.cpp create mode 100644 platform/mtqueue.h create mode 100644 platform/sse_receiver.cpp create mode 100644 platform/sse_receiver.h diff --git a/game/globals.h b/game/globals.h index ec3fe1d..66658db 100644 --- a/game/globals.h +++ b/game/globals.h @@ -39,7 +39,7 @@ #define E_MENU_SELECT 36 #define E_CLOSE_MAP 37 #define E_CLOSE_GEN 38 -#define E_HACKER 39 +#define E_EXTERNAL_MSG 40 //external message arrived from sse_listener //side flags diff --git a/game/skeldal.c b/game/skeldal.c index ee1b6ec..c289f50 100644 --- a/game/skeldal.c +++ b/game/skeldal.c @@ -280,6 +280,10 @@ INIS sinit[]= int last_ms_cursor=-1; int vmode=2; +#include + +static SSE_RECEIVER *sse_receiver = NULL; +static MTQUEUE *mtqueue = NULL; void purge_temps(char _) { temp_storage_clear(); @@ -741,7 +745,8 @@ void done_skeldal(void) cur_config = NULL; } kill_timer(); - + if (sse_receiver) sse_receiver_stop(sse_receiver); + if (mtqueue) mtqueue_destroy(mtqueue); } @@ -980,6 +985,38 @@ void show_loading_picture(char *filename) ablock_free(p); } +void sse_listener_watch(EVENT_MSG *msg, void **userdata) { + if (msg->msg == E_WATCH) { + char *s = mtqueue_pop(mtqueue); + if (s) { + send_message(E_EXTERNAL_MSG, s); + free(s); + } + } +} +void sse_listener_init(const char *hostport) { + + char *host = local_strdup(hostport); + char *port = strrchr(host,':'); + if (port == NULL) { + port = local_strdup("80"); + } else { + *port = 0; + ++port; + } + + MTQUEUE *q = mtqueue_create(); + SSE_RECEIVER *rcv = sse_receiver_install(q, host, port); + + if (rcv == NULL) { + mtqueue_destroy(q); + return; + } + mtqueue = q; + sse_receiver = rcv; + send_message(E_ADD, E_WATCH, sse_listener_watch); + +} void init_skeldal(const INI_CONFIG *cfg) { @@ -989,10 +1026,11 @@ void init_skeldal(const INI_CONFIG *cfg) cti_texty(); timer_tree.next=NULL; - init_events(); + init_events(); steam_init(); + char verr = game_display_init(ini_section_open(cfg, "video"), "Skeldal"); if (!verr) { @@ -1120,15 +1158,21 @@ char doNotLoadMapState=0; static int reload_map_handler(EVENT_MSG *msg,void **usr) { extern char running_battle; - if (msg->msg==E_RELOADMAP) + if (msg->msg==E_EXTERNAL_MSG) { - int i; - ReloadMapInfo *minfo=va_arg(msg->data, ReloadMapInfo *); - const char *fname=minfo->fname; - int sektor=minfo->sektor; - strcopy_n(loadlevel.name,fname,sizeof(loadlevel.name)); - loadlevel.start_pos=sektor; - for(i=0;idata, const char *); + char fname[13]; + int sector; + int i; + + if (sscanf(m, "RELOAD %12s %d", fname, §or) != 2) return 0; + + strcopy_n(loadlevel.name,fname,sizeof(loadlevel.name)); + loadlevel.start_pos=sector; + for(i=0;isse_hostport) { + sse_listener_init(start_cfg->sse_hostport); + } + start_check(); purge_temps(1); clrscr(); diff --git a/game/skeldal.h b/game/skeldal.h index baf737c..ce44873 100644 --- a/game/skeldal.h +++ b/game/skeldal.h @@ -13,6 +13,7 @@ typedef struct { const char *config_path; const char *lang_path; + const char *sse_hostport; } SKELDAL_CONFIG; diff --git a/platform/CMakeLists.txt b/platform/CMakeLists.txt index 343a8e2..90cde4f 100644 --- a/platform/CMakeLists.txt +++ b/platform/CMakeLists.txt @@ -12,17 +12,19 @@ target_sources(skeldal_platform PRIVATE timer.cpp getopt.c achievements.cpp + mtqueue.cpp + sse_receiver.cpp ) -set(all_libs - skeldal_main - skeldal_libs +set(all_libs + skeldal_main + skeldal_libs skeldal_platform skeldal_sdl skeldal_libs ${SDL2_LIBRARIES} ${STANDARD_LIBRARIES}) - + if(WIN32) target_sources(skeldal_platform PRIVATE windows/save_folder.cpp @@ -35,7 +37,7 @@ if(WIN32) windows/skeldal.rc ) target_compile_definitions(skeldal_platform PRIVATE PLATFORM_WINDOWS) - if(STEAM_ENABLED) + if(STEAM_ENABLED) if(CMAKE_SIZEOF_VOID_P EQUAL 8) # 64-bit set(STEAMLIB ${STEAMWORKS_SDK_DIR}/redistributable_bin/win64/steam_api64.lib) @@ -49,7 +51,7 @@ if(WIN32) set(STEAMLIB "") set(STEAMDLL "") endif() - + target_link_libraries(skeldal ${all_libs} ${STEAMLIB}) if(STEAMDLL) @@ -69,7 +71,7 @@ elseif(UNIX AND NOT APPLE) linux/app_start.cpp ) target_compile_definitions(skeldal_bin PRIVATE PLATFORM_LINUX) - if(STEAM_ENABLED) + if(STEAM_ENABLED) if(CMAKE_SIZEOF_VOID_P EQUAL 8) # 64-bit set(STEAMLIB ${STEAMWORKS_SDK_DIR}/redistributable_bin/linux64/libsteam_api.so) @@ -83,10 +85,10 @@ elseif(UNIX AND NOT APPLE) COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/linux/skeldal.sh ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/skeldal.sh) - target_link_libraries(skeldal_bin ${all_libs} ${STEAMLIB}) + target_link_libraries(skeldal_bin ${all_libs} ${STEAMLIB}) message(STATUS "Building for Linux") -elseif(APPLE) +elseif(APPLE) target_sources(skeldal_platform PRIVATE mac_os/save_folder.cpp ) @@ -96,10 +98,10 @@ elseif(APPLE) ) target_compile_definitions(mylib PRIVATE PLATFORM_MACOS) set(STEAMLIB ${STEAMWORKS_SDK_DIR}/redistributable_bin/osx/libsteam_api.dylib) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations") message(STATUS "Building for macOS") target_link_libraries(skeldal ${all_libs} ${STEAMLIB}) -else() +else() error("Platform not detected, please add new platform here") endif() set_property(TARGET skeldal_platform PROPERTY CXX_STANDARD 20) diff --git a/platform/linux/app_start.cpp b/platform/linux/app_start.cpp index 7ec40d4..760efd1 100644 --- a/platform/linux/app_start.cpp +++ b/platform/linux/app_start.cpp @@ -19,6 +19,7 @@ void show_help(const char *arg0) { "-a path for adventure file (.adv)\n" "-l set language (cz|en)\n" "-s generate string-tables (for localization) and exit\n" + "-L connect to host:port to listen commands (mapedit)\n" "-h this help\n"); exit(1); } @@ -33,13 +34,15 @@ int main(int argc, char **argv) { std::string adv_config_file; std::string gen_stringtable_path; std::string lang; - for (int optchr = -1; (optchr = getopt(argc, argv, "hf:a:s:l:")) != -1; ) { + std::string sse_hostport; + for (int optchr = -1; (optchr = getopt(argc, argv, "hf:a:s:l:L:")) != -1; ) { switch (optchr) { case 'f': config_name = optarg;break; case 'a': adv_config_file = optarg;break; case 'h': show_help(argv[0]);break; case 'l': lang = optarg;break; case 's': gen_stringtable_path = optarg;break; + case 'L': sse_hostport = optarg;break; default: show_help_short(); return 1; } @@ -53,6 +56,7 @@ int main(int argc, char **argv) { cfg.adventure_path = adv_config_file.empty()?NULL:adv_config_file.c_str(); cfg.config_path = config_name.c_str(); cfg.lang_path = lang.empty()?NULL:lang.c_str(); + cfg.sse_hostport = sse_hostport.c_str(); try { if (!gen_stringtable_path.empty()) { diff --git a/platform/mtqueue.cpp b/platform/mtqueue.cpp new file mode 100644 index 0000000..bc9d23d --- /dev/null +++ b/platform/mtqueue.cpp @@ -0,0 +1,44 @@ +#include "mtqueue.h" + +#include +#include +#include +#include +#include +struct StringDeleter { + void operator()(char *x) { + free(x); + } +}; + +std::unique_ptr alloc_string(const char *x) { + return std::unique_ptr(strdup(x)); +} + + +typedef struct tag_mtqueue { + std::queue > _q; + std::mutex _mx; + + +} MTQUEUE; + +MTQUEUE *mtqueue_create() { + return new MTQUEUE(); +} +void mtqueue_push(MTQUEUE *q, const char *message) { + std::lock_guard _(q->_mx); + q->_q.push(alloc_string(message)); +} +char *mtqueue_pop(MTQUEUE *q) { + std::lock_guard _(q->_mx); + if (q->_q.empty()) return NULL; + else { + char *c = q->_q.front().release(); + q->_q.pop(); + return c; + } +} +void mtqueue_destroy(MTQUEUE *q) { + delete q; +} diff --git a/platform/mtqueue.h b/platform/mtqueue.h new file mode 100644 index 0000000..07919e0 --- /dev/null +++ b/platform/mtqueue.h @@ -0,0 +1,32 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + + +typedef struct tag_mtqueue MTQUEUE; +///Create multithread queue +MTQUEUE *mtqueue_create(); +///push to queue (string is copied) +/** + * @param q queue + * @param message message (string is copied) + */ +void mtqueue_push(MTQUEUE *q, const char *message); +///pop from the queue +/** + * + * @param q queue + * @return NULL, if queue is empty, or string. You have to release + * string by calling free() when you finish. + */ +char *mtqueue_pop(MTQUEUE *q); + +///destroy the queue +void mtqueue_destroy(MTQUEUE *q); + + +#ifdef __cplusplus +} +#endif diff --git a/platform/sse_receiver.cpp b/platform/sse_receiver.cpp new file mode 100644 index 0000000..f043e83 --- /dev/null +++ b/platform/sse_receiver.cpp @@ -0,0 +1,142 @@ +#include "sse_receiver.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include + #include + #pragma comment(lib, "ws2_32.lib") + typedef SOCKET sock_t; + #define CLOSESOCK closesocket + #define sock_init() { WSADATA wsa; WSAStartup(MAKEWORD(2,2), &wsa); } + #define sock_cleanup() WSACleanup() + #define SHUT_RD SD_RECEIVE +#else + #include + #include + #include + #include + #include + typedef int sock_t; + #define INVALID_SOCKET -1 + #define CLOSESOCK close + #define sock_init() + #define sock_cleanup() +#endif + +#define BUFFER_SIZE 1024 + + +void sse_client_loop(const char *host, const char *port, std::function callback, std::stop_token tkn) { + sock_init(); + + struct addrinfo hints = {}, *res = NULL; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + if (getaddrinfo(host, port, &hints, &res) != 0) { + perror("getaddrinfo"); + return; + } + + sock_t sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sock == INVALID_SOCKET) { + perror("socket"); + freeaddrinfo(res); + return; + } + + + + if (connect(sock, res->ai_addr, res->ai_addrlen) != 0) { + perror("connect"); + CLOSESOCK(sock); + freeaddrinfo(res); + return; + } + + + freeaddrinfo(res); + + { + std::stop_callback _cb(tkn, [&]{ + shutdown(sock, SHUT_RD); + }); + + // Send HTTP GET request + char req[512]; + snprintf(req, sizeof(req), + "GET /command HTTP/1.1\r\n" + "Host: %s\r\n" + "Accept: text/event-stream\r\n" + "Connection: keep-alive\r\n\r\n", host); + send(sock, req, strlen(req), 0); + + // Read response and extract "data: " lines + char buffer[BUFFER_SIZE]; + int buf_len = 0; + + while (!tkn.stop_requested()) { + int n = recv(sock, buffer + buf_len, BUFFER_SIZE - buf_len - 1, 0); + if (n <= 0) { + break; + } + + buf_len += n; + buffer[buf_len] = '\0'; + + char *line_start = buffer; + while (1) { + char *newline = strstr(line_start, "\n"); + if (!newline) break; + + *newline = '\0'; + + if (strncmp(line_start, "data: ", 6) == 0) { + callback(line_start + 6); + } + + line_start = newline + 1; + } + + // Move leftover data to start + buf_len = strlen(line_start); + memmove(buffer, line_start, buf_len); + } + + } + CLOSESOCK(sock); + sock_cleanup(); + return; +} + + + + +typedef struct tag_sse_receiver { + std::jthread thr; +} +SSE_RECEIVER; + +SSE_RECEIVER *sse_receiver_install(MTQUEUE *q, const char *host, const char *port) { + SSE_RECEIVER *sse = new SSE_RECEIVER; + sse->thr = std::jthread([sse, q, host = std::string(host), port = std::string(port)](std::stop_token tkn){ + while (!tkn.stop_requested()) { + std::this_thread::sleep_for(std::chrono::seconds(2)); + sse_client_loop(host.c_str(), port.c_str(), [q](const char *msg){ + mtqueue_push(q, msg); + }, tkn); + } + }); + return sse; +} +void sse_receiver_stop(SSE_RECEIVER *inst) { + delete inst; +} + diff --git a/platform/sse_receiver.h b/platform/sse_receiver.h new file mode 100644 index 0000000..044c5a7 --- /dev/null +++ b/platform/sse_receiver.h @@ -0,0 +1,30 @@ +#pragma once + +#include "mtqueue.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct tag_sse_receiver SSE_RECEIVER; + +///Install sse receiver +/** + * @param q mtqueue, which receives messages received by the receiver + * @param host host + * @param port port + * @return pointer to instance of receiver + */ +SSE_RECEIVER *sse_receiver_install(MTQUEUE *q, const char *host, const char *port); + +///Stops the receiver +/** + * @param inst instance of receiver + * @note the associated queue is not destroyed + */ +void sse_receiver_stop(SSE_RECEIVER *inst); + + +#ifdef __cplusplus +} +#endif diff --git a/platform/windows/app_start.cpp b/platform/windows/app_start.cpp index 8ecc5a9..64903a4 100644 --- a/platform/windows/app_start.cpp +++ b/platform/windows/app_start.cpp @@ -10,7 +10,7 @@ #include void show_help(std::ostream &out, const char *arg0) { - out << + out << "Brany Skeldalu (Gates of Skeldal) portable game player\n" "Copyright (c) 2025 Ondrej Novak. All rights reserved.\n\n" "This work is licensed under the terms of the MIT license.\n" @@ -22,7 +22,8 @@ void show_help(std::ostream &out, const char *arg0) { "-a path for adventure file (.adv)\n" "-l set language (cz|en)\n" "-s generate string-tables (for localization) and exit\n" - "-h this help\n"; + "-L connect to host:port to listen commands (mapedit)\n" + "-h this help\n"; } void show_help_short(std::ostream &out) { @@ -30,20 +31,22 @@ void show_help_short(std::ostream &out) { } -int main(int argc, char **argv) { +int main(int argc, char **argv) { std::string config_name = SKELDALINI; std::string adv_config_file; std::string gen_stringtable_path; std::string lang; + std::string sse_hostport; std::ostringstream console; - for (int optchr = -1; (optchr = getopt(argc, argv, "hf:a:s:l:")) != -1; ) { + for (int optchr = -1; (optchr = getopt(argc, argv, "hf:a:s:l:L:")) != -1; ) { switch (optchr) { case 'f': config_name = optarg;break; case 'a': adv_config_file = optarg;break; case 'h': show_help(console, argv[0]);break; case 'l': lang = optarg;break; case 's': gen_stringtable_path = optarg;break; - default: show_help_short(console);break; + case 'L': sse_hostport = optarg;break; + default: show_help_short(console);break; } } @@ -52,7 +55,7 @@ int main(int argc, char **argv) { show_help(console, argv[0]); } - SKELDAL_CONFIG cfg; + SKELDAL_CONFIG cfg = {}; cfg.short_help = []{}; cfg.show_error = [](const char *txt) { char buff[MAX_PATH]; @@ -63,6 +66,7 @@ int main(int argc, char **argv) { cfg.adventure_path = adv_config_file.empty()?NULL:adv_config_file.c_str(); cfg.config_path = config_name.c_str(); cfg.lang_path = lang.empty()?NULL:lang.c_str(); + cfg.sse_hostport = sse_hostport.c_str(); { std::string msg = console.str();