sse client to listen commands (from mapedit)

This commit is contained in:
Ondřej Novák 2025-05-07 17:46:51 +02:00
parent 13f6c05c60
commit bb5be10adc
10 changed files with 336 additions and 29 deletions

View file

@ -39,7 +39,7 @@
#define E_MENU_SELECT 36 #define E_MENU_SELECT 36
#define E_CLOSE_MAP 37 #define E_CLOSE_MAP 37
#define E_CLOSE_GEN 38 #define E_CLOSE_GEN 38
#define E_HACKER 39 #define E_EXTERNAL_MSG 40 //external message arrived from sse_listener
//side flags //side flags

View file

@ -280,6 +280,10 @@ INIS sinit[]=
int last_ms_cursor=-1; int last_ms_cursor=-1;
int vmode=2; int vmode=2;
#include <platform/sse_receiver.h>
static SSE_RECEIVER *sse_receiver = NULL;
static MTQUEUE *mtqueue = NULL;
void purge_temps(char _) { void purge_temps(char _) {
temp_storage_clear(); temp_storage_clear();
@ -741,7 +745,8 @@ void done_skeldal(void)
cur_config = NULL; cur_config = NULL;
} }
kill_timer(); 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); 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) void init_skeldal(const INI_CONFIG *cfg)
{ {
@ -989,10 +1026,11 @@ void init_skeldal(const INI_CONFIG *cfg)
cti_texty(); cti_texty();
timer_tree.next=NULL; timer_tree.next=NULL;
init_events(); init_events();
steam_init(); steam_init();
char verr = game_display_init(ini_section_open(cfg, "video"), "Skeldal"); char verr = game_display_init(ini_section_open(cfg, "video"), "Skeldal");
if (!verr) if (!verr)
{ {
@ -1120,15 +1158,21 @@ char doNotLoadMapState=0;
static int reload_map_handler(EVENT_MSG *msg,void **usr) static int reload_map_handler(EVENT_MSG *msg,void **usr)
{ {
extern char running_battle; extern char running_battle;
if (msg->msg==E_RELOADMAP) if (msg->msg==E_EXTERNAL_MSG)
{ {
int i; const char *m = va_arg(msg->data, const char *);
ReloadMapInfo *minfo=va_arg(msg->data, ReloadMapInfo *); char fname[13];
const char *fname=minfo->fname; int sector;
int sektor=minfo->sektor; int i;
strcopy_n(loadlevel.name,fname,sizeof(loadlevel.name));
loadlevel.start_pos=sektor; if (sscanf(m, "RELOAD %12s %d", fname, &sector) != 2) return 0;
for(i=0;i<POCET_POSTAV;i++)postavy[i].sektor=loadlevel.start_pos;
strcopy_n(loadlevel.name,fname,sizeof(loadlevel.name));
loadlevel.start_pos=sector;
for(i=0;i<POCET_POSTAV;i++) {
postavy[i].sektor=loadlevel.start_pos;
postavy[i].groupnum = 1;
}
SEND_LOG("(WIZARD) Load map '%s' %d",loadlevel.name,loadlevel.start_pos); SEND_LOG("(WIZARD) Load map '%s' %d",loadlevel.name,loadlevel.start_pos);
unwire_proc(); unwire_proc();
if (battle) konec_kola(); if (battle) konec_kola();
@ -1751,6 +1795,10 @@ int skeldal_entry_point(const SKELDAL_CONFIG *start_cfg)
enable_achievements(1); enable_achievements(1);
} }
if (start_cfg->sse_hostport) {
sse_listener_init(start_cfg->sse_hostport);
}
start_check(); start_check();
purge_temps(1); purge_temps(1);
clrscr(); clrscr();

View file

@ -13,6 +13,7 @@ typedef struct {
const char *config_path; const char *config_path;
const char *lang_path; const char *lang_path;
const char *sse_hostport;
} SKELDAL_CONFIG; } SKELDAL_CONFIG;

View file

@ -12,17 +12,19 @@ target_sources(skeldal_platform PRIVATE
timer.cpp timer.cpp
getopt.c getopt.c
achievements.cpp achievements.cpp
mtqueue.cpp
sse_receiver.cpp
) )
set(all_libs set(all_libs
skeldal_main skeldal_main
skeldal_libs skeldal_libs
skeldal_platform skeldal_platform
skeldal_sdl skeldal_sdl
skeldal_libs skeldal_libs
${SDL2_LIBRARIES} ${SDL2_LIBRARIES}
${STANDARD_LIBRARIES}) ${STANDARD_LIBRARIES})
if(WIN32) if(WIN32)
target_sources(skeldal_platform PRIVATE target_sources(skeldal_platform PRIVATE
windows/save_folder.cpp windows/save_folder.cpp
@ -35,7 +37,7 @@ if(WIN32)
windows/skeldal.rc windows/skeldal.rc
) )
target_compile_definitions(skeldal_platform PRIVATE PLATFORM_WINDOWS) target_compile_definitions(skeldal_platform PRIVATE PLATFORM_WINDOWS)
if(STEAM_ENABLED) if(STEAM_ENABLED)
if(CMAKE_SIZEOF_VOID_P EQUAL 8) if(CMAKE_SIZEOF_VOID_P EQUAL 8)
# 64-bit # 64-bit
set(STEAMLIB ${STEAMWORKS_SDK_DIR}/redistributable_bin/win64/steam_api64.lib) set(STEAMLIB ${STEAMWORKS_SDK_DIR}/redistributable_bin/win64/steam_api64.lib)
@ -49,7 +51,7 @@ if(WIN32)
set(STEAMLIB "") set(STEAMLIB "")
set(STEAMDLL "") set(STEAMDLL "")
endif() endif()
target_link_libraries(skeldal ${all_libs} ${STEAMLIB}) target_link_libraries(skeldal ${all_libs} ${STEAMLIB})
if(STEAMDLL) if(STEAMDLL)
@ -69,7 +71,7 @@ elseif(UNIX AND NOT APPLE)
linux/app_start.cpp linux/app_start.cpp
) )
target_compile_definitions(skeldal_bin PRIVATE PLATFORM_LINUX) target_compile_definitions(skeldal_bin PRIVATE PLATFORM_LINUX)
if(STEAM_ENABLED) if(STEAM_ENABLED)
if(CMAKE_SIZEOF_VOID_P EQUAL 8) if(CMAKE_SIZEOF_VOID_P EQUAL 8)
# 64-bit # 64-bit
set(STEAMLIB ${STEAMWORKS_SDK_DIR}/redistributable_bin/linux64/libsteam_api.so) 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 COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_LIST_DIR}/linux/skeldal.sh ${CMAKE_CURRENT_LIST_DIR}/linux/skeldal.sh
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/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") message(STATUS "Building for Linux")
elseif(APPLE) elseif(APPLE)
target_sources(skeldal_platform PRIVATE target_sources(skeldal_platform PRIVATE
mac_os/save_folder.cpp mac_os/save_folder.cpp
) )
@ -96,10 +98,10 @@ elseif(APPLE)
) )
target_compile_definitions(mylib PRIVATE PLATFORM_MACOS) target_compile_definitions(mylib PRIVATE PLATFORM_MACOS)
set(STEAMLIB ${STEAMWORKS_SDK_DIR}/redistributable_bin/osx/libsteam_api.dylib) 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") message(STATUS "Building for macOS")
target_link_libraries(skeldal ${all_libs} ${STEAMLIB}) target_link_libraries(skeldal ${all_libs} ${STEAMLIB})
else() else()
error("Platform not detected, please add new platform here") error("Platform not detected, please add new platform here")
endif() endif()
set_property(TARGET skeldal_platform PROPERTY CXX_STANDARD 20) set_property(TARGET skeldal_platform PROPERTY CXX_STANDARD 20)

