summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-09 05:27:00 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-09 05:27:00 +0000
commit1cad8f1308e4af86027f9ae25523b8a8b3f6592c (patch)
tree7fa911b052986a69728343a09f8dde3d1c206c9b
parent20cb29c2f8c5c87bc590896854a20b1473ceb358 (diff)
Next development steps
-rw-r--r--CMakeLists.txt8
-rw-r--r--include/hybbx/chess.h105
-rw-r--r--plugins/ssh/ssh.c4
-rw-r--r--plugins/telnet/telnet.c9
-rw-r--r--plugins/websocket/websocket.c4
-rw-r--r--share/areas.yaml13
-rw-r--r--share/commands.yaml13
-rw-r--r--src/CMakeLists.txt4
-rw-r--r--src/core/chess.c668
-rw-r--r--src/core/chess_cmd.c397
-rw-r--r--src/core/command.c6
-rw-r--r--src/core/commands_registry.c14
-rw-r--r--src/core/service.c15
-rw-r--r--src/main.c53
14 files changed, 1303 insertions, 10 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 70a6c32..550f3e0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -146,6 +146,14 @@ if(HYBBX_STORAGE_SQLITE AND HYBBX_BUILD_DAEMON)
endif()
# ---------------------------------------------------------------------------
+# ---------------------------------------------------------------------------
+find_package(PkgConfig QUIET)
+if(PkgConfig_FOUND)
+endif()
+else()
+endif()
+
+# ---------------------------------------------------------------------------
# Platform detection (see docs/PLATFORMS.md)
# ---------------------------------------------------------------------------
if(CMAKE_SYSTEM_NAME STREQUAL "AmigaOS")
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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+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 <string.h>
+#include <strings.h>
+#include <stdio.h>
+#include <ctype.h>
+
+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 <stdio.h>
@@ -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 <errno.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <signal.h>
#include <unistd.h>
#include <libgen.h>
@@ -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;
}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com