From 1cad8f1308e4af86027f9ae25523b8a8b3f6592c Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Sun, 9 Aug 2026 05:27:00 +0000 Subject: Next development steps --- CMakeLists.txt | 8 + include/hybbx/chess.h | 105 +++++++ plugins/ssh/ssh.c | 4 + plugins/telnet/telnet.c | 9 +- plugins/websocket/websocket.c | 4 + share/areas.yaml | 13 +- share/commands.yaml | 13 +- src/CMakeLists.txt | 4 + src/core/chess.c | 668 ++++++++++++++++++++++++++++++++++++++++++ src/core/chess_cmd.c | 397 +++++++++++++++++++++++++ src/core/command.c | 6 + src/core/commands_registry.c | 14 +- src/core/service.c | 15 + src/main.c | 53 ++++ 14 files changed, 1303 insertions(+), 10 deletions(-) create mode 100644 include/hybbx/chess.h create mode 100644 src/core/chess.c create mode 100644 src/core/chess_cmd.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 70a6c32..550f3e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -145,6 +145,14 @@ if(HYBBX_STORAGE_SQLITE AND HYBBX_BUILD_DAEMON) endif() endif() +# --------------------------------------------------------------------------- +# --------------------------------------------------------------------------- +find_package(PkgConfig QUIET) +if(PkgConfig_FOUND) +endif() +else() +endif() + # --------------------------------------------------------------------------- # Platform detection (see docs/PLATFORMS.md) # --------------------------------------------------------------------------- diff --git a/include/hybbx/chess.h b/include/hybbx/chess.h new file mode 100644 index 0000000..beb1fe9 --- /dev/null +++ b/include/hybbx/chess.h @@ -0,0 +1,105 @@ +#ifndef HYBBX_CHESS_H +#define HYBBX_CHESS_H + +#include "hybbx/types.h" +#include "hybbx/limits.h" +#include "hybbx/storage.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CHESS_BOARD_SIZE 8 +#define CHESS_SQ_MAX 4 +#define CHESS_MOVE_MAX 8 +#define CHESS_SAN_MAX 8 +#define CHESS_GAME_MAX 16 /* array upper bound */ +#define CHESS_SPECTATOR_MAX 32 +#define CHESS_REFRESH_SEC 15 +#define CHESS_DEFAULT_MAX 8 /* default active games if INI omits [chess] */ + +#define CHESS_EMPTY '.' + +typedef enum { + CHESS_COLOR_NONE = 0, + CHESS_WHITE, + CHESS_BLACK +} chess_color_t; + +typedef enum { + CHESS_RESULT_NONE = 0, + CHESS_WHITE_WINS, + CHESS_BLACK_WINS, + CHESS_DRAW_STALEMATE, + CHESS_DRAW_AGREEMENT +} chess_result_t; + +typedef struct chess_board { + char sq[CHESS_BOARD_SIZE][CHESS_BOARD_SIZE]; +} chess_board_t; + +typedef struct chess_game { + int active; + char white_name[HYBBX_USER_NAME_MAX]; + char black_name[HYBBX_USER_NAME_MAX]; + struct hybbx_session *white_session; + struct hybbx_session *black_session; + struct hybbx_session *spectators[CHESS_SPECTATOR_MAX]; + unsigned spectator_count; + chess_board_t board; + chess_color_t turn; + unsigned move_number; + chess_result_t result; + char last_move_san[CHESS_SAN_MAX]; + unsigned tick_counter; + char game_id[8]; +} chess_game_t; + +void hybbx_chess_init(void); +void hybbx_chess_shutdown(void); +void hybbx_chess_config_apply(const struct hybbx_config *config); +unsigned hybbx_chess_max_games(void); +void hybbx_chess_shutdown(void); + +chess_game_t *hybbx_chess_create_game(struct hybbx_session *white, + const char *white_name); +chess_game_t *hybbx_chess_find_open_game(void); +chess_game_t *hybbx_chess_find_game_by_id(const char *id); +chess_game_t *hybbx_chess_get_game(unsigned index); +int hybbx_chess_join_game(chess_game_t *game, + struct hybbx_session *black, + const char *black_name); +int hybbx_chess_add_spectator(chess_game_t *game, + struct hybbx_session *spec); +void hybbx_chess_remove_spectator(chess_game_t *game, + struct hybbx_session *spec); +int hybbx_chess_is_player(const chess_game_t *game, + const struct hybbx_session *session); +int hybbx_chess_is_spectator(const chess_game_t *game, + const struct hybbx_session *session); +chess_color_t hybbx_chess_player_color(const chess_game_t *game, + const struct hybbx_session *session); +int hybbx_chess_make_move(chess_game_t *game, + const char *from, const char *to, + char promote, char *err, size_t errlen); +void hybbx_chess_render_board(const chess_board_t *board, + struct hybbx_session *session, + const char *label); +void hybbx_chess_send_board(chess_game_t *game, + struct hybbx_session *session); +void hybbx_chess_tick(struct hybbx_service *service); +void hybbx_chess_resign(chess_game_t *game, + struct hybbx_session *who); +void hybbx_chess_offer_draw(chess_game_t *game, + struct hybbx_session *who); + +/* Command handler - called from command.c dispatch */ +hybbx_result_t cmd_chess(struct hybbx_service *service, + struct hybbx_session *session, + const struct hybbx_parsed_command *cmd); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CHESS_H */ diff --git a/plugins/ssh/ssh.c b/plugins/ssh/ssh.c index 442653d..aaf31ce 100644 --- a/plugins/ssh/ssh.c +++ b/plugins/ssh/ssh.c @@ -464,6 +464,10 @@ static void *ssh_client_thread(void *arg) cleanup: if (client->hbx_session != NULL) { + const char *uname = hybbx_session_username(client->hbx_session); + hybbx_log_info("[ssh] session end: user=%s last_rc=%d", + (uname && uname[0]) ? uname : "?", + (int)client->last_rc); hybbx_session_close(client->hbx_session); client->hbx_session = NULL; } diff --git a/plugins/telnet/telnet.c b/plugins/telnet/telnet.c index a857b7d..dd08dd1 100644 --- a/plugins/telnet/telnet.c +++ b/plugins/telnet/telnet.c @@ -207,7 +207,7 @@ static void telnet_send_busy(int fd) return; } - (void)send(fd, msg, sizeof(msg) - 1u, 0); + (void)send(fd, msg, sizeof(msg) - 1u, MSG_NOSIGNAL); } static void *telnet_client_thread(void *arg) @@ -307,6 +307,13 @@ static void *telnet_client_thread(void *arg) } } + { + const char *uname = hybbx_session_username(session); + hybbx_log_info("[telnet] session end: user=%s fd=%d last_rc=%d", + (uname && uname[0]) ? uname : "?", + client->fd, (int)tctx.last_rc); + } + hybbx_session_close(session); shutdown(client->fd, SHUT_RDWR); close(client->fd); diff --git a/plugins/websocket/websocket.c b/plugins/websocket/websocket.c index 4ff2230..f466c5f 100644 --- a/plugins/websocket/websocket.c +++ b/plugins/websocket/websocket.c @@ -470,6 +470,10 @@ static void *ws_client_thread(void *arg) cleanup: if (client->hbx_session != NULL) { + const char *uname = hybbx_session_username(client->hbx_session); + hybbx_log_info("[websocket] session end: user=%s last_rc=%d", + (uname && uname[0]) ? uname : "?", + (int)client->last_rc); hybbx_session_close(client->hbx_session); client->hbx_session = NULL; } diff --git a/share/areas.yaml b/share/areas.yaml index 8a10d54..ed678a2 100644 --- a/share/areas.yaml +++ b/share/areas.yaml @@ -24,8 +24,12 @@ areas: commands: [login, changeme, register, deleteme] - label: Admin commands: [usercreate, activate, changeuser, promote, demote, delete] + - label: Monitor + commands: [monitor] + - label: Entertain + commands: [chess] - label: Sysop - commands: [deleteuser, shutdown, restart, broadcast, monitor] + commands: [deleteuser, shutdown, restart, broadcast] menu: Guest: @@ -38,23 +42,28 @@ menu: - Screen - Areas - Account + - Entertain Mod: - General - Screen - Areas - Account + - Entertain Admin: - General - Screen - Areas - Account - Admin + - Entertain Sysop: - General - Screen - Areas - Account - Admin + - Monitor + - Entertain - Sysop index: @@ -63,4 +72,6 @@ index: - Areas - Account - Admin + - Monitor + - Entertain - Sysop diff --git a/share/commands.yaml b/share/commands.yaml index 0f97200..5a5d848 100644 --- a/share/commands.yaml +++ b/share/commands.yaml @@ -75,6 +75,9 @@ aliases: - announce monitor: - mon + chess: + - play + - mv proxymail: - /mail proxymail proxychat: @@ -300,7 +303,15 @@ commands: line2: "Help: Local +++ announce or sequential RF beacon. More: /announce" monitor: - group: sysop + group: monitor min: Sysop line1: "/monitor on|off|list|cmds|meet user" line2: "Help: Live log monitor and sysop admin menu. More: /mon" + + chess: + group: entertain + min: User + line1: "/chess new|join|watch|move|resign|draw|list|board" + line2: "Help: Play chess. Two players + spectators. More: /play /mv" + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e26bbcd..e966362 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,8 @@ if(HYBBX_BUILD_DAEMON) core/util.c core/log.c core/monitor.c + core/chess.c + core/chess_cmd.c core/max25.c core/security.c core/security_ban.c @@ -106,6 +108,8 @@ if(HYBBX_BUILD_DAEMON) target_link_libraries(hybbx_core PRIVATE ${SQLITE3_LIBRARIES}) endif() + + add_executable(hybbxd main.c instance_role_main.c) add_executable(hybbxsd main.c instance_role_secondary.c) add_executable(hybbxpd main.c instance_role_proxy.c) diff --git a/src/core/chess.c b/src/core/chess.c new file mode 100644 index 0000000..94d35a6 --- /dev/null +++ b/src/core/chess.c @@ -0,0 +1,668 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/chess.h" +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/log.h" +#include "hybbx/util.h" +#include "hybbx/config.h" + +#include +#include +#include +#include + +static chess_game_t g_games[CHESS_GAME_MAX]; +static unsigned g_max_games = CHESS_DEFAULT_MAX; + +/* ── Board init ────────────────────────────────────────────── */ + +static void board_init(chess_board_t *b) +{ + static const char back[] = "RNBQKBNR"; + unsigned f; + + memset(b, 0, sizeof(*b)); + for (f = 0; f < 8; f++) { + b->sq[0][f] = back[f]; + b->sq[1][f] = 'P'; + b->sq[6][f] = 'p'; + b->sq[7][f] = (char)tolower((unsigned char)back[f]); + } + for (unsigned r = 2; r < 6; r++) + for (f = 0; f < 8; f++) + b->sq[r][f] = CHESS_EMPTY; +} + +void hybbx_chess_init(void) +{ + memset(g_games, 0, sizeof(g_games)); + g_max_games = CHESS_DEFAULT_MAX; +} + +void hybbx_chess_shutdown(void) +{ + unsigned i; + for (i = 0; i < g_max_games; i++) + g_games[i].active = 0; +} + +void hybbx_chess_config_apply(const struct hybbx_config *config) +{ + unsigned val; + + if (config != NULL) { + val = hybbx_config_get_uint(config, "chess", "max_games", + CHESS_DEFAULT_MAX, 1, CHESS_GAME_MAX); + } else { + val = CHESS_DEFAULT_MAX; + } + g_max_games = val; + hybbx_log_info("[chess] max_games=%u", g_max_games); +} + +unsigned hybbx_chess_max_games(void) +{ + return g_max_games; +} + +/* ── Helpers ───────────────────────────────────────────────── */ + +static int parse_square(const char *s, unsigned *rank, unsigned *file) +{ + if (s == NULL || strlen(s) < 2) + return 0; + if (s[0] < 'a' || s[0] > 'h' || s[1] < '1' || s[1] > '8') + return 0; + *file = (unsigned)(s[0] - 'a'); + *rank = (unsigned)(s[1] - '1'); + return 1; +} + +static chess_color_t piece_color(char p) +{ + if (p == CHESS_EMPTY) return CHESS_COLOR_NONE; + return (p >= 'A' && p <= 'Z') ? CHESS_WHITE : CHESS_BLACK; +} + +static char piece_upper(char p) +{ + return (char)toupper((unsigned char)p); +} + +static int in_bounds(unsigned r, unsigned f) +{ + return r < 8 && f < 8; +} + +/* ── Check detection ───────────────────────────────────────── */ + +static int square_attacked_by(const chess_board_t *b, unsigned rank, + unsigned file, chess_color_t by) +{ + unsigned r, f; + int dr, df; + char pawn, knight, bishop, rook, queen, king; + + if (by == CHESS_WHITE) { + pawn = 'P'; knight = 'N'; bishop = 'B'; + rook = 'R'; queen = 'Q'; king = 'K'; + } else { + pawn = 'p'; knight = 'n'; bishop = 'b'; + rook = 'r'; queen = 'q'; king = 'k'; + } + + /* Pawn attacks */ + if (by == CHESS_WHITE) { + if (rank > 0 && file > 0 && b->sq[rank - 1][file - 1] == pawn) return 1; + if (rank > 0 && file < 7 && b->sq[rank - 1][file + 1] == pawn) return 1; + } else { + if (rank < 7 && file > 0 && b->sq[rank + 1][file - 1] == pawn) return 1; + if (rank < 7 && file < 7 && b->sq[rank + 1][file + 1] == pawn) return 1; + } + + /* Knight */ + { + static const int kr[] = {2,2,-2,-2,1,1,-1,-1}; + static const int kf[] = {1,-1,1,-1,2,-2,2,-2}; + for (unsigned i = 0; i < 8; i++) { + int nr = (int)rank + kr[i], nf = (int)file + kf[i]; + if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) + if (b->sq[nr][nf] == knight) return 1; + } + } + + /* Sliding: bishop/queen diagonals */ + { + static const int drs[] = {1,1,-1,-1}; + static const int dfs[] = {1,-1,1,-1}; + for (unsigned d = 0; d < 4; d++) { + dr = drs[d]; df = dfs[d]; + r = rank; f = file; + while (1) { + r = (unsigned)((int)r + dr); + f = (unsigned)((int)f + df); + if (!in_bounds(r, f)) break; + if (b->sq[r][f] != CHESS_EMPTY) { + if (b->sq[r][f] == bishop || b->sq[r][f] == queen) return 1; + break; + } + } + } + } + + /* Sliding: rook/queen straights */ + { + static const int drs[] = {1,-1,0,0}; + static const int dfs[] = {0,0,1,-1}; + for (unsigned d = 0; d < 4; d++) { + dr = drs[d]; df = dfs[d]; + r = rank; f = file; + while (1) { + r = (unsigned)((int)r + dr); + f = (unsigned)((int)f + df); + if (!in_bounds(r, f)) break; + if (b->sq[r][f] != CHESS_EMPTY) { + if (b->sq[r][f] == rook || b->sq[r][f] == queen) return 1; + break; + } + } + } + } + + /* King */ + { + static const int drs[] = {1,1,1,0,0,-1,-1,-1}; + static const int dfs[] = {1,0,-1,1,-1,1,0,-1}; + for (unsigned d = 0; d < 8; d++) { + int nr = (int)rank + drs[d], nf = (int)file + dfs[d]; + if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) + if (b->sq[nr][nf] == king) return 1; + } + } + + return 0; +} + +static int find_king(const chess_board_t *b, chess_color_t color, + unsigned *kr, unsigned *kf) +{ + char king = (color == CHESS_WHITE) ? 'K' : 'k'; + for (unsigned r = 0; r < 8; r++) + for (unsigned f = 0; f < 8; f++) + if (b->sq[r][f] == king) { *kr = r; *kf = f; return 1; } + return 0; +} + +static int is_in_check(const chess_board_t *b, chess_color_t side) +{ + unsigned kr, kf; + chess_color_t opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE; + if (!find_king(b, side, &kr, &kf)) return 0; + return square_attacked_by(b, kr, kf, opp); +} + +/* ── Move legality ─────────────────────────────────────────── */ + +static int pseudo_legal_pawn(const chess_board_t *b, unsigned fr, + unsigned ff, unsigned tr, unsigned tf, + chess_color_t side) +{ + int dir = (side == CHESS_WHITE) ? 1 : -1; + int start_rank = (side == CHESS_WHITE) ? 1 : 6; + int dr = (int)tr - (int)fr; + int df = (int)tf - (int)ff; + + if (df == 0) { + if (dr == dir && b->sq[tr][tf] == CHESS_EMPTY) return 1; + if (dr == 2 * dir && (int)fr == start_rank && + b->sq[fr + (unsigned)dir][ff] == CHESS_EMPTY && + b->sq[tr][tf] == CHESS_EMPTY) return 1; + } + if ((df == 1 || df == -1) && dr == dir) { + if (b->sq[tr][tf] != CHESS_EMPTY) return 1; + } + return 0; +} + +static int pseudo_legal_knight(unsigned fr, unsigned ff, unsigned tr, + unsigned tf) +{ + int dr = abs((int)tr - (int)fr); + int df = abs((int)tf - (int)ff); + return (dr == 2 && df == 1) || (dr == 1 && df == 2); +} + +static int sliding_clear(const chess_board_t *b, unsigned fr, unsigned ff, + unsigned tr, unsigned tf) +{ + int dr = 0, df = 0; + unsigned r, f; + + if ((int)tr > (int)fr) dr = 1; + else if ((int)tr < (int)fr) dr = -1; + if ((int)tf > (int)ff) df = 1; + else if ((int)tf < (int)ff) df = -1; + + r = (unsigned)((int)fr + dr); + f = (unsigned)((int)ff + df); + while (r != tr || f != tf) { + if (!in_bounds(r, f) || b->sq[r][f] != CHESS_EMPTY) return 0; + r = (unsigned)((int)r + dr); + f = (unsigned)((int)f + df); + } + return 1; +} + +static int pseudo_legal(const chess_board_t *b, unsigned fr, unsigned ff, + unsigned tr, unsigned tf, chess_color_t side) +{ + char piece = b->sq[fr][ff]; + int dr = abs((int)tr - (int)fr); + int df = abs((int)tf - (int)ff); + chess_color_t target_color; + + if (piece == CHESS_EMPTY) return 0; + if (piece_color(piece) != side) return 0; + + target_color = piece_color(b->sq[tr][tf]); + if (target_color == side) return 0; + + switch (piece_upper(piece)) { + case 'P': return pseudo_legal_pawn(b, fr, ff, tr, tf, side); + case 'N': return pseudo_legal_knight(fr, ff, tr, tf); + case 'B': + if (dr != df || dr == 0) return 0; + return sliding_clear(b, fr, ff, tr, tf); + case 'R': + if (fr != tr && ff != tf) return 0; + return sliding_clear(b, fr, ff, tr, tf); + case 'Q': + if (fr != tr && ff != tf && dr != df) return 0; + return sliding_clear(b, fr, ff, tr, tf); + case 'K': + return dr <= 1 && df <= 1 && (dr + df > 0); + } + return 0; +} + +static int has_any_legal_move(chess_board_t *b, chess_color_t side); + +static int is_legal_move(chess_board_t *b, unsigned fr, unsigned ff, + unsigned tr, unsigned tf, chess_color_t side) +{ + chess_board_t tmp; + + if (!pseudo_legal(b, fr, ff, tr, tf, side)) return 0; + + memcpy(&tmp, b, sizeof(tmp)); + tmp.sq[tr][tf] = tmp.sq[fr][ff]; + tmp.sq[fr][ff] = CHESS_EMPTY; + + /* Auto-promote to queen */ + if (piece_upper(tmp.sq[tr][tf]) == 'P') { + if ((side == CHESS_WHITE && tr == 7) || + (side == CHESS_BLACK && tr == 0)) { + tmp.sq[tr][tf] = (side == CHESS_WHITE) ? 'Q' : 'q'; + } + } + + return !is_in_check(&tmp, side); +} + +static int has_any_legal_move(chess_board_t *b, chess_color_t side) +{ + for (unsigned fr = 0; fr < 8; fr++) + for (unsigned ff = 0; ff < 8; ff++) { + if (piece_color(b->sq[fr][ff]) != side) continue; + for (unsigned tr = 0; tr < 8; tr++) + for (unsigned tf = 0; tf < 8; tf++) + if (is_legal_move(b, fr, ff, tr, tf, side)) + return 1; + } + return 0; +} + +/* ── SAN notation (simplified) ─────────────────────────────── */ + +static void make_san(char *out, size_t outlen, char piece, + unsigned ff, unsigned tr, unsigned tf, + int capture, int check, int mate, char promote) +{ + char *p = out; + size_t room = outlen; + + if (piece_upper(piece) == 'P') { + if (capture) { + int n = snprintf(p, room, "%cx%c%d", 'a' + ff, 'a' + tf, tr + 1); + p += n; room -= (size_t)n; + } else { + int n = snprintf(p, room, "%c%d", 'a' + tf, tr + 1); + p += n; room -= (size_t)n; + } + } else { + int n = snprintf(p, room, "%c%c%d", piece_upper(piece), + 'a' + tf, tr + 1); + p += n; room -= (size_t)n; + } + if (promote) { + snprintf(p, room, "=%c", piece_upper(promote)); + } else if (mate) { + snprintf(p, room, "#"); + } else if (check) { + snprintf(p, room, "+"); + } +} + +/* ── Game management ───────────────────────────────────────── */ + +chess_game_t *hybbx_chess_get_game(unsigned index) +{ + if (index >= CHESS_GAME_MAX) return NULL; + return &g_games[index]; +} + +chess_game_t *hybbx_chess_create_game(struct hybbx_session *white, + const char *white_name) +{ + unsigned i; + for (i = 0; i < g_max_games; i++) { + if (!g_games[i].active) { + memset(&g_games[i], 0, sizeof(g_games[i])); + g_games[i].active = 1; + hybbx_strlcpy(g_games[i].white_name, white_name, + sizeof(g_games[i].white_name)); + g_games[i].white_session = white; + board_init(&g_games[i].board); + g_games[i].turn = CHESS_WHITE; + g_games[i].move_number = 1; + snprintf(g_games[i].game_id, sizeof(g_games[i].game_id), + "G%u", i + 1); + hybbx_log_info("[chess] game %s created by %s", + g_games[i].game_id, white_name); + return &g_games[i]; + } + } + return NULL; +} + +chess_game_t *hybbx_chess_find_open_game(void) +{ + unsigned i; + for (i = 0; i < g_max_games; i++) + if (g_games[i].active && g_games[i].black_session == NULL) + return &g_games[i]; + return NULL; +} + +chess_game_t *hybbx_chess_find_game_by_id(const char *id) +{ + unsigned i; + if (id == NULL) return NULL; + for (i = 0; i < g_max_games; i++) + if (g_games[i].active && strcasecmp(g_games[i].game_id, id) == 0) + return &g_games[i]; + return NULL; +} + +int hybbx_chess_join_game(chess_game_t *game, struct hybbx_session *black, + const char *black_name) +{ + if (game == NULL || game->black_session != NULL) return 0; + game->black_session = black; + hybbx_strlcpy(game->black_name, black_name, sizeof(game->black_name)); + hybbx_log_info("[chess] %s joined game %s as black", black_name, + game->game_id); + return 1; +} + +int hybbx_chess_add_spectator(chess_game_t *game, + struct hybbx_session *spec) +{ + unsigned i; + if (game == NULL || spec == NULL) return 0; + if (hybbx_chess_is_player(game, spec)) return 0; + for (i = 0; i < game->spectator_count; i++) + if (game->spectators[i] == spec) return 1; + if (game->spectator_count >= CHESS_SPECTATOR_MAX) return 0; + game->spectators[game->spectator_count++] = spec; + return 1; +} + +void hybbx_chess_remove_spectator(chess_game_t *game, + struct hybbx_session *spec) +{ + unsigned i; + if (game == NULL || spec == NULL) return; + for (i = 0; i < game->spectator_count; i++) { + if (game->spectators[i] == spec) { + game->spectators[i] = game->spectators[--game->spectator_count]; + return; + } + } +} + +int hybbx_chess_is_player(const chess_game_t *game, + const struct hybbx_session *session) +{ + if (game == NULL || session == NULL) return 0; + return game->white_session == session || game->black_session == session; +} + +int hybbx_chess_is_spectator(const chess_game_t *game, + const struct hybbx_session *session) +{ + unsigned i; + if (game == NULL || session == NULL) return 0; + for (i = 0; i < game->spectator_count; i++) + if (game->spectators[i] == session) return 1; + return 0; +} + +chess_color_t hybbx_chess_player_color(const chess_game_t *game, + const struct hybbx_session *session) +{ + if (game == NULL || session == NULL) return CHESS_COLOR_NONE; + if (game->white_session == session) return CHESS_WHITE; + if (game->black_session == session) return CHESS_BLACK; + return CHESS_COLOR_NONE; +} + +/* ── Move execution ────────────────────────────────────────── */ + +int hybbx_chess_make_move(chess_game_t *game, const char *from, + const char *to, char promote, char *err, + size_t errlen) +{ + unsigned fr, ff, tr, tf; + char piece, captured; + int check, mate; + chess_color_t side, opp; + + if (game == NULL || game->result != CHESS_RESULT_NONE) { + if (err) snprintf(err, errlen, "Game is over."); + return 0; + } + + side = game->turn; + + if (!parse_square(from, &fr, &ff)) { + if (err) snprintf(err, errlen, "Invalid square: %s", from); + return 0; + } + if (!parse_square(to, &tr, &tf)) { + if (err) snprintf(err, errlen, "Invalid square: %s", to); + return 0; + } + + piece = game->board.sq[fr][ff]; + if (piece_color(piece) != side) { + if (err) snprintf(err, errlen, "Not your piece."); + return 0; + } + + if (!is_legal_move(&game->board, fr, ff, tr, tf, side)) { + if (err) snprintf(err, errlen, "Illegal move."); + return 0; + } + + captured = game->board.sq[tr][tf]; + game->board.sq[tr][tf] = piece; + game->board.sq[fr][ff] = CHESS_EMPTY; + + /* Pawn promotion */ + if (piece_upper(piece) == 'P') { + if ((side == CHESS_WHITE && tr == 7) || + (side == CHESS_BLACK && tr == 0)) { + char pp = promote ? (char)toupper((unsigned char)promote) : 'Q'; + game->board.sq[tr][tf] = (side == CHESS_WHITE) ? pp : + (char)tolower((unsigned char)pp); + } + } + + opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE; + check = is_in_check(&game->board, opp); + mate = check && !has_any_legal_move(&game->board, opp); + + make_san(game->last_move_san, sizeof(game->last_move_san), piece, + ff, tr, tf, captured != CHESS_EMPTY, check, mate, promote); + + if (mate) { + game->result = (side == CHESS_WHITE) ? CHESS_WHITE_WINS : + CHESS_BLACK_WINS; + } else if (!has_any_legal_move(&game->board, opp)) { + game->result = CHESS_DRAW_STALEMATE; + } + + game->turn = opp; + if (opp == CHESS_WHITE) game->move_number++; + game->tick_counter = 0; + + hybbx_log_info("[chess] %s: %s %s", game->game_id, + (side == CHESS_WHITE) ? game->white_name : game->black_name, + game->last_move_san); + return 1; +} + +/* ── Resign / Draw ─────────────────────────────────────────── */ + +void hybbx_chess_resign(chess_game_t *game, struct hybbx_session *who) +{ + if (game == NULL || game->result != CHESS_RESULT_NONE) return; + if (game->white_session == who) + game->result = CHESS_BLACK_WINS; + else if (game->black_session == who) + game->result = CHESS_WHITE_WINS; +} + +void hybbx_chess_offer_draw(chess_game_t *game, struct hybbx_session *who) +{ + (void)who; + if (game == NULL || game->result != CHESS_RESULT_NONE) return; + game->result = CHESS_DRAW_AGREEMENT; +} + +/* ── Board rendering ───────────────────────────────────────── */ + +void hybbx_chess_render_board(const chess_board_t *board, + struct hybbx_session *session, + const char *label) +{ + static const char *glyph = ".KQRBNPkqrbnp"; + char line[96]; + unsigned r, f; + + if (label && label[0]) + hybbx_session_write_line(session, label); + + hybbx_session_write_line(session, + " +---+---+---+---+---+---+---+---+"); + + for (r = 0; r < 8; r++) { + unsigned rank = 7 - r; + int n = snprintf(line, sizeof(line), "%u |", rank + 1); + for (f = 0; f < 8; f++) { + char p = board->sq[rank][f]; + const char *ch; + switch (p) { + case 'K': ch = "K"; break; case 'Q': ch = "Q"; break; + case 'R': ch = "R"; break; case 'B': ch = "B"; break; + case 'N': ch = "N"; break; case 'P': ch = "P"; break; + case 'k': ch = "k"; break; case 'q': ch = "q"; break; + case 'r': ch = "r"; break; case 'b': ch = "b"; break; + case 'n': ch = "n"; break; case 'p': ch = "p"; break; + default: ch = "."; break; + } + n += snprintf(line + n, sizeof(line) - (size_t)n, " %s |", ch); + } + hybbx_session_write_line(session, line); + hybbx_session_write_line(session, + " +---+---+---+---+---+---+---+---+"); + } + hybbx_session_write_line(session, + " a b c d e f g h"); +} + +void hybbx_chess_send_board(chess_game_t *game, + struct hybbx_session *session) +{ + char label[96]; + const char *turn_name; + + if (game == NULL || session == NULL) return; + + turn_name = (game->turn == CHESS_WHITE) ? game->white_name : + game->black_name; + + if (game->result != CHESS_RESULT_NONE) { + const char *result_str; + switch (game->result) { + case CHESS_WHITE_WINS: result_str = "1-0 White wins"; break; + case CHESS_BLACK_WINS: result_str = "0-1 Black wins"; break; + case CHESS_DRAW_STALEMATE: result_str = "1/2-1/2 Stalemate"; break; + case CHESS_DRAW_AGREEMENT: result_str = "1/2-1/2 Draw"; break; + default: result_str = "Game over"; break; + } + snprintf(label, sizeof(label), "[%s] %s vs %s %s Move %u", + game->game_id, game->white_name, game->black_name, + result_str, game->move_number); + } else { + snprintf(label, sizeof(label), + "[%s] %s(W) vs %s(B) %s to move %s Move %u", + game->game_id, game->white_name, game->black_name, + turn_name, + game->last_move_san[0] ? game->last_move_san : "", + game->move_number); + } + + hybbx_chess_render_board(&game->board, session, label); +} + +/* ── 15-second broadcast tick ──────────────────────────────── */ + +void hybbx_chess_tick(struct hybbx_service *service) +{ + unsigned i, j; + + (void)service; + + for (i = 0; i < g_max_games; i++) { + chess_game_t *g = &g_games[i]; + + if (!g->active) continue; + if (g->black_session == NULL) continue; + + g->tick_counter++; + if (g->tick_counter < CHESS_REFRESH_SEC) continue; + g->tick_counter = 0; + + if (g->white_session) + hybbx_chess_send_board(g, g->white_session); + if (g->black_session) + hybbx_chess_send_board(g, g->black_session); + for (j = 0; j < g->spectator_count; j++) + hybbx_chess_send_board(g, g->spectators[j]); + } +} diff --git a/src/core/chess_cmd.c b/src/core/chess_cmd.c new file mode 100644 index 0000000..6ad1058 --- /dev/null +++ b/src/core/chess_cmd.c @@ -0,0 +1,397 @@ +/* ── Chess command handler ──────────────────────────────────── */ + +#include "hybbx/chess.h" +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/hybbx.h" + +#include +#include +#include +#include + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) return 0; + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + if (ca != cb) return 0; + a++; b++; + } + return *a == '\0' && *b == '\0'; +} + +hybbx_result_t cmd_chess(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + const char *sub; + chess_game_t *game; + char line[128]; + + (void)service; + + if (!hybbx_session_logged_in(session)) { + hybbx_session_write_line(session, "Log in to play chess."); + return HYBBX_ERR_DENIED; + } + + sub = (cmd->argc >= 1) ? cmd->argv[0] : "help"; + + /* ── /chess help ───────────────────────────────────────── */ + if (str_ieq(sub, "help") || str_ieq(sub, "?")) { + hybbx_session_write_line(session, "Chess commands:"); + hybbx_session_write_line(session, + " /chess new - Create a new game (you are White)"); + hybbx_session_write_line(session, + " /chess join - Join an open game as Black"); + hybbx_session_write_line(session, + " /chess join G2 - Join a specific game by ID"); + hybbx_session_write_line(session, + " /chess watch - Watch a game as spectator"); + hybbx_session_write_line(session, + " /chess watch G2 - Watch a specific game"); + hybbx_session_write_line(session, + " /chess board - Show current board"); + hybbx_session_write_line(session, + " /chess move e2 e4 - Move piece (alias: /mv e2 e4)"); + hybbx_session_write_line(session, + " /chess resign - Resign the game"); + hybbx_session_write_line(session, + " /chess draw - Offer/accept draw"); + hybbx_session_write_line(session, + " /chess list - List games with board + spectators"); + hybbx_session_write_line(session, + " /chess leave - Stop watching"); + hybbx_session_write_line(session, + "Board auto-refreshes every 15 seconds for all."); + return HYBBX_OK; + } + + /* ── /chess list ───────────────────────────────────────── */ + if (str_ieq(sub, "list") || str_ieq(sub, "ls")) { + unsigned found = 0; + unsigned i; + for (i = 0; i < hybbx_chess_max_games(); i++) { + game = hybbx_chess_get_game(i); + if (game == NULL || !game->active) continue; + + /* Header: game id, players, state */ + { + const char *state = + (game->result != CHESS_RESULT_NONE) ? "finished" : + (game->black_session == NULL) ? "waiting for Black" : "playing"; + snprintf(line, sizeof(line), + "[%s] %s(W) vs %s(B) %s move %u", + game->game_id, game->white_name, + game->black_session ? game->black_name : "...", + state, game->move_number); + } + hybbx_session_write_line(session, line); + + /* Board */ + hybbx_chess_send_board(game, session); + + /* Spectators */ + if (game->spectator_count > 0) { + unsigned j; + int n = snprintf(line, sizeof(line), " Spectators (%u): ", + game->spectator_count); + for (j = 0; j < game->spectator_count; j++) { + const char *sname = hybbx_session_display_name( + game->spectators[j]); + if (j > 0) + n += snprintf(line + n, sizeof(line) - (size_t)n, ", "); + n += snprintf(line + n, sizeof(line) - (size_t)n, "%s", + sname ? sname : "?"); + } + hybbx_session_write_line(session, line); + } + + hybbx_session_write_line(session, ""); + found++; + } + if (!found) + hybbx_session_write_line(session, " No active games."); + return HYBBX_OK; + } + + /* ── /chess new ────────────────────────────────────────── */ + if (str_ieq(sub, "new") || str_ieq(sub, "create")) { + unsigned i; + for (i = 0; i < hybbx_chess_max_games(); i++) { + game = hybbx_chess_get_game(i); + if (game && game->active && + hybbx_chess_is_player(game, session)) { + hybbx_session_write_line(session, + "You are already in a game. /chess resign first."); + return HYBBX_ERR_DENIED; + } + } + game = hybbx_chess_create_game(session, + hybbx_session_username(session)); + if (game == NULL) { + hybbx_session_write_line(session, "No free game slots."); + return HYBBX_ERR_DENIED; + } + snprintf(line, sizeof(line), "Game %s created. You are White.", + game->game_id); + hybbx_session_write_line(session, line); + hybbx_session_write_line(session, + "Waiting for Black to /chess join..."); + hybbx_chess_send_board(game, session); + return HYBBX_OK; + } + + /* ── /chess join [id] ──────────────────────────────────── */ + if (str_ieq(sub, "join")) { + const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL; + unsigned i; + + for (i = 0; i < hybbx_chess_max_games(); i++) { + game = hybbx_chess_get_game(i); + if (game && game->active && + hybbx_chess_is_player(game, session)) { + hybbx_session_write_line(session, + "You are already in a game. /chess resign first."); + return HYBBX_ERR_DENIED; + } + } + + if (id) { + game = hybbx_chess_find_game_by_id(id); + if (game == NULL || !game->active) { + hybbx_session_write_line(session, "Game not found."); + return HYBBX_ERR_NOT_FOUND; + } + if (game->black_session != NULL) { + hybbx_session_write_line(session, "Game is full."); + return HYBBX_ERR_DENIED; + } + } else { + game = hybbx_chess_find_open_game(); + if (game == NULL) { + hybbx_session_write_line(session, + "No open games. /chess new to create one."); + return HYBBX_ERR_NOT_FOUND; + } + } + + hybbx_chess_join_game(game, session, + hybbx_session_username(session)); + snprintf(line, sizeof(line), "Joined %s as Black. Game on!", + game->game_id); + hybbx_session_write_line(session, line); + hybbx_chess_send_board(game, game->white_session); + hybbx_chess_send_board(game, session); + return HYBBX_OK; + } + + /* ── /chess watch [id] ─────────────────────────────────── */ + if (str_ieq(sub, "watch") || str_ieq(sub, "spectate")) { + const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL; + unsigned i; + + if (id) { + game = hybbx_chess_find_game_by_id(id); + } else { + game = NULL; + for (i = 0; i < hybbx_chess_max_games(); i++) { + chess_game_t *g = hybbx_chess_get_game(i); + if (g && g->active && g->black_session != NULL) { + game = g; + break; + } + } + } + if (game == NULL || !game->active) { + hybbx_session_write_line(session, "No game to watch."); + return HYBBX_ERR_NOT_FOUND; + } + if (!hybbx_chess_add_spectator(game, session)) { + hybbx_session_write_line(session, "Spectator list full."); + return HYBBX_ERR_DENIED; + } + snprintf(line, sizeof(line), + "Watching %s. Board refreshes every %ds.", + game->game_id, CHESS_REFRESH_SEC); + hybbx_session_write_line(session, line); + hybbx_chess_send_board(game, session); + return HYBBX_OK; + } + + /* ── /chess leave ──────────────────────────────────────── */ + if (str_ieq(sub, "leave") || str_ieq(sub, "unwatch")) { + unsigned i; + for (i = 0; i < hybbx_chess_max_games(); i++) { + game = hybbx_chess_get_game(i); + if (game && game->active && + hybbx_chess_is_spectator(game, session)) { + hybbx_chess_remove_spectator(game, session); + hybbx_session_write_line(session, "Stopped watching."); + return HYBBX_OK; + } + } + hybbx_session_write_line(session, + "You are not watching any game."); + return HYBBX_ERR_NOT_FOUND; + } + + /* ── /chess board ──────────────────────────────────────── */ + if (str_ieq(sub, "board") || str_ieq(sub, "show")) { + unsigned i; + game = NULL; + for (i = 0; i < hybbx_chess_max_games(); i++) { + chess_game_t *g = hybbx_chess_get_game(i); + if (g && g->active && + (hybbx_chess_is_player(g, session) || + hybbx_chess_is_spectator(g, session))) { + game = g; + break; + } + } + if (game == NULL) { + hybbx_session_write_line(session, + "Not in a game. /chess new, join, or watch."); + return HYBBX_ERR_NOT_FOUND; + } + hybbx_chess_send_board(game, session); + return HYBBX_OK; + } + + /* ── /chess move e2 e4 ─────────────────────────────────── */ + if (str_ieq(sub, "move") || str_ieq(sub, "mv") || str_ieq(sub, "m")) { + const char *from, *to; + char err[64]; + unsigned i; + + if (cmd->argc < 3) { + hybbx_session_write_line(session, + "Usage: /chess move e2 e4"); + return HYBBX_ERR_INVALID; + } + from = cmd->argv[1]; + to = cmd->argv[2]; + + game = NULL; + for (i = 0; i < hybbx_chess_max_games(); i++) { + chess_game_t *g = hybbx_chess_get_game(i); + if (g && g->active && g->result == CHESS_RESULT_NONE && + hybbx_chess_is_player(g, session)) { + chess_color_t color = hybbx_chess_player_color(g, session); + if (color == g->turn) { + game = g; + break; + } + } + } + if (game == NULL) { + hybbx_session_write_line(session, + "Not your turn or no active game."); + return HYBBX_ERR_DENIED; + } + + if (!hybbx_chess_make_move(game, from, to, 0, err, sizeof(err))) { + snprintf(line, sizeof(line), "Illegal: %s", err); + hybbx_session_write_line(session, line); + return HYBBX_ERR_INVALID; + } + + /* Broadcast to all */ + if (game->white_session) + hybbx_chess_send_board(game, game->white_session); + if (game->black_session) + hybbx_chess_send_board(game, game->black_session); + { + unsigned j; + for (j = 0; j < game->spectator_count; j++) + hybbx_chess_send_board(game, game->spectators[j]); + } + + if (game->result != CHESS_RESULT_NONE) { + const char *res; + unsigned j; + switch (game->result) { + case CHESS_WHITE_WINS: res = "White wins!"; break; + case CHESS_BLACK_WINS: res = "Black wins!"; break; + case CHESS_DRAW_STALEMATE: res = "Stalemate!"; break; + case CHESS_DRAW_AGREEMENT: res = "Draw!"; break; + default: res = "Game over."; break; + } + snprintf(line, sizeof(line), "[%s] %s", game->game_id, res); + if (game->white_session) + hybbx_session_write_line(game->white_session, line); + if (game->black_session) + hybbx_session_write_line(game->black_session, line); + for (j = 0; j < game->spectator_count; j++) + hybbx_session_write_line(game->spectators[j], line); + } + + return HYBBX_OK; + } + + /* ── /chess resign ─────────────────────────────────────── */ + if (str_ieq(sub, "resign")) { + unsigned i; + game = NULL; + for (i = 0; i < hybbx_chess_max_games(); i++) { + chess_game_t *g = hybbx_chess_get_game(i); + if (g && g->active && hybbx_chess_is_player(g, session)) { + game = g; + break; + } + } + if (game == NULL) { + hybbx_session_write_line(session, "You are not in a game."); + return HYBBX_ERR_NOT_FOUND; + } + hybbx_chess_resign(game, session); + snprintf(line, sizeof(line), "[%s] %s resigned.", + game->game_id, hybbx_session_username(session)); + if (game->white_session) + hybbx_session_write_line(game->white_session, line); + if (game->black_session) + hybbx_session_write_line(game->black_session, line); + { + unsigned j; + for (j = 0; j < game->spectator_count; j++) + hybbx_session_write_line(game->spectators[j], line); + } + return HYBBX_OK; + } + + /* ── /chess draw ───────────────────────────────────────── */ + if (str_ieq(sub, "draw")) { + unsigned i; + game = NULL; + for (i = 0; i < hybbx_chess_max_games(); i++) { + chess_game_t *g = hybbx_chess_get_game(i); + if (g && g->active && hybbx_chess_is_player(g, session)) { + game = g; + break; + } + } + if (game == NULL) { + hybbx_session_write_line(session, "You are not in a game."); + return HYBBX_ERR_NOT_FOUND; + } + hybbx_chess_offer_draw(game, session); + snprintf(line, sizeof(line), "[%s] Draw agreed.", + game->game_id); + if (game->white_session) + hybbx_session_write_line(game->white_session, line); + if (game->black_session) + hybbx_session_write_line(game->black_session, line); + { + unsigned j; + for (j = 0; j < game->spectator_count; j++) + hybbx_session_write_line(game->spectators[j], line); + } + return HYBBX_OK; + } + + hybbx_session_write_line(session, + "Unknown chess command. /chess help for list."); + return HYBBX_ERR_NOT_FOUND; +} diff --git a/src/core/command.c b/src/core/command.c index b60ee35..8cd4e5c 100644 --- a/src/core/command.c +++ b/src/core/command.c @@ -18,6 +18,7 @@ #include "hybbx/password.h" #include "hybbx/traffic.h" #include "hybbx/monitor.h" +#include "hybbx/chess.h" #include "hybbx/util.h" #include @@ -2311,6 +2312,11 @@ hybbx_result_t hybbx_command_dispatch(hybbx_service_t *service, return cmd_monitor(service, session, cmd); } + if (str_ieq(cmd->verb, "chess") || str_ieq(cmd->verb, "play")) { + return cmd_chess(service, session, cmd); + } + + if (str_ieq(cmd->verb, "users")) { return cmd_users(service, session); } diff --git a/src/core/commands_registry.c b/src/core/commands_registry.c index 6f63178..6b0be63 100644 --- a/src/core/commands_registry.c +++ b/src/core/commands_registry.c @@ -1312,15 +1312,15 @@ void hybbx_commands_registry_show_menu(hybbx_session_t *session) } /* - * Allow-list grants are not Sysop — their menu layout has no Sysop area. - * Append filtered Sysop area so /monitor is visible when granted. + * Allow-list grants are not Sysop - their menu layout has no Monitor area. + * Append filtered Monitor area so /monitor is visible when granted. */ if (hybbx_monitor_session_may_use(session) && !hybbx_user_level_is_sysop(level)) { - const menu_area_t *sysop = area_find("Sysop"); + const menu_area_t *monitor = area_find("Monitor"); - if (sysop != NULL) { - render_area(session, sysop, level, 1); + if (monitor != NULL) { + render_area(session, monitor, level, 1); } } } @@ -1335,11 +1335,11 @@ void hybbx_commands_registry_show_index(hybbx_session_t *session) level = hybbx_session_user_level(session); hybbx_session_write_line(session, g_registry.index_header); - /* Filter by access — Sysop cmds (incl. /monitor) only for Sysop + grants. */ + /* No access filter - /index lists ALL commands for every user level. */ render_area_labels(session, (const char (*)[16])g_registry.index_labels, g_registry.index_label_count, - level, 1); + level, 0); } void hybbx_commands_registry_show_aliases(hybbx_session_t *session) diff --git a/src/core/service.c b/src/core/service.c index a8bd402..6cf2b16 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -26,6 +26,8 @@ void hybbx_mains_proxy_plugin_tick(void); #include "hybbx/instance.h" #include "hybbx/log.h" #include "hybbx/monitor.h" +#include "hybbx/chess.h" +#include #include "hybbx/security.h" #include "hybbx/security_ban.h" #include "hybbx/util.h" @@ -166,6 +168,7 @@ void hybbx_service_destroy(hybbx_service_t *service) } hybbx_monitor_shutdown(); + hybbx_chess_init(); hybbx_log_shutdown(); hybbx_security_log_shutdown(); hybbx_security_ban_shutdown(); @@ -978,10 +981,21 @@ hybbx_result_t hybbx_service_run(hybbx_service_t *service) Sleep(1000); #else static unsigned prune_tick; + static unsigned health_tick; sleep(1); + + /* Periodic health log — every 300s (5 min) */ + health_tick++; + if (health_tick >= 300u) { + health_tick = 0; + hybbx_log_info("[service] health: running=%d transports=%u sessions_active (tick)", + svc->running, svc->transport_count); + } + hybbx_security_ban_tick(); hybbx_monitor_tick(service); + hybbx_chess_tick(service); hybbx_broadcast_ax25_tick(service); #ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY hybbx_mains_proxy_plugin_tick(); @@ -1209,6 +1223,7 @@ hybbx_result_t hybbx_service_apply_config(hybbx_service_t *service, hybbx_log_config_apply(config); hybbx_security_log_config_apply(config); hybbx_monitor_config_apply(config); + hybbx_chess_config_apply(config); hybbx_security_ban_config_apply(config); service_apply_texts(svc, config); hybbx_chat_config_apply(&svc->chat, config); diff --git a/src/main.c b/src/main.c index 670de7a..c91982e 100644 --- a/src/main.c +++ b/src/main.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -215,6 +216,42 @@ static void setup_install_root(const char *config_path) } } + +/* ── Signal handling ────────────────────────────────────────── */ + +static volatile sig_atomic_t g_signal_received; + +static void signal_handler(int sig) +{ + g_signal_received = sig; + /* SIGPIPE: just ignore — send() with MSG_NOSIGNAL handles most cases */ + if (sig == SIGPIPE) { + return; + } + /* SIGTERM/SIGINT: main loop will see g_signal_received and exit */ +} + +static void install_signal_handlers(void) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + + /* SIGPIPE: ignore — prevents silent death on broken socket writes */ + sa.sa_handler = SIG_IGN; + sa.sa_flags = 0; + sigaction(SIGPIPE, &sa, NULL); + + /* SIGTERM: graceful shutdown */ + sa.sa_handler = signal_handler; + sigaction(SIGTERM, &sa, NULL); + + /* SIGINT: graceful shutdown (Ctrl+C) */ + sigaction(SIGINT, &sa, NULL); + + g_signal_received = 0; +} + int main(int argc, char *argv[]) { hybbx_service_t *service; @@ -355,8 +392,22 @@ int main(int argc, char *argv[]) hybbx_service_set_launch_binary(service, argv[0]); + install_signal_handlers(); + + printf("HyBBX %s running (%s). Signals: SIGPIPE=ignored, SIGTERM/SIGINT=graceful stop.\n", + HYBBX_VERSION_STRING, hybbx_instance_binary_name()); + hybbx_service_run(service); + if (g_signal_received) { + printf("HyBBX: received signal %d (%s) — shutting down.\n", + g_signal_received, + g_signal_received == SIGTERM ? "SIGTERM" : + g_signal_received == SIGINT ? "SIGINT" : "unknown"); + } else { + printf("HyBBX: service loop exited normally.\n"); + } + if (hybbx_service_shutdown_mode(service) == HYBBX_SHUTDOWN_RESTART) { hybbx_service_restart_exec(service); } @@ -367,5 +418,7 @@ int main(int argc, char *argv[]) hybbx_commands_registry_shutdown(); hybbx_service_destroy(service); + printf("HyBBX: shutdown complete (exit %d, signal=%d).\n", + EXIT_SUCCESS, (int)g_signal_received); return EXIT_SUCCESS; } -- cgit v1.3.1