summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-09 11:54:17 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-09 11:54:17 +0000
commita989bd16c40e04bba5a5404b0ef0cd22f19d2707 (patch)
tree604a0439c7d8c022cbd483d62bdc2cfc1c8d18f8
parenta72e5ea7558e5172c08d397d6f1e0e0f9273e694 (diff)
now including entertain/chess
-rw-r--r--CMakeLists.txt2
-rw-r--r--_chess_h_test.c1
-rw-r--r--include/hybbx/chess.h106
-rw-r--r--include/hybbx/plugin.h20
-rw-r--r--include/hybbx/service.h10
-rw-r--r--plugins/CMakeLists.txt4
-rw-r--r--plugins/entertain/CMakeLists.txt27
-rw-r--r--plugins/entertain/chess.c552
-rw-r--r--plugins/entertain/chess_cmd.c277
-rw-r--r--plugins/entertain/entertain.c67
-rw-r--r--plugins/entertain/entertain_chess.h88
-rw-r--r--share/commands.yaml4
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/core/chess.c668
-rw-r--r--src/core/chess_cmd.c397
-rw-r--r--src/core/command.c9
-rw-r--r--src/core/service.c42
-rw-r--r--src/main.c6
-rw-r--r--text/news.txt2
-rw-r--r--text/rules.txt2
20 files changed, 1101 insertions, 1185 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 70a6c32..c715036 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -58,6 +58,7 @@ option(HYBBX_PLUGIN_SSH "Build SSH transport (requires libssh)" ON)
option(HYBBX_PLUGIN_BAYCOM "Build BayCom/based transport plugin (default ON)" ON)
option(HYBBX_PLUGIN_MAINS_PROXY "Build Main-to-Main mesh proxy" ON)
option(HYBBX_PLUGIN_WEBSOCKET "Build WebSocket forward-proxy transport" ON)
+option(HYBBX_PLUGIN_ENTERTAIN "Build Entertain plugin (chess)" ON)
option(HYBBX_BUILD_TESTS "Build unit tests" OFF)
option(HYBBX_HARDENING "Enable compiler security hardening" ON)
option(HYBBX_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF)
@@ -181,6 +182,7 @@ if(HYBBX_PLATFORM_AMIGAOS)
set(HYBBX_PLUGIN_MAINS_PROXY OFF CACHE BOOL "AmigaOS build" FORCE)
set(HYBBX_PLUGIN_ARDOP OFF CACHE BOOL "AmigaOS build" FORCE)
set(HYBBX_PLUGIN_CRDOP OFF CACHE BOOL "AmigaOS build" FORCE)
+ set(HYBBX_PLUGIN_ENTERTAIN OFF CACHE BOOL "AmigaOS build" FORCE)
message(STATUS "HyBBX: AmigaOS — hybbx-telnet client (clib2); telnet + packet_radio plugins")
endif()
diff --git a/_chess_h_test.c b/_chess_h_test.c
new file mode 100644
index 0000000..2ad32f7
--- /dev/null
+++ b/_chess_h_test.c
@@ -0,0 +1 @@
+#include "hybbx/chess.h"
diff --git a/include/hybbx/chess.h b/include/hybbx/chess.h
deleted file mode 100644
index bdfaa7b..0000000
--- a/include/hybbx/chess.h
+++ /dev/null
@@ -1,106 +0,0 @@
-#ifndef HYBBX_CHESS_H
-#define HYBBX_CHESS_H
-
-#include "hybbx/types.h"
-#include "hybbx/limits.h"
-#include "hybbx/storage.h"
-#include "hybbx/service.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/include/hybbx/plugin.h b/include/hybbx/plugin.h
index ea72a65..9a51211 100644
--- a/include/hybbx/plugin.h
+++ b/include/hybbx/plugin.h
@@ -9,6 +9,7 @@ extern "C" {
struct hybbx_service;
struct hybbx_session;
+struct hybbx_parsed_command;
/**
* Transport plugin interface (link adapters / host-client bridges).
@@ -19,6 +20,10 @@ struct hybbx_session;
* Core uses TCP/IPv4+IPv6 and HBX internally. Plugins terminate the wire
* to external services (telnet TCP, serial TNC, ARDOP host TCP, …) and
* expose a byte stream to hybbx_session via the write callback.
+ *
+ * Optional extensions (NULL = not used):
+ * tick — called every ~1s from service loop (game clocks, AI, …)
+ * on_command — intercept "/verb" before core dispatch (entertainment, …)
*/
typedef struct hybbx_transport_plugin {
const char *name;
@@ -48,6 +53,21 @@ typedef struct hybbx_transport_plugin {
hybbx_result_t (*write)(struct hybbx_session *session,
const char *data, size_t len);
+ /**
+ * Periodic tick — called every ~1 second from the service loop.
+ * NULL → no periodic work. Used by game clocks, AI timers, etc.
+ */
+ void (*tick)(struct hybbx_service *service);
+
+ /**
+ * Command intercept — called before core command dispatch.
+ * Return HYBBX_OK if handled, HYBBX_ERR_NOT_FOUND to fall through.
+ * NULL → no command handling.
+ */
+ hybbx_result_t (*on_command)(struct hybbx_service *service,
+ struct hybbx_session *session,
+ const struct hybbx_parsed_command *cmd);
+
} hybbx_transport_plugin_t;
/**
diff --git a/include/hybbx/service.h b/include/hybbx/service.h
index 4730b8b..b9aa7fa 100644
--- a/include/hybbx/service.h
+++ b/include/hybbx/service.h
@@ -15,6 +15,8 @@
#include "hybbx/storage.h"
#include "hybbx/texts.h"
+struct hybbx_parsed_command;
+
#ifdef __cplusplus
extern "C" {
#endif
@@ -167,6 +169,14 @@ struct hybbx_session *hybbx_service_find_registered_session(
struct hybbx_circuit_hub;
struct hybbx_circuit_hub *hybbx_service_circuit_hub(hybbx_service_t *service);
+/**
+ * Try plugin on_command interceptors.
+ * Returns HYBBX_OK if a plugin handled the command, HYBBX_ERR_NOT_FOUND otherwise.
+ */
+hybbx_result_t hybbx_service_try_plugin_command(hybbx_service_t *service,
+ struct hybbx_session *session,
+ const struct hybbx_parsed_command *cmd);
+
#ifdef __cplusplus
}
#endif
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index 9736302..161861b 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -42,3 +42,7 @@ endif()
if(HYBBX_PLUGIN_WEBSOCKET)
add_subdirectory(websocket)
endif()
+
+if(HYBBX_PLUGIN_ENTERTAIN)
+ add_subdirectory(entertain)
+endif()
diff --git a/plugins/entertain/CMakeLists.txt b/plugins/entertain/CMakeLists.txt
new file mode 100644
index 0000000..b626028
--- /dev/null
+++ b/plugins/entertain/CMakeLists.txt
@@ -0,0 +1,27 @@
+add_library(hybbx_plugin_entertain STATIC
+ entertain.c
+ chess.c
+ chess_cmd.c
+)
+
+find_package(Threads REQUIRED)
+
+target_include_directories(hybbx_plugin_entertain
+ PRIVATE ${CMAKE_SOURCE_DIR}/include
+ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+target_compile_definitions(hybbx_plugin_entertain
+ PRIVATE HYBBX_PLUGIN_BUILD
+)
+
+target_link_libraries(hybbx_plugin_entertain PRIVATE hybbx_core Threads::Threads)
+hybbx_link_plugin_instances(hybbx_plugin_entertain HYBBX_HAVE_PLUGIN_ENTERTAIN)
+hybbx_apply_hardening(hybbx_plugin_entertain)
+
+if(HYBBX_INSTALL_DEV)
+install(TARGETS hybbx_plugin_entertain
+ ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR}
+ LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR}
+)
+endif()
diff --git a/plugins/entertain/chess.c b/plugins/entertain/chess.c
new file mode 100644
index 0000000..7875d3b
--- /dev/null
+++ b/plugins/entertain/chess.c
@@ -0,0 +1,552 @@
+/*
+ * Entertain plugin — chess.c
+ * Chess game engine: board, moves, check/checkmate, SAN, tick broadcast.
+ */
+#include "entertain_chess.h"
+#include "hybbx/session.h"
+#include "hybbx/log.h"
+#include "hybbx/util.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;
+
+/* ── Session slot tracking (future use) ────────────────────── */
+
+#define SESS_MAX 256u
+static struct { uint64_t id; unsigned slot; } g_sess[SESS_MAX];
+
+unsigned chess_session_slot(const struct hybbx_session *s)
+{
+ uint64_t sid; unsigned i;
+ if (!s) return 0;
+ sid = hybbx_session_id(s);
+ for (i = 0; i < SESS_MAX; i++)
+ if (g_sess[i].id == sid) return g_sess[i].slot;
+ return 0;
+}
+
+void chess_session_set_slot(struct hybbx_session *s, unsigned slot)
+{
+ uint64_t sid; unsigned i, fi = SESS_MAX;
+ if (!s) return;
+ sid = hybbx_session_id(s);
+ for (i = 0; i < SESS_MAX; i++) {
+ if (g_sess[i].id == sid) { g_sess[i].slot = slot; return; }
+ if (g_sess[i].id == 0 && fi == SESS_MAX) fi = i;
+ }
+ if (slot > 0 && fi < SESS_MAX) { g_sess[fi].id = sid; g_sess[fi].slot = slot; }
+}
+
+/* ── Board init ────────────────────────────────────────────── */
+
+static void board_init(chess_board_t *b)
+{
+ static const char bk[] = "RNBQKBNR";
+ unsigned f;
+ memset(b, 0, sizeof(*b));
+ for (f = 0; f < 8; f++) {
+ b->sq[0][f] = bk[f];
+ b->sq[1][f] = 'P';
+ b->sq[6][f] = 'p';
+ b->sq[7][f] = (char)tolower((unsigned char)bk[f]);
+ }
+ for (unsigned r = 2; r < 6; r++)
+ for (f = 0; f < 8; f++)
+ b->sq[r][f] = CHESS_EMPTY;
+}
+
+/* ── Lifecycle ─────────────────────────────────────────────── */
+
+void chess_init(unsigned max_games)
+{
+ unsigned i;
+ memset(g_games, 0, sizeof(g_games));
+ memset(g_sess, 0, sizeof(g_sess));
+ if (max_games == 0 || max_games > CHESS_GAME_MAX)
+ max_games = CHESS_DEFAULT_MAX;
+ g_max_games = max_games;
+ for (i = 0; i < g_max_games; i++) {
+ g_games[i].active = 1;
+ snprintf(g_games[i].game_id, sizeof(g_games[i].game_id), "G%u", i + 1);
+ }
+}
+
+void chess_shutdown(void)
+{
+ unsigned i;
+ for (i = 0; i < CHESS_GAME_MAX; i++) g_games[i].active = 0;
+}
+
+unsigned chess_max_games(void) { return g_max_games; }
+
+/* ── Helpers ───────────────────────────────────────────────── */
+
+static int parse_sq(const char *s, unsigned *r, unsigned *f)
+{
+ if (!s || strlen(s) < 2 || s[0] < 'a' || s[0] > 'h' ||
+ s[1] < '1' || s[1] > '8')
+ return 0;
+ *f = (unsigned)(s[0] - 'a');
+ *r = (unsigned)(s[1] - '1');
+ return 1;
+}
+
+static chess_color_t pcol(char p)
+{
+ if (p == CHESS_EMPTY) return CHESS_COLOR_NONE;
+ return (p >= 'A' && p <= 'Z') ? CHESS_WHITE : CHESS_BLACK;
+}
+
+static char pup(char p) { return (char)toupper((unsigned char)p); }
+static int inb(unsigned r, unsigned f) { return r < 8 && f < 8; }
+
+/* ── Check detection ───────────────────────────────────────── */
+
+static int sq_attacked(const chess_board_t *b, unsigned rk, unsigned fl,
+ chess_color_t by)
+{
+ unsigned r, f;
+ char P, N, B, R, Q, K;
+ if (by == CHESS_WHITE) { P='P'; N='N'; B='B'; R='R'; Q='Q'; K='K'; }
+ else { P='p'; N='n'; B='b'; R='r'; Q='q'; K='k'; }
+
+ /* Pawn */
+ if (by == CHESS_WHITE) {
+ if (rk > 0 && fl > 0 && b->sq[rk-1][fl-1] == P) return 1;
+ if (rk > 0 && fl < 7 && b->sq[rk-1][fl+1] == P) return 1;
+ } else {
+ if (rk < 7 && fl > 0 && b->sq[rk+1][fl-1] == P) return 1;
+ if (rk < 7 && fl < 7 && b->sq[rk+1][fl+1] == P) 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)rk + kr[i], nf = (int)fl + kf[i];
+ if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8)
+ if (b->sq[nr][nf] == N) return 1;
+ }
+ }
+
+ /* Bishop/Queen diagonals */
+ {
+ static const int dr[] = {1,1,-1,-1}, df[] = {1,-1,1,-1};
+ for (unsigned d = 0; d < 4; d++) {
+ r = rk; f = fl;
+ while (1) {
+ r = (unsigned)((int)r + dr[d]);
+ f = (unsigned)((int)f + df[d]);
+ if (!inb(r, f)) break;
+ if (b->sq[r][f] != CHESS_EMPTY) {
+ if (b->sq[r][f] == B || b->sq[r][f] == Q) return 1;
+ break;
+ }
+ }
+ }
+ }
+
+ /* Rook/Queen straights */
+ {
+ static const int dr[] = {1,-1,0,0}, df[] = {0,0,1,-1};
+ for (unsigned d = 0; d < 4; d++) {
+ r = rk; f = fl;
+ while (1) {
+ r = (unsigned)((int)r + dr[d]);
+ f = (unsigned)((int)f + df[d]);
+ if (!inb(r, f)) break;
+ if (b->sq[r][f] != CHESS_EMPTY) {
+ if (b->sq[r][f] == R || b->sq[r][f] == Q) return 1;
+ break;
+ }
+ }
+ }
+ }
+
+ /* King */
+ {
+ static const int dr[] = {1,1,1,0,0,-1,-1,-1};
+ static const int df[] = {1,0,-1,1,-1,1,0,-1};
+ for (unsigned d = 0; d < 8; d++) {
+ int nr = (int)rk + dr[d], nf = (int)fl + df[d];
+ if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8)
+ if (b->sq[nr][nf] == K) return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int find_king(const chess_board_t *b, chess_color_t c,
+ unsigned *kr, unsigned *kf)
+{
+ char k = (c == CHESS_WHITE) ? 'K' : 'k';
+ for (unsigned r = 0; r < 8; r++)
+ for (unsigned f = 0; f < 8; f++)
+ if (b->sq[r][f] == k) { *kr = r; *kf = f; return 1; }
+ return 0;
+}
+
+static int 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 sq_attacked(b, kr, kf, opp);
+}
+
+/* ── Move legality ─────────────────────────────────────────── */
+
+static int pseudo_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 = (side == CHESS_WHITE) ? 1 : 6;
+ int dr = (int)tr - (int)fr, 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 &&
+ b->sq[fr+(unsigned)dir][ff] == CHESS_EMPTY &&
+ b->sq[tr][tf] == CHESS_EMPTY) return 1;
+ }
+ if ((df == 1 || df == -1) && dr == dir && b->sq[tr][tf] != CHESS_EMPTY)
+ return 1;
+ return 0;
+}
+
+static int pseudo_knight(unsigned fr, unsigned ff, unsigned tr, unsigned tf)
+{
+ int dr = abs((int)tr-(int)fr), df = abs((int)tf-(int)ff);
+ return (dr==2&&df==1)||(dr==1&&df==2);
+}
+
+static int slide_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 (!inb(r,f) || b->sq[r][f] != CHESS_EMPTY) return 0;
+ r = (unsigned)((int)r+dr); f = (unsigned)((int)f+df);
+ }
+ return 1;
+}
+
+static int has_any_legal(chess_board_t *b, chess_color_t side);
+
+static int pseudo_legal(const chess_board_t *b, unsigned fr, unsigned ff,
+ unsigned tr, unsigned tf, chess_color_t side)
+{
+ char pc = b->sq[fr][ff];
+ int dr = abs((int)tr-(int)fr), df = abs((int)tf-(int)ff);
+ if (pc == CHESS_EMPTY || pcol(pc) != side) return 0;
+ if (pcol(b->sq[tr][tf]) == side) return 0;
+ switch (pup(pc)) {
+ case 'P': return pseudo_pawn(b, fr, ff, tr, tf, side);
+ case 'N': return pseudo_knight(fr, ff, tr, tf);
+ case 'B': return dr==df && dr>0 && slide_clear(b, fr, ff, tr, tf);
+ case 'R': return (fr==tr||ff==tf) && slide_clear(b, fr, ff, tr, tf);
+ case 'Q': return ((fr==tr||ff==tf)||(dr==df)) && slide_clear(b, fr, ff, tr, tf);
+ case 'K': return dr<=1 && df<=1 && (dr+df>0);
+ }
+ return 0;
+}
+
+static int is_legal(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;
+ if (pup(tmp.sq[tr][tf]) == 'P' &&
+ ((side == CHESS_WHITE && tr == 7) || (side == CHESS_BLACK && tr == 0)))
+ tmp.sq[tr][tf] = (side == CHESS_WHITE) ? 'Q' : 'q';
+ return !in_check(&tmp, side);
+}
+
+static int has_any_legal(chess_board_t *b, chess_color_t side)
+{
+ for (unsigned fr = 0; fr < 8; fr++)
+ for (unsigned ff = 0; ff < 8; ff++) {
+ if (pcol(b->sq[fr][ff]) != side) continue;
+ for (unsigned tr = 0; tr < 8; tr++)
+ for (unsigned tf = 0; tf < 8; tf++)
+ if (is_legal(b, fr, ff, tr, tf, side)) return 1;
+ }
+ return 0;
+}
+
+/* ── SAN ───────────────────────────────────────────────────── */
+
+static void make_san(char *out, size_t len, char pc, unsigned ff,
+ unsigned tr, unsigned tf, int cap, int chk,
+ int mate, char promo)
+{
+ int n;
+ if (pup(pc) == 'P') {
+ if (cap) n = snprintf(out, len, "%cx%c%d", 'a'+(int)ff, 'a'+(int)tf, (int)(tr+1));
+ else n = snprintf(out, len, "%c%d", 'a'+(int)tf, (int)(tr+1));
+ } else {
+ n = snprintf(out, len, "%c%c%d", pup(pc), 'a'+(int)tf, (int)(tr+1));
+ }
+ if (promo) snprintf(out+n, len-(size_t)n, "=%c", pup(promo));
+ else if (mate) snprintf(out+n, len-(size_t)n, "#");
+ else if (chk) snprintf(out+n, len-(size_t)n, "+");
+}
+
+/* ── Game management ───────────────────────────────────────── */
+
+chess_game_t *chess_get_game(unsigned i)
+{
+ return (i < CHESS_GAME_MAX && g_games[i].active) ? &g_games[i] : NULL;
+}
+
+chess_game_t *chess_create_game(struct hybbx_session *white, const char *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, name, CHESS_NAME_MAX);
+ 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, name);
+ return &g_games[i];
+ }
+ }
+ return NULL;
+}
+
+chess_game_t *chess_find_open_game(void)
+{
+ for (unsigned i = 0; i < g_max_games; i++)
+ if (g_games[i].active && !g_games[i].black_session) return &g_games[i];
+ return NULL;
+}
+
+chess_game_t *chess_find_game_by_id(const char *id)
+{
+ if (!id) return NULL;
+ for (unsigned 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 chess_join_game(chess_game_t *g, struct hybbx_session *black, const char *name)
+{
+ if (!g || g->black_session) return 0;
+ g->black_session = black;
+ hybbx_strlcpy(g->black_name, name, CHESS_NAME_MAX);
+ hybbx_log_info("[chess] %s joined %s as black", name, g->game_id);
+ return 1;
+}
+
+int chess_add_spectator(chess_game_t *g, struct hybbx_session *spec)
+{
+ unsigned i;
+ if (!g || !spec) return 0;
+ if (chess_is_player(g, spec)) return 0;
+ for (i = 0; i < g->spectator_count; i++)
+ if (g->spectators[i] == spec) return 1;
+ if (g->spectator_count >= CHESS_SPECTATOR_MAX) return 0;
+ g->spectators[g->spectator_count++] = spec;
+ return 1;
+}
+
+void chess_remove_spectator(chess_game_t *g, struct hybbx_session *spec)
+{
+ unsigned i;
+ if (!g || !spec) return;
+ for (i = 0; i < g->spectator_count; i++) {
+ if (g->spectators[i] == spec) {
+ g->spectators[i] = g->spectators[--g->spectator_count];
+ return;
+ }
+ }
+}
+
+int chess_is_player(const chess_game_t *g, const struct hybbx_session *s)
+{
+ return g && s && (g->white_session == s || g->black_session == s);
+}
+
+int chess_is_spectator(const chess_game_t *g, const struct hybbx_session *s)
+{
+ unsigned i;
+ if (!g || !s) return 0;
+ for (i = 0; i < g->spectator_count; i++)
+ if (g->spectators[i] == s) return 1;
+ return 0;
+}
+
+chess_color_t chess_player_color(const chess_game_t *g, const struct hybbx_session *s)
+{
+ if (!g || !s) return CHESS_COLOR_NONE;
+ if (g->white_session == s) return CHESS_WHITE;
+ if (g->black_session == s) return CHESS_BLACK;
+ return CHESS_COLOR_NONE;
+}
+
+/* ── Move execution ────────────────────────────────────────── */
+
+int chess_make_move(chess_game_t *g, 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 (!g || g->result != CHESS_RESULT_NONE) {
+ if (err) snprintf(err, errlen, "Game is over.");
+ return 0;
+ }
+ side = g->turn;
+ if (!parse_sq(from, &fr, &ff)) { if (err) snprintf(err, errlen, "Bad square: %s", from); return 0; }
+ if (!parse_sq(to, &tr, &tf)) { if (err) snprintf(err, errlen, "Bad square: %s", to); return 0; }
+
+ piece = g->board.sq[fr][ff];
+ if (pcol(piece) != side) { if (err) snprintf(err, errlen, "Not your piece."); return 0; }
+ if (!is_legal(&g->board, fr, ff, tr, tf, side)) { if (err) snprintf(err, errlen, "Illegal move."); return 0; }
+
+ captured = g->board.sq[tr][tf];
+ g->board.sq[tr][tf] = piece;
+ g->board.sq[fr][ff] = CHESS_EMPTY;
+
+ /* Promotion */
+ if (pup(piece) == 'P' &&
+ ((side == CHESS_WHITE && tr == 7) || (side == CHESS_BLACK && tr == 0))) {
+ char pp = promote ? pup(promote) : 'Q';
+ g->board.sq[tr][tf] = (side == CHESS_WHITE) ? pp : (char)tolower((unsigned char)pp);
+ }
+
+ opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE;
+ check = in_check(&g->board, opp);
+ mate = check && !has_any_legal(&g->board, opp);
+
+ make_san(g->last_move_san, sizeof(g->last_move_san), piece, ff, tr, tf,
+ captured != CHESS_EMPTY, check, mate, promote);
+
+ if (mate) g->result = (side == CHESS_WHITE) ? CHESS_WHITE_WINS : CHESS_BLACK_WINS;
+ else if (!has_any_legal(&g->board, opp)) g->result = CHESS_DRAW_STALEMATE;
+
+ g->turn = opp;
+ if (opp == CHESS_WHITE) g->move_number++;
+ g->tick_counter = 0;
+
+ hybbx_log_info("[chess] %s: %s %s", g->game_id,
+ (side == CHESS_WHITE) ? g->white_name : g->black_name,
+ g->last_move_san);
+ return 1;
+}
+
+/* ── Resign / Draw ─────────────────────────────────────────── */
+
+void chess_resign(chess_game_t *g, struct hybbx_session *who)
+{
+ if (!g || g->result != CHESS_RESULT_NONE) return;
+ if (g->white_session == who) g->result = CHESS_BLACK_WINS;
+ else if (g->black_session == who) g->result = CHESS_WHITE_WINS;
+}
+
+void chess_offer_draw(chess_game_t *g, struct hybbx_session *who)
+{
+ (void)who;
+ if (!g || g->result != CHESS_RESULT_NONE) return;
+ g->result = CHESS_DRAW_AGREEMENT;
+}
+
+/* ── Board rendering ───────────────────────────────────────── */
+
+static void render_board(const chess_board_t *b, struct hybbx_session *s,
+ const char *label)
+{
+ char line[96];
+ unsigned r, f;
+ static const char *gc = ".KQRBNPkqrbnp";
+ (void)gc;
+
+ if (label && label[0]) hybbx_session_write_line(s, label);
+ hybbx_session_write_line(s, " +---+---+---+---+---+---+---+---+");
+ for (r = 0; r < 8; r++) {
+ unsigned rk = 7 - r;
+ int n = snprintf(line, sizeof(line), "%u |", rk + 1);
+ for (f = 0; f < 8; f++) {
+ char p = b->sq[rk][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(s, line);
+ hybbx_session_write_line(s, " +---+---+---+---+---+---+---+---+");
+ }
+ hybbx_session_write_line(s, " a b c d e f g h");
+}
+
+void chess_send_board(chess_game_t *g, struct hybbx_session *s)
+{
+ char label[160];
+ const char *turn;
+ if (!g || !s) return;
+ turn = (g->turn == CHESS_WHITE) ? g->white_name : g->black_name;
+ if (g->result != CHESS_RESULT_NONE) {
+ const char *rs;
+ switch (g->result) {
+ case CHESS_WHITE_WINS: rs = "1-0 White wins"; break;
+ case CHESS_BLACK_WINS: rs = "0-1 Black wins"; break;
+ case CHESS_DRAW_STALEMATE: rs = "1/2 Stalemate"; break;
+ case CHESS_DRAW_AGREEMENT: rs = "1/2 Draw"; break;
+ default: rs = "Over"; break;
+ }
+ snprintf(label, sizeof(label), "[%s] %s vs %s %s Move %u",
+ g->game_id, g->white_name, g->black_name, rs, g->move_number);
+ } else {
+ snprintf(label, sizeof(label), "[%s] %s(W) vs %s(B) %s to move %s Move %u",
+ g->game_id, g->white_name, g->black_name, turn,
+ g->last_move_san[0] ? g->last_move_san : "", g->move_number);
+ }
+ render_board(&g->board, s, label);
+}
+
+/* ── Tick — 15s board broadcast ────────────────────────────── */
+
+void 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 || !g->black_session) continue;
+ g->tick_counter++;
+ if (g->tick_counter < CHESS_REFRESH_SEC) continue;
+ g->tick_counter = 0;
+ if (g->white_session) chess_send_board(g, g->white_session);
+ if (g->black_session) chess_send_board(g, g->black_session);
+ for (j = 0; j < g->spectator_count; j++)
+ chess_send_board(g, g->spectators[j]);
+ }
+}
diff --git a/plugins/entertain/chess_cmd.c b/plugins/entertain/chess_cmd.c
new file mode 100644
index 0000000..3e57223
--- /dev/null
+++ b/plugins/entertain/chess_cmd.c
@@ -0,0 +1,277 @@
+/*
+ * Entertain plugin — chess_cmd.c
+ * /chess command handler (join/leave/list/move/resign/draw/clear/status/board).
+ */
+#include "entertain_chess.h"
+#include "hybbx/session.h"
+#include "hybbx/service.h"
+#include "hybbx/command.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+
+/* Forward declaration */
+hybbx_result_t entertain_cmd_chess(struct hybbx_service *service,
+ struct hybbx_session *session,
+ const struct hybbx_parsed_command *cmd);
+
+static int ieq(const char *a, const char *b)
+{
+ if (!a || !b) return 0;
+ while (*a && *b) {
+ char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
+ char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
+ if (ca != cb) return 0;
+ a++; b++;
+ }
+ return *a == *b;
+}
+
+hybbx_result_t entertain_cmd_chess(struct hybbx_service *service,
+ struct hybbx_session *session,
+ const struct hybbx_parsed_command *cmd)
+{
+ const char *sub;
+ 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";
+
+ /* ── help ──────────────────────────────────────────────── */
+ if (ieq(sub, "help") || ieq(sub, "?")) {
+ hybbx_session_write_line(session, "Chess commands:");
+ hybbx_session_write_line(session, " /chess new - Create game (you are White)");
+ hybbx_session_write_line(session, " /chess join [id] - Join game as Black");
+ hybbx_session_write_line(session, " /chess watch [id] - Spectate");
+ hybbx_session_write_line(session, " /chess board - Show board");
+ hybbx_session_write_line(session, " /chess move e2 e4 - Move (alias: /mv e2 e4)");
+ hybbx_session_write_line(session, " /chess resign - Resign");
+ hybbx_session_write_line(session, " /chess draw - Offer draw");
+ hybbx_session_write_line(session, " /chess list - List games + boards");
+ hybbx_session_write_line(session, " /chess leave - Stop watching");
+ hybbx_session_write_line(session, " /chess clear - Clear game history");
+ snprintf(line, sizeof(line), "Board refreshes every %ds for all.", CHESS_REFRESH_SEC);
+ hybbx_session_write_line(session, line);
+ return HYBBX_OK;
+ }
+
+ /* ── status ────────────────────────────────────────────── */
+ if (ieq(sub, "status")) {
+ snprintf(line, sizeof(line), "Chess: %u max games", chess_max_games());
+ hybbx_session_write_line(session, line);
+ return HYBBX_OK;
+ }
+
+ /* ── list ──────────────────────────────────────────────── */
+ if (ieq(sub, "list") || ieq(sub, "ls")) {
+ unsigned found = 0;
+ hybbx_session_write_line(session, "Chess games:");
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *g = chess_get_game(i);
+ if (!g) continue;
+ const char *st = (g->result != CHESS_RESULT_NONE) ? "finished" :
+ (!g->black_session) ? "waiting" : "playing";
+ snprintf(line, sizeof(line), " [%s] %s(W) vs %s(B) %s move %u",
+ g->game_id, g->white_name,
+ g->black_session ? g->black_name : "...",
+ st, g->move_number);
+ hybbx_session_write_line(session, line);
+ chess_send_board(g, session);
+ if (g->spectator_count > 0) {
+ int n = snprintf(line, sizeof(line), " Spectators (%u): ", g->spectator_count);
+ for (unsigned j = 0; j < g->spectator_count; j++) {
+ const char *nm = hybbx_session_display_name(g->spectators[j]);
+ if (j > 0) n += snprintf(line+n, sizeof(line)-(size_t)n, ", ");
+ n += snprintf(line+n, sizeof(line)-(size_t)n, "%s", nm ? nm : "?");
+ }
+ 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;
+ }
+
+ /* ── new ───────────────────────────────────────────────── */
+ if (ieq(sub, "new") || ieq(sub, "create")) {
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *g = chess_get_game(i);
+ if (g && chess_is_player(g, session)) {
+ hybbx_session_write_line(session, "Already in a game. /chess resign first.");
+ return HYBBX_ERR_DENIED;
+ }
+ }
+ chess_game_t *g = chess_create_game(session, hybbx_session_username(session));
+ if (!g) { hybbx_session_write_line(session, "No free game slots."); return HYBBX_ERR_DENIED; }
+ snprintf(line, sizeof(line), "Game %s created. You are White.", g->game_id);
+ hybbx_session_write_line(session, line);
+ hybbx_session_write_line(session, "Waiting for Black to /chess join...");
+ chess_send_board(g, session);
+ return HYBBX_OK;
+ }
+
+ /* ── join ──────────────────────────────────────────────── */
+ if (ieq(sub, "join")) {
+ const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL;
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *g = chess_get_game(i);
+ if (g && chess_is_player(g, session)) {
+ hybbx_session_write_line(session, "Already in a game. /chess resign first.");
+ return HYBBX_ERR_DENIED;
+ }
+ }
+ chess_game_t *g = NULL;
+ if (id) {
+ g = chess_find_game_by_id(id);
+ if (!g || !g->active) { hybbx_session_write_line(session, "Game not found."); return HYBBX_ERR_NOT_FOUND; }
+ if (g->black_session) { hybbx_session_write_line(session, "Game is full."); return HYBBX_ERR_DENIED; }
+ } else {
+ g = chess_find_open_game();
+ if (!g) { hybbx_session_write_line(session, "No open games. /chess new"); return HYBBX_ERR_NOT_FOUND; }
+ }
+ chess_join_game(g, session, hybbx_session_username(session));
+ snprintf(line, sizeof(line), "Joined %s as Black. Game on!", g->game_id);
+ hybbx_session_write_line(session, line);
+ chess_send_board(g, g->white_session);
+ chess_send_board(g, session);
+ return HYBBX_OK;
+ }
+
+ /* ── watch ─────────────────────────────────────────────── */
+ if (ieq(sub, "watch") || ieq(sub, "spectate")) {
+ const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL;
+ chess_game_t *g = NULL;
+ if (id) {
+ g = chess_find_game_by_id(id);
+ } else {
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *t = chess_get_game(i);
+ if (t && t->black_session) { g = t; break; }
+ }
+ }
+ if (!g || !g->active) { hybbx_session_write_line(session, "No game to watch."); return HYBBX_ERR_NOT_FOUND; }
+ if (!chess_add_spectator(g, session)) { hybbx_session_write_line(session, "Spectator list full."); return HYBBX_ERR_DENIED; }
+ snprintf(line, sizeof(line), "Watching %s. Board refreshes every %ds.", g->game_id, CHESS_REFRESH_SEC);
+ hybbx_session_write_line(session, line);
+ chess_send_board(g, session);
+ return HYBBX_OK;
+ }
+
+ /* ── leave ─────────────────────────────────────────────── */
+ if (ieq(sub, "leave") || ieq(sub, "unwatch")) {
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *g = chess_get_game(i);
+ if (g && chess_is_spectator(g, session)) {
+ chess_remove_spectator(g, session);
+ hybbx_session_write_line(session, "Stopped watching.");
+ return HYBBX_OK;
+ }
+ }
+ hybbx_session_write_line(session, "Not watching any game.");
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ /* ── board ─────────────────────────────────────────────── */
+ if (ieq(sub, "board") || ieq(sub, "show")) {
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *g = chess_get_game(i);
+ if (g && (chess_is_player(g, session) || chess_is_spectator(g, session))) {
+ chess_send_board(g, session);
+ return HYBBX_OK;
+ }
+ }
+ hybbx_session_write_line(session, "Not in a game. /chess new, join, or watch.");
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ /* ── move ──────────────────────────────────────────────── */
+ if (ieq(sub, "move") || ieq(sub, "mv") || ieq(sub, "m")) {
+ if (cmd->argc < 3) { hybbx_session_write_line(session, "Usage: /chess move e2 e4"); return HYBBX_ERR_INVALID; }
+ chess_game_t *game = NULL;
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *g = chess_get_game(i);
+ if (g && g->result == CHESS_RESULT_NONE && chess_is_player(g, session)) {
+ if (chess_player_color(g, session) == g->turn) { game = g; break; }
+ }
+ }
+ if (!game) { hybbx_session_write_line(session, "Not your turn or no active game."); return HYBBX_ERR_DENIED; }
+ char err[64];
+ if (!chess_make_move(game, cmd->argv[1], cmd->argv[2], 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) chess_send_board(game, game->white_session);
+ if (game->black_session) chess_send_board(game, game->black_session);
+ for (unsigned j = 0; j < game->spectator_count; j++)
+ chess_send_board(game, game->spectators[j]);
+ /* Game over? */
+ if (game->result != CHESS_RESULT_NONE) {
+ const char *res;
+ 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 (unsigned j = 0; j < game->spectator_count; j++)
+ hybbx_session_write_line(game->spectators[j], line);
+ }
+ return HYBBX_OK;
+ }
+
+ /* ── resign ────────────────────────────────────────────── */
+ if (ieq(sub, "resign")) {
+ chess_game_t *game = NULL;
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *g = chess_get_game(i);
+ if (g && chess_is_player(g, session)) { game = g; break; }
+ }
+ if (!game) { hybbx_session_write_line(session, "Not in a game."); return HYBBX_ERR_NOT_FOUND; }
+ 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);
+ for (unsigned j = 0; j < game->spectator_count; j++)
+ hybbx_session_write_line(game->spectators[j], line);
+ return HYBBX_OK;
+ }
+
+ /* ── draw ──────────────────────────────────────────────── */
+ if (ieq(sub, "draw")) {
+ chess_game_t *game = NULL;
+ for (unsigned i = 0; i < chess_max_games(); i++) {
+ chess_game_t *g = chess_get_game(i);
+ if (g && chess_is_player(g, session)) { game = g; break; }
+ }
+ if (!game) { hybbx_session_write_line(session, "Not in a game."); return HYBBX_ERR_NOT_FOUND; }
+ 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);
+ for (unsigned j = 0; j < game->spectator_count; j++)
+ hybbx_session_write_line(game->spectators[j], line);
+ return HYBBX_OK;
+ }
+
+ /* ── clear ─────────────────────────────────────────────── */
+ if (ieq(sub, "clear") || ieq(sub, "reset")) {
+ hybbx_session_write_line(session, "Chess has no persistent history to clear.");
+ return HYBBX_OK;
+ }
+
+ hybbx_session_write_line(session, "Unknown chess command. /chess help");
+ return HYBBX_ERR_NOT_FOUND;
+}
diff --git a/plugins/entertain/entertain.c b/plugins/entertain/entertain.c
new file mode 100644
index 0000000..cf3e123
--- /dev/null
+++ b/plugins/entertain/entertain.c
@@ -0,0 +1,67 @@
+/*
+ * Entertain plugin — entertain.c
+ * Plugin registration: init, shutdown, tick, on_command.
+ * Chess is the first entertainment feature; more can be added.
+ */
+#include "hybbx/plugin.h"
+#include "hybbx/service.h"
+#include "hybbx/command.h"
+#include "hybbx/log.h"
+#include "entertain_chess.h"
+
+#include <string.h>
+
+static struct hybbx_service *g_service;
+
+/* Forward declarations from chess_cmd.c */
+extern hybbx_result_t entertain_cmd_chess(struct hybbx_service *service,
+ struct hybbx_session *session,
+ const struct hybbx_parsed_command *cmd);
+
+static hybbx_result_t entertain_init(struct hybbx_service *service)
+{
+ g_service = service;
+ chess_init(CHESS_DEFAULT_MAX);
+ hybbx_log_info("[entertain] plugin loaded — chess_max_games=%u", (unsigned)CHESS_DEFAULT_MAX);
+ return HYBBX_OK;
+}
+
+static void entertain_shutdown(void)
+{
+ chess_shutdown();
+ g_service = NULL;
+ hybbx_log_info("[entertain] plugin unloaded");
+}
+
+static void entertain_tick(struct hybbx_service *service)
+{
+ chess_tick(service);
+}
+
+static hybbx_result_t entertain_on_command(struct hybbx_service *service,
+ struct hybbx_session *session,
+ const struct hybbx_parsed_command *cmd)
+{
+ if (cmd == NULL || cmd->verb == NULL) {
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ if (strcmp(cmd->verb, "chess") == 0 || strcmp(cmd->verb, "play") == 0) {
+ return entertain_cmd_chess(service, session, cmd);
+ }
+
+ return HYBBX_ERR_NOT_FOUND;
+}
+
+const hybbx_transport_plugin_t hybbx_plugin_entertain = {
+ .name = "entertain",
+ .kind = 0, /* not a transport — feature plugin */
+ .version = 1,
+ .init = entertain_init,
+ .shutdown = entertain_shutdown,
+ .start = NULL,
+ .stop = NULL,
+ .write = NULL,
+ .tick = entertain_tick,
+ .on_command = entertain_on_command,
+};
diff --git a/plugins/entertain/entertain_chess.h b/plugins/entertain/entertain_chess.h
new file mode 100644
index 0000000..9d7026c
--- /dev/null
+++ b/plugins/entertain/entertain_chess.h
@@ -0,0 +1,88 @@
+/*
+ * Entertain plugin — chess.h (internal)
+ * Chess game engine for HyBBX Entertain area.
+ */
+#ifndef ENTERTAIN_CHESS_H
+#define ENTERTAIN_CHESS_H
+
+#include "hybbx/types.h"
+
+struct hybbx_session;
+struct hybbx_service;
+
+#define CHESS_BOARD_SIZE 8
+#define CHESS_GAME_MAX 16
+#define CHESS_SPECTATOR_MAX 32
+#define CHESS_REFRESH_SEC 15
+#define CHESS_DEFAULT_MAX 8
+#define CHESS_NAME_MAX 32
+#define CHESS_SAN_MAX 8
+#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[CHESS_NAME_MAX];
+ char black_name[CHESS_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;
+
+/* Lifecycle */
+void chess_init(unsigned max_games);
+void chess_shutdown(void);
+unsigned chess_max_games(void);
+
+/* Game management */
+chess_game_t *chess_create_game(struct hybbx_session *white, const char *name);
+chess_game_t *chess_find_open_game(void);
+chess_game_t *chess_find_game_by_id(const char *id);
+chess_game_t *chess_get_game(unsigned index);
+int chess_join_game(chess_game_t *g, struct hybbx_session *black, const char *name);
+int chess_add_spectator(chess_game_t *g, struct hybbx_session *spec);
+void chess_remove_spectator(chess_game_t *g, struct hybbx_session *spec);
+int chess_is_player(const chess_game_t *g, const struct hybbx_session *s);
+int chess_is_spectator(const chess_game_t *g, const struct hybbx_session *s);
+chess_color_t chess_player_color(const chess_game_t *g, const struct hybbx_session *s);
+
+/* Session slot (for future use) */
+unsigned chess_session_slot(const struct hybbx_session *s);
+void chess_session_set_slot(struct hybbx_session *s, unsigned slot);
+
+/* Moves + display */
+int chess_make_move(chess_game_t *g, const char *from, const char *to,
+ char promote, char *err, size_t errlen);
+void chess_send_board(chess_game_t *g, struct hybbx_session *s);
+void chess_resign(chess_game_t *g, struct hybbx_session *who);
+void chess_offer_draw(chess_game_t *g, struct hybbx_session *who);
+
+/* Tick — call from plugin tick every 1s */
+void chess_tick(struct hybbx_service *service);
+
+#endif /* ENTERTAIN_CHESS_H */
diff --git a/share/commands.yaml b/share/commands.yaml
index 5a5d848..c64352e 100644
--- a/share/commands.yaml
+++ b/share/commands.yaml
@@ -310,8 +310,10 @@ commands:
chess:
group: entertain
- min: User
+ min: Guest
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 e966362..9b71bcc 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -40,8 +40,6 @@ 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
diff --git a/src/core/chess.c b/src/core/chess.c
deleted file mode 100644
index 94d35a6..0000000
--- a/src/core/chess.c
+++ /dev/null
@@ -1,668 +0,0 @@
-#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
deleted file mode 100644
index 6ad1058..0000000
--- a/src/core/chess_cmd.c
+++ /dev/null
@@ -1,397 +0,0 @@
-/* ── 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 8cd4e5c..3614821 100644
--- a/src/core/command.c
+++ b/src/core/command.c
@@ -18,7 +18,6 @@
#include "hybbx/password.h"
#include "hybbx/traffic.h"
#include "hybbx/monitor.h"
-#include "hybbx/chess.h"
#include "hybbx/util.h"
#include <stdio.h>
@@ -2304,6 +2303,11 @@ hybbx_result_t hybbx_command_dispatch(hybbx_service_t *service,
return cmd_rules(service, session);
}
+ /* Plugin command intercept — try all plugins first */
+ if (hybbx_service_try_plugin_command(service, session, cmd) == HYBBX_OK) {
+ return HYBBX_OK;
+ }
+
if (str_ieq(cmd->verb, "who") || str_ieq(cmd->verb, "online")) {
return cmd_who(service, session);
}
@@ -2312,9 +2316,6 @@ 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")) {
diff --git a/src/core/service.c b/src/core/service.c
index 6cf2b16..20934a7 100644
--- a/src/core/service.c
+++ b/src/core/service.c
@@ -7,6 +7,7 @@
#include "storage_private.h"
#include "hybbx/registry.h"
#include "hybbx/config.h"
+#include "hybbx/command.h"
#include "hybbx/crypto_config.h"
#include "hybbx/traffic.h"
#include "hybbx/circuit_tcp.h"
@@ -26,7 +27,6 @@ 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"
@@ -168,7 +168,6 @@ 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();
@@ -989,13 +988,22 @@ hybbx_result_t hybbx_service_run(hybbx_service_t *service)
health_tick++;
if (health_tick >= 300u) {
health_tick = 0;
- hybbx_log_info("[service] health: running=%d transports=%u sessions_active (tick)",
+ hybbx_log_info("[service] health: running=%d transports=%zu sessions_active (tick)",
svc->running, svc->transport_count);
}
hybbx_security_ban_tick();
hybbx_monitor_tick(service);
- hybbx_chess_tick(service);
+ /* Plugin ticks — iterate all registered plugins */
+ {
+ size_t pi;
+ for (pi = 0; pi < svc->transport_count; pi++) {
+ const hybbx_transport_plugin_t *p = svc->transports[pi].plugin;
+ if (p != NULL && p->tick != NULL) {
+ p->tick(service);
+ }
+ }
+ }
hybbx_broadcast_ax25_tick(service);
#ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY
hybbx_mains_proxy_plugin_tick();
@@ -1016,6 +1024,31 @@ hybbx_result_t hybbx_service_run(hybbx_service_t *service)
return HYBBX_OK;
}
+hybbx_result_t hybbx_service_try_plugin_command(hybbx_service_t *service,
+ struct hybbx_session *session,
+ const struct hybbx_parsed_command *cmd)
+{
+ struct hybbx_service_internal *svc =
+ (struct hybbx_service_internal *)service;
+ size_t i;
+
+ if (svc == NULL || cmd == NULL) {
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ for (i = 0; i < svc->transport_count; i++) {
+ const hybbx_transport_plugin_t *p = svc->transports[i].plugin;
+ if (p != NULL && p->on_command != NULL) {
+ hybbx_result_t rc = p->on_command(service, session, cmd);
+ if (rc == HYBBX_OK) {
+ return HYBBX_OK;
+ }
+ }
+ }
+
+ return HYBBX_ERR_NOT_FOUND;
+}
+
void hybbx_service_stop(hybbx_service_t *service)
{
struct hybbx_service_internal *svc =
@@ -1223,7 +1256,6 @@ 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 c91982e..3d46def 100644
--- a/src/main.c
+++ b/src/main.c
@@ -45,6 +45,9 @@ extern const hybbx_transport_plugin_t hybbx_plugin_ssh;
#ifdef HYBBX_HAVE_PLUGIN_WEBSOCKET
extern const hybbx_transport_plugin_t hybbx_plugin_websocket;
#endif
+#ifdef HYBBX_HAVE_PLUGIN_ENTERTAIN
+extern const hybbx_transport_plugin_t hybbx_plugin_entertain;
+#endif
#ifdef HYBBX_HAVE_PLUGIN_BAYCOM
extern const hybbx_transport_plugin_t hybbx_plugin_baycom;
#endif
@@ -73,6 +76,9 @@ static void register_builtin_plugins(void)
#ifdef HYBBX_HAVE_PLUGIN_WEBSOCKET
hybbx_registry_register(&hybbx_plugin_websocket);
#endif
+#ifdef HYBBX_HAVE_PLUGIN_ENTERTAIN
+ hybbx_registry_register(&hybbx_plugin_entertain);
+#endif
#ifdef HYBBX_HAVE_PLUGIN_BAYCOM
hybbx_registry_register(&hybbx_plugin_baycom);
#endif
diff --git a/text/news.txt b/text/news.txt
index 9823220..6fbe11f 100644
--- a/text/news.txt
+++ b/text/news.txt
@@ -1 +1 @@
-HyBBX 2.4.1 — Menu/index: General command list wraps at 80 columns (/session /version visible). BayCom: optional transport plugin (CMake ON; example INI `baycom=no`). MAX25 attach: `kiss_entry=none`, `[max25] check=yes`. Includes v2.0.0-p2 baseline (menu registry, TNC recovery, broadcast fixes).
+HyBBX 2.8.0 — Menu/index: General command list wraps at 80 columns (/session /version visible). BayCom: optional transport plugin (CMake ON; example INI `baycom=no`). MAX25 attach: `kiss_entry=none`, `[max25] check=yes`. Includes v2.0.0-p2 baseline (menu registry, TNC recovery, broadcast fixes).
diff --git a/text/rules.txt b/text/rules.txt
index 837efbc..12a4a99 100644
--- a/text/rules.txt
+++ b/text/rules.txt
@@ -1,4 +1,4 @@
-HyBBX — Terms of use (v2.4.0)
+HyBBX — Terms of use (v2.8.0)
HyBBX is a session service for low-bandwidth links: mail, chat,
conference, and commands over telnet, SSH, or WebSocket. RF paths
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com