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

View file

@ -280,6 +280,10 @@ INIS sinit[]=
int last_ms_cursor=-1;
int vmode=2;
#include <platform/sse_receiver.h>
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;i<POCET_POSTAV;i++)postavy[i].sektor=loadlevel.start_pos;
const char *m = va_arg(msg->data, const char *);
char fname[13];
int sector;
int i;
if (sscanf(m, "RELOAD %12s %d", fname, &sector) != 2) return 0;
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);
unwire_proc();
if (battle) konec_kola();
@ -1751,6 +1795,10 @@ int skeldal_entry_point(const SKELDAL_CONFIG *start_cfg)
enable_achievements(1);
}
if (start_cfg->sse_hostport) {
sse_listener_init(start_cfg->sse_hostport);
}
start_check();
purge_temps(1);
clrscr();

View file

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

View file

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

View file

@ -19,6 +19,7 @@ void show_help(const char *arg0) {
"-a <adv> path for adventure file (.adv)\n"
"-l <lang> set language (cz|en)\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");
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()) {

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>
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 <adv> path for adventure file (.adv)\n"
"-l <lang> set language (cz|en)\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) {
@ -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();