View file

@ -19,6 +19,7 @@ void show_help(const char *arg0) {
"-a <adv> path for adventure file (.adv)\n" "-a <adv> path for adventure file (.adv)\n"
"-l <lang> set language (cz|en)\n" "-l <lang> set language (cz|en)\n"
"-s <directory> generate string-tables (for localization) and exit\n" "-s <directory> generate string-tables (for localization) and exit\n"
"-L <host:port> connect to host:port to listen commands (mapedit)\n"
"-h this help\n"); "-h this help\n");
exit(1); exit(1);
} }
@ -33,13 +34,15 @@ int main(int argc, char **argv) {
std::string adv_config_file; std::string adv_config_file;
std::string gen_stringtable_path; std::string gen_stringtable_path;
std::string lang; 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) { switch (optchr) {
case 'f': config_name = optarg;break; case 'f': config_name = optarg;break;
case 'a': adv_config_file = optarg;break; case 'a': adv_config_file = optarg;break;
case 'h': show_help(argv[0]);break; case 'h': show_help(argv[0]);break;
case 'l': lang = optarg;break; case 'l': lang = optarg;break;
case 's': gen_stringtable_path = optarg;break; case 's': gen_stringtable_path = optarg;break;
case 'L': sse_hostport = optarg;break;
default: show_help_short(); default: show_help_short();
return 1; 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.adventure_path = adv_config_file.empty()?NULL:adv_config_file.c_str();
cfg.config_path = config_name.c_str(); cfg.config_path = config_name.c_str();
cfg.lang_path = lang.empty()?NULL:lang.c_str(); cfg.lang_path = lang.empty()?NULL:lang.c_str();
cfg.sse_hostport = sse_hostport.c_str();
try { try {
if (!gen_stringtable_path.empty()) { if (!gen_stringtable_path.empty()) {

44
platform/mtqueue.cpp Normal file
View file

@ -0,0 +1,44 @@
#include "mtqueue.h"
#include <cstring>
#include <malloc.h>
#include <memory>
#include <mutex>
#include <queue>
struct StringDeleter {
void operator()(char *x) {
free(x);
}
};
std::unique_ptr<char, StringDeleter> alloc_string(const char *x) {
return std::unique_ptr<char, StringDeleter>(strdup(x));
}
typedef struct tag_mtqueue {
std::queue<std::unique_ptr<char, StringDeleter> > _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;
}

32
platform/mtqueue.h Normal file
View file

@ -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

142
platform/sse_receiver.cpp Normal file
View file

@ -0,0 +1,142 @@
#include "sse_receiver.h"
#include <atomic>
#include <thread>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <functional>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#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 <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <fcntl.h>
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<void(const char *)> 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;
}

30
platform/sse_receiver.h Normal file
View file

@ -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

View file

@ -10,7 +10,7 @@
#include <shellapi.h> #include <shellapi.h>
void show_help(std::ostream &out, const char *arg0) { void show_help(std::ostream &out, const char *arg0) {
out << out <<
"Brany Skeldalu (Gates of Skeldal) portable game player\n" "Brany Skeldalu (Gates of Skeldal) portable game player\n"
"Copyright (c) 2025 Ondrej Novak. All rights reserved.\n\n" "Copyright (c) 2025 Ondrej Novak. All rights reserved.\n\n"
"This work is licensed under the terms of the MIT license.\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 <adv> path for adventure file (.adv)\n" "-a <adv> path for adventure file (.adv)\n"
"-l <lang> set language (cz|en)\n" "-l <lang> set language (cz|en)\n"
"-s <directory> generate string-tables (for localization) and exit\n" "-s <directory> generate string-tables (for localization) and exit\n"
"-h this help\n"; "-L <host:port> connect to host:port to listen commands (mapedit)\n"
"-h this help\n";
} }
void show_help_short(std::ostream &out) { 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 config_name = SKELDALINI;
std::string adv_config_file; std::string adv_config_file;
std::string gen_stringtable_path; std::string gen_stringtable_path;
std::string lang; std::string lang;
std::string sse_hostport;
std::ostringstream console; 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) { switch (optchr) {
case 'f': config_name = optarg;break; case 'f': config_name = optarg;break;
case 'a': adv_config_file = optarg;break; case 'a': adv_config_file = optarg;break;
case 'h': show_help(console, argv[0]);break; case 'h': show_help(console, argv[0]);break;
case 'l': lang = optarg;break; case 'l': lang = optarg;break;
case 's': gen_stringtable_path = 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]); show_help(console, argv[0]);
} }
SKELDAL_CONFIG cfg; SKELDAL_CONFIG cfg = {};
cfg.short_help = []{}; cfg.short_help = []{};
cfg.show_error = [](const char *txt) { cfg.show_error = [](const char *txt) {
char buff[MAX_PATH]; 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.adventure_path = adv_config_file.empty()?NULL:adv_config_file.c_str();
cfg.config_path = config_name.c_str(); cfg.config_path = config_name.c_str();
cfg.lang_path = lang.empty()?NULL:lang.c_str(); cfg.lang_path = lang.empty()?NULL:lang.c_str();
cfg.sse_hostport = sse_hostport.c_str();
{ {
std::string msg = console.str(); std::string msg = console.str();