diff --git a/game/globals.h b/game/globals.h index 7bfa0c1..8214d1e 100644 --- a/game/globals.h +++ b/game/globals.h @@ -44,7 +44,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 2df3c97..6ded63a 100644 --- a/game/skeldal.c +++ b/game/skeldal.c @@ -281,6 +281,10 @@ INIS sinit[]= int last_ms_cursor=-1; int vmode=2; +#include + +static SSE_RECEIVER *sse_receiver = NULL; + void purge_temps(char _) { temp_storage_clear(); @@ -740,7 +744,7 @@ void done_skeldal(void) cur_config = NULL; } kill_timer(); - + if (sse_receiver) sse_receiver_destroy(sse_receiver); } @@ -960,6 +964,34 @@ char end_of_song_callback(void *, TMUSIC_SOURCE *s, TMUSIC_SOURCE_TYPE *t) { } +void sse_listener_watch(EVENT_MSG *msg, void **userdata) { + if (msg->msg == E_WATCH) { + const char *s = sse_receiver_receive(sse_receiver); + if (s) { + send_message(E_EXTERNAL_MSG, 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; + } + + SSE_RECEIVER *rcv = sse_receiver_create(host, port); + + if (rcv == NULL) { + return; + } + sse_receiver = rcv; + send_message(E_ADD, E_WATCH, sse_listener_watch); + +} void init_skeldal(const INI_CONFIG *cfg) { @@ -973,6 +1005,7 @@ void init_skeldal(const INI_CONFIG *cfg) steam_init(); + char verr = game_display_init(ini_section_open(cfg, "video"), "Skeldal"); if (!verr) { @@ -980,7 +1013,7 @@ void init_skeldal(const INI_CONFIG *cfg) exit(1); } showview = game_display_update_rect; - game_display_set_icon(getWindowIcon(), getWindowIconSize()); +// game_display_set_icon(getWindowIcon(), getWindowIconSize()); init_joystick(ini_section_open(cfg, "controller")); general_engine_init(); @@ -1090,29 +1123,39 @@ 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) { + + 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 a2308dc..c2e12e7 100644 --- a/game/skeldal.h +++ b/game/skeldal.h @@ -14,6 +14,7 @@ typedef struct { const char *lang_path; const char *patch_file; + const char *sse_hostport; } SKELDAL_CONFIG; diff --git a/platform/CMakeLists.txt b/platform/CMakeLists.txt index d9daadb..5d6b058 100644 --- a/platform/CMakeLists.txt +++ b/platform/CMakeLists.txt @@ -12,17 +12,18 @@ target_sources(skeldal_platform PRIVATE timer.cpp getopt.c achievements.cpp + sse_receiver.c ) -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 +36,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 +50,7 @@ if(WIN32) set(STEAMLIB "") set(STEAMDLL "") endif() - + target_link_libraries(skeldal ${all_libs} ${STEAMLIB}) if(STEAMDLL) @@ -69,7 +70,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 +84,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 ) @@ -100,7 +101,7 @@ elseif(APPLE) 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 cfa4c47..210ed8a 100644 --- a/platform/linux/app_start.cpp +++ b/platform/linux/app_start.cpp @@ -21,6 +21,7 @@ void show_help(const char *arg0) { "-p patch data with custom DDL\n" "-l set language (cz|en)\n" "-s generate string-tables (for localization) and exit\n" + "-c connect to host:port for remote control (mapedit)\n" "-h this help\n"); exit(1); } @@ -36,7 +37,8 @@ int main(int argc, char **argv) { std::string gen_stringtable_path; std::string patch; std::string lang; - for (int optchr = -1; (optchr = getopt(argc, argv, "hf:a:s:l:p:")) != -1; ) { + std::string sse_hostport; + for (int optchr = -1; (optchr = getopt(argc, argv, "hf:a:s:l:p:c:")) != -1; ) { switch (optchr) { case 'f': config_name = optarg;break; case 'a': adv_config_file = optarg;break; @@ -44,6 +46,7 @@ int main(int argc, char **argv) { case 'p': patch = optarg; break; case 'l': lang = optarg;break; case 's': gen_stringtable_path = optarg;break; + case 'c': sse_hostport = optarg;break; default: show_help_short(); return 1; } @@ -58,6 +61,7 @@ int main(int argc, char **argv) { cfg.config_path = config_name.c_str(); cfg.lang_path = lang.empty()?NULL:lang.c_str(); cfg.patch_file = patch.empty()?NULL:patch.c_str(); + cfg.sse_hostport = sse_hostport.c_str(); try { if (!gen_stringtable_path.empty()) { diff --git a/platform/sse_receiver.c b/platform/sse_receiver.c new file mode 100644 index 0000000..50d5cdd --- /dev/null +++ b/platform/sse_receiver.c @@ -0,0 +1,183 @@ +// sse_receiver.c +#include "sse_receiver.h" + +#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() +#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 4096 + +struct tag_sse_receiver { + char host[256]; + char port[16]; + sock_t sock; + int connected; + char buffer[BUFFER_SIZE+1]; + size_t buf_len; + size_t line_len; + time_t next_attempt; +}; + +static void set_nonblocking(sock_t sock) { +#ifdef _WIN32 + u_long mode = 1; + ioctlsocket(sock, FIONBIO, &mode); +#else + fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK); +#endif +} + +static int connect_and_handshake(SSE_RECEIVER *sse) { + struct addrinfo hints = {0}, *res = NULL; + + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + if (getaddrinfo(sse->host, sse->port, &hints, &res) != 0) { + return 0; + } + + sock_t sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sock == INVALID_SOCKET) { + freeaddrinfo(res); + return 0; + } + + if (connect(sock, res->ai_addr, (int)res->ai_addrlen) != 0) { + CLOSESOCK(sock); + freeaddrinfo(res); + return 0; + } + + freeaddrinfo(res); + + const char *req_fmt = + "GET /command HTTP/1.1\r\n" + "Host: %s\r\n" + "Accept: text/event-stream\r\n" + "Connection: keep-alive\r\n\r\n"; + + char req[512]; + snprintf(req, sizeof(req), req_fmt, sse->host); + send(sock, req, (int)strlen(req), 0); + + set_nonblocking(sock); + sse->sock = sock; + sse->buf_len = 0; + sse->line_len = 0; + sse->connected = 1; + + return 1; +} + +SSE_RECEIVER *sse_receiver_create(const char *host, const char *port) { + sock_init(); + SSE_RECEIVER *sse = calloc(1, sizeof(SSE_RECEIVER)); + strncpy(sse->host, host, sizeof(sse->host) - 1); + strncpy(sse->port, port, sizeof(sse->port) - 1); + sse->sock = INVALID_SOCKET; + sse->connected = 0; + sse->buf_len = 0; + sse->line_len = 0; + sse->next_attempt = 0; + return sse; +} + +size_t find_sep(const char *buffer, size_t size, char sep) { + for (size_t i = 0; i < size; ++i) { + if (buffer[i] == sep) return i; + } + return size; +} + +const char *sse_receiver_receive(SSE_RECEIVER *sse) { + while(1) { + size_t rm = sse->buf_len-sse->line_len; + if (rm) { + const char *r = sse->buffer+sse->line_len; + size_t sep = find_sep(r, rm, '\n'); + if (sep < rm) { + sep += sse->line_len; + sse->buffer[sep] = 0; + sse->line_len = sep+1; + + if (strncmp(r,"data:",5) == 0) { + r+=5; + while (*r && isspace(*r)) ++r; + if (*r) { + return r; + } + } + continue; + } + if (r != sse->buffer) memmove(sse->buffer, r, rm); + } + sse->line_len = 0; + sse->buf_len = rm; + + if (!sse->connected) { + time_t t = time(NULL); + if (t < sse->next_attempt) { + return NULL; + } + if (!connect_and_handshake(sse)) { + snprintf(sse->buffer, sizeof(sse->buffer) - 1, + "MESSAGE Failed to connect the command server %s:%s ", + sse->host, sse->port); + sse->next_attempt = t+5; + return sse->buffer; + } else { + return "MESSAGE Connected to command server"; + } + } + + int n = recv(sse->sock, sse->buffer + sse->buf_len, (int)(BUFFER_SIZE - sse->buf_len), 0); + if (n <= 0) { +#ifdef _WIN32 + int err = WSAGetLastError(); + if (err != WSAEWOULDBLOCK && err != WSAEINPROGRESS) +#else + if (errno != EWOULDBLOCK && errno != EAGAIN) +#endif + { + CLOSESOCK(sse->sock); + sse->connected = 0; + } + return NULL; + } + + sse->buf_len += n; + } +} + +void sse_receiver_destroy(SSE_RECEIVER *sse) { + if (sse->connected) { + CLOSESOCK(sse->sock); + } + free(sse); + sock_cleanup(); +} diff --git a/platform/sse_receiver.h b/platform/sse_receiver.h new file mode 100644 index 0000000..a567c67 --- /dev/null +++ b/platform/sse_receiver.h @@ -0,0 +1,16 @@ +#pragma once + + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct tag_sse_receiver SSE_RECEIVER; +SSE_RECEIVER *sse_receiver_create(const char *host, const char *port); +const char *sse_receiver_receive(SSE_RECEIVER *sse); +void sse_receiver_destroy(SSE_RECEIVER *inst); + + +#ifdef __cplusplus +} +#endif diff --git a/platform/windows/app_start.cpp b/platform/windows/app_start.cpp index 44f29c3..21c3eea 100644 --- a/platform/windows/app_start.cpp +++ b/platform/windows/app_start.cpp @@ -24,6 +24,7 @@ void show_help(std::ostream &out, const char *arg0) { "-p patch data with custom DDL\n" "-l set language (cz|en)\n" "-s generate string-tables (for localization) and exit\n" + "-c connect to host:port for remote control (mapedit)\n" "-h this help\n"; } @@ -38,8 +39,9 @@ int main(int argc, char **argv) { std::string gen_stringtable_path; std::string lang; std::string patch; + std::string sse_hostport; std::ostringstream console; - for (int optchr = -1; (optchr = getopt(argc, argv, "hf:a:s:l:p:")) != -1; ) { + for (int optchr = -1; (optchr = getopt(argc, argv, "hf:a:s:l:c:p:")) != -1; ) { switch (optchr) { case 'f': config_name = optarg;break; case 'a': adv_config_file = optarg;break; @@ -47,6 +49,7 @@ int main(int argc, char **argv) { case 'p': patch = optarg; break; case 'l': lang = optarg;break; case 's': gen_stringtable_path = optarg;break; + case 'c': sse_hostport = optarg;break; default: show_help_short(console);break; } } @@ -56,7 +59,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]; @@ -68,6 +71,7 @@ int main(int argc, char **argv) { cfg.config_path = config_name.c_str(); cfg.lang_path = lang.empty()?NULL:lang.c_str(); cfg.patch_file = patch.empty()?NULL:patch.c_str(); + cfg.sse_hostport = sse_hostport.c_str(); { std::string msg = console.str();