diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-09 12:22:53 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-09 12:22:53 +0000 |
| commit | e1730d8012fca0a712de588285342f7ad10dd804 (patch) | |
| tree | b351ae5da04fe10a211be772636b2bf8de6d49dd | |
| parent | a989bd16c40e04bba5a5404b0ef0cd22f19d2707 (diff) | |
now including entertain/chess
| -rw-r--r-- | docs/ENTERTAIN.md | 43 | ||||
| -rw-r--r-- | docs/MASTER-GUIDE.md | 2 | ||||
| -rw-r--r-- | include/hybbx/networks.h | 2 | ||||
| -rw-r--r-- | plugins/entertain/chess.c | 801 | ||||
| -rw-r--r-- | plugins/entertain/chess_cmd.c | 500 | ||||
| -rw-r--r-- | plugins/entertain/entertain.c | 40 | ||||
| -rw-r--r-- | plugins/entertain/entertain_chess.h | 26 | ||||
| -rw-r--r-- | share/commands.yaml | 10 | ||||
| -rw-r--r-- | share/hybbx-main.ini.example | 2 | ||||
| -rw-r--r-- | share/hybbx-standalone.ini.example | 2 | ||||
| -rw-r--r-- | share/hybbxd-main.ini.example | 2 | ||||
| -rw-r--r-- | src/core/command.c | 19 | ||||
| -rw-r--r-- | src/core/instance.c | 23 | ||||
| -rw-r--r-- | src/core/networks.c | 15 | ||||
| -rw-r--r-- | src/core/service.c | 12 |
15 files changed, 1062 insertions, 437 deletions
diff --git a/docs/ENTERTAIN.md b/docs/ENTERTAIN.md index cbdec78..f6b9667 100644 --- a/docs/ENTERTAIN.md +++ b/docs/ENTERTAIN.md @@ -1,21 +1,37 @@ -# Entertain Area · HyBBX 2.8.0 +# Entertain Area · HyBBX -Optional Main-only plugins for games and apps — not on Secondary RF hosts. +Optional **Main / standalone Main** feature plugin for games (chess first). +Not loaded on Secondary or Proxy. AmigaOS builds force the plugin **OFF** at CMake. -## Plugin matrix +## Enable -| Plugin | Status | Notes | -|--------|--------|-------| -| Entertain Area framework | opt-in CMake | Main hosts only | -| Chess / games | deferred | future or doc removal | +| Layer | Setting | +|-------|---------| +| Build | `-DHYBBX_PLUGIN_ENTERTAIN=ON` (default on POSIX; forced off on AmigaOS) | +| INI | `[networks] entertain=yes` | +| Role | `hybbxd` Main (or standalone Main) | -## Config matrix +```ini +[networks] +entertain = yes +``` -| Item | Value | -|------|-------| -| Enable | `-DHYBBX_PLUGIN_ENTERTAIN=ON` (when available) | -| Host role | Main only — Secondary runs RF transports | -| User access | Level-gated via `[areas]` / `share/areas.yaml` | +Log line on success: `[entertain] plugin loaded — chess_max_games=…` + +## Chess + +| Command | Action | +|---------|--------| +| `/chess help` | Command list | +| `/chess new` | Create game (White) | +| `/chess join [id]` | Join as Black | +| `/chess watch [id]` | Spectate | +| `/chess move e2 e4` | Move (`/mv e2 e4` or `/mv e2e4`) | +| `/chess resign` / `/chess draw` | Resign / offer-accept draw | +| `/chess list` / `/chess board` / `/chess clear` | List / show / free finished slots | +| `/play …` | Alias for `/chess` | + +Limits: no castling, no en passant; pawn promotes to Q/R/B/N. ASCII board for telnet/SSH. ## Related @@ -23,3 +39,4 @@ Optional Main-only plugins for games and apps — not on Secondary RF hosts. |------|-----| | Topology | [TOPOLOGY.md](TOPOLOGY.md) | | Commands | [COMMANDS.md](COMMANDS.md) | +| Areas | `share/areas.yaml` · Entertain menu (User+) | diff --git a/docs/MASTER-GUIDE.md b/docs/MASTER-GUIDE.md index 53895af..b6d025d 100644 --- a/docs/MASTER-GUIDE.md +++ b/docs/MASTER-GUIDE.md @@ -61,7 +61,7 @@ Full key tables: frozen `docs/MANUAL.md`. Vault INI copy (reference station): `s |---------|---------| | `[service]` | Name, session limit, prompt | | `[storage]` | `flatfile` or `sqlite`; user shards under `users/` | -| `[networks]` | Enable plugins: `ax25`, `baycom`, `crdop`, `circuit`, `ssh`, `websocket` | +| `[networks]` | Enable plugins: `ax25`, `baycom`, `crdop`, `circuit`, `ssh`, `websocket`, `entertain` (Main chess) | | `[transport.telnet]` | Bind, port `2323` | | `[transport.circuit]` | HBX hub port `7323`, `max_links` (default 8, max 16) | | `[transport.packet_radioN]` | TNC instance — device, `tnc=` profile, `protocol=kiss` | diff --git a/include/hybbx/networks.h b/include/hybbx/networks.h index ab8e954..b58642d 100644 --- a/include/hybbx/networks.h +++ b/include/hybbx/networks.h @@ -35,6 +35,8 @@ typedef struct hybbx_networks_config { int circuit; /** Main-to-Main mesh (`transport.mains_proxy`) — Secondary primary. */ int mains_proxy; + /** Entertain feature plugin (chess, …) — Main / standalone Main only. */ + int entertain; } hybbx_networks_config_t; void hybbx_networks_config_defaults(hybbx_networks_config_t *networks); diff --git a/plugins/entertain/chess.c b/plugins/entertain/chess.c index 7875d3b..bbf077b 100644 --- a/plugins/entertain/chess.c +++ b/plugins/entertain/chess.c @@ -1,6 +1,6 @@ /* * Entertain plugin — chess.c - * Chess game engine: board, moves, check/checkmate, SAN, tick broadcast. + * Board, move legality (no castling / en passant), checkmate, SAN, tick. */ #include "entertain_chess.h" #include "hybbx/session.h" @@ -15,39 +15,13 @@ 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]; @@ -55,9 +29,11 @@ static void board_init(chess_board_t *b) 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++) + for (unsigned r = 2; r < 6; r++) { + for (f = 0; f < 8; f++) { b->sq[r][f] = CHESS_EMPTY; + } + } } /* ── Lifecycle ─────────────────────────────────────────────── */ @@ -65,118 +41,194 @@ static void board_init(chess_board_t *b) 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) + 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; + for (i = 0; i < CHESS_GAME_MAX; i++) { snprintf(g_games[i].game_id, sizeof(g_games[i].game_id), "G%u", i + 1); + g_games[i].active = 0; } } void chess_shutdown(void) { unsigned i; - for (i = 0; i < CHESS_GAME_MAX; i++) g_games[i].active = 0; + + for (i = 0; i < CHESS_GAME_MAX; i++) { + g_games[i].active = 0; + } } -unsigned chess_max_games(void) { return g_max_games; } +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') + char c0; + char c1; + + if (s == NULL || s[0] == '\0' || s[1] == '\0') { + return 0; + } + c0 = (char)tolower((unsigned char)s[0]); + c1 = s[1]; + if (c0 < 'a' || c0 > 'h' || c1 < '1' || c1 > '8') { return 0; - *f = (unsigned)(s[0] - 'a'); - *r = (unsigned)(s[1] - '1'); + } + *f = (unsigned)(c0 - 'a'); + *r = (unsigned)(c1 - '1'); return 1; } static chess_color_t pcol(char p) { - if (p == CHESS_EMPTY) return CHESS_COLOR_NONE; + 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; } +static char pup(char p) +{ + return (char)toupper((unsigned char)p); +} + +static int inb(unsigned r, unsigned f) +{ + return r < 8 && f < 8; +} + +static const char *color_name(chess_color_t c) +{ + if (c == CHESS_WHITE) { + return "White"; + } + if (c == CHESS_BLACK) { + return "Black"; + } + return "?"; +} /* ── 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'; } + unsigned r; + unsigned f; + char P; + char N; + char B; + char R; + char Q; + char 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; + 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'; + } + + 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; + 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; + static const int kr[] = {2, 2, -2, -2, 1, 1, -1, -1}; + static const int kf[] = {1, -1, 1, -1, 2, -2, 2, -2}; + unsigned i; + + for (i = 0; i < 8; i++) { + int nr = (int)rk + kr[i]; + int nf = (int)fl + kf[i]; + + if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8 && + 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) { + static const int dr[] = {1, 1, -1, -1}; + static const int df[] = {1, -1, 1, -1}; + unsigned d; + + for (d = 0; d < 4; d++) { + r = rk; + f = fl; + for (;;) { r = (unsigned)((int)r + dr[d]); f = (unsigned)((int)f + df[d]); - if (!inb(r, f)) break; + 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; + 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) { + static const int dr[] = {1, -1, 0, 0}; + static const int df[] = {0, 0, 1, -1}; + unsigned d; + + for (d = 0; d < 4; d++) { + r = rk; + f = fl; + for (;;) { r = (unsigned)((int)r + dr[d]); f = (unsigned)((int)f + df[d]); - if (!inb(r, f)) break; + 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; + 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; + static const int dr[] = {1, 1, 1, 0, 0, -1, -1, -1}; + static const int df[] = {1, 0, -1, 1, -1, 1, 0, -1}; + unsigned d; + + for (d = 0; d < 8; d++) { + int nr = (int)rk + dr[d]; + int nf = (int)fl + df[d]; + + if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8 && + b->sq[nr][nf] == K) { + return 1; + } } } @@ -187,17 +239,30 @@ 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; } + unsigned r; + unsigned f; + + for (r = 0; r < 8; r++) { + for (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; + unsigned kr; + unsigned kf; chess_color_t opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE; - if (!find_king(b, side, &kr, &kf)) return 0; + + if (!find_king(b, side, &kr, &kf)) { + return 0; + } return sq_attacked(b, kr, kf, opp); } @@ -208,55 +273,93 @@ static int pseudo_pawn(const chess_board_t *b, unsigned fr, unsigned ff, { 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; + 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 && - b->sq[fr+(unsigned)dir][ff] == CHESS_EMPTY && - b->sq[tr][tf] == CHESS_EMPTY) return 1; + 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) + 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); + 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 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); + int dr = 0; + int df = 0; + unsigned r; + unsigned 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); + 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; + int dr = abs((int)tr - (int)fr); + int 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); + 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); + default: + break; } return 0; } @@ -265,25 +368,42 @@ 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; + + 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))) + ((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; + unsigned fr; + unsigned ff; + unsigned tr; + unsigned tf; + + for (fr = 0; fr < 8; fr++) { + for (ff = 0; ff < 8; ff++) { + if (pcol(b->sq[fr][ff]) != side) { + continue; + } + for (tr = 0; tr < 8; tr++) { + for (tf = 0; tf < 8; tf++) { + if (is_legal(b, fr, ff, tr, tf, side)) { + return 1; + } + } + } } + } return 0; } @@ -294,77 +414,182 @@ static void make_san(char *out, size_t len, char pc, unsigned ff, 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)); + 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)); + n = snprintf(out, len, "%c%c%d", pup(pc), 'a' + (int)tf, + (int)(tr + 1)); + } + if (n < 0) { + n = 0; + } + 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, "+"); } - 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; + if (i >= g_max_games || !g_games[i].active) { + return NULL; + } + return &g_games[i]; +} + +void chess_release_game(chess_game_t *g) +{ + char id[8]; + + if (g == NULL) { + return; + } + hybbx_strlcpy(id, g->game_id, sizeof(id)); + memset(g, 0, sizeof(*g)); + hybbx_strlcpy(g->game_id, id, sizeof(g->game_id)); + g->active = 0; +} + +unsigned chess_clear_finished(void) +{ + unsigned i; + unsigned n = 0; + + for (i = 0; i < g_max_games; i++) { + if (g_games[i].active && g_games[i].result != CHESS_RESULT_NONE) { + chess_release_game(&g_games[i]); + n++; + } + } + return n; } chess_game_t *chess_create_game(struct hybbx_session *white, const char *name) { unsigned i; + chess_game_t *slot = NULL; + 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]; + slot = &g_games[i]; + break; + } + if (g_games[i].result != CHESS_RESULT_NONE) { + slot = &g_games[i]; + break; } } - return NULL; + if (slot == NULL) { + return NULL; + } + + { + char id[8]; + + hybbx_strlcpy(id, slot->game_id, sizeof(id)); + memset(slot, 0, sizeof(*slot)); + hybbx_strlcpy(slot->game_id, id, sizeof(slot->game_id)); + } + slot->active = 1; + hybbx_strlcpy(slot->white_name, name != NULL ? name : "?", CHESS_NAME_MAX); + slot->white_session = white; + board_init(&slot->board); + slot->turn = CHESS_WHITE; + slot->move_number = 1; + hybbx_log_info("[chess] game %s created by %s", slot->game_id, + slot->white_name); + return slot; } 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]; + unsigned i; + + for (i = 0; i < g_max_games; i++) { + if (g_games[i].active && g_games[i].black_session == NULL && + g_games[i].result == CHESS_RESULT_NONE) { + 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) + 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; +} + +chess_game_t *chess_find_player_game(const struct hybbx_session *s) +{ + unsigned i; + + if (s == NULL) { + return NULL; + } + for (i = 0; i < g_max_games; i++) { + if (g_games[i].active && chess_is_player(&g_games[i], s)) { return &g_games[i]; + } + } return NULL; } -int chess_join_game(chess_game_t *g, struct hybbx_session *black, const char *name) +int chess_join_game(chess_game_t *g, struct hybbx_session *black, + const char *name) { - if (!g || g->black_session) return 0; + if (g == NULL || black == NULL || g->black_session != NULL || + g->result != CHESS_RESULT_NONE) { + return 0; + } + if (g->white_session == black) { + 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); + hybbx_strlcpy(g->black_name, name != NULL ? name : "?", CHESS_NAME_MAX); + hybbx_log_info("[chess] %s joined %s as black", g->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; + + if (g == NULL || spec == NULL) { + 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; } @@ -372,7 +597,10 @@ int chess_add_spectator(chess_game_t *g, struct hybbx_session *spec) void chess_remove_spectator(chess_game_t *g, struct hybbx_session *spec) { unsigned i; - if (!g || !spec) return; + + if (g == NULL || spec == NULL) { + return; + } for (i = 0; i < g->spectator_count; i++) { if (g->spectators[i] == spec) { g->spectators[i] = g->spectators[--g->spectator_count]; @@ -383,23 +611,37 @@ 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) { - return g && s && (g->white_session == s || g->black_session == s); + return g != NULL && s != NULL && + (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; + + if (g == NULL || s == NULL) { + 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) +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; + if (g == NULL || s == NULL) { + return CHESS_COLOR_NONE; + } + if (g->white_session == s) { + return CHESS_WHITE; + } + if (g->black_session == s) { + return CHESS_BLACK; + } return CHESS_COLOR_NONE; } @@ -408,32 +650,74 @@ chess_color_t chess_player_color(const chess_game_t *g, const struct hybbx_sessi 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; + unsigned fr; + unsigned ff; + unsigned tr; + unsigned tf; + char piece; + char captured; + int check; + int mate; + chess_color_t side; + chess_color_t opp; + char promo_piece = 0; - if (!g || g->result != CHESS_RESULT_NONE) { - if (err) snprintf(err, errlen, "Game is over."); + if (g == NULL || g->result != CHESS_RESULT_NONE) { + if (err != NULL) { + snprintf(err, errlen, "Game is over"); + } return 0; } + if (g->black_session == NULL) { + if (err != NULL) { + snprintf(err, errlen, "Waiting for Black"); + } + 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; } + if (!parse_sq(from, &fr, &ff)) { + if (err != NULL) { + snprintf(err, errlen, "Bad square '%s'", from != NULL ? from : ""); + } + return 0; + } + if (!parse_sq(to, &tr, &tf)) { + if (err != NULL) { + snprintf(err, errlen, "Bad square '%s'", to != NULL ? 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; } + if (pcol(piece) != side) { + if (err != NULL) { + snprintf(err, errlen, "Not your piece"); + } + return 0; + } + if (!is_legal(&g->board, fr, ff, tr, tf, side)) { + if (err != NULL) { + 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))) { + ((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); + + if (pp != 'Q' && pp != 'R' && pp != 'B' && pp != 'N') { + pp = 'Q'; + } + promo_piece = pp; + g->board.sq[tr][tf] = + (side == CHESS_WHITE) ? pp : (char)tolower((unsigned char)pp); } opp = (side == CHESS_WHITE) ? CHESS_BLACK : CHESS_WHITE; @@ -441,14 +725,20 @@ int chess_make_move(chess_game_t *g, const char *from, const char *to, 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); + captured != CHESS_EMPTY, check, mate, promo_piece); - 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; + 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++; + if (opp == CHESS_WHITE) { + g->move_number++; + } g->tick_counter = 0; + g->draw_offered_by = CHESS_COLOR_NONE; hybbx_log_info("[chess] %s: %s %s", g->game_id, (side == CHESS_WHITE) ? g->white_name : g->black_name, @@ -460,16 +750,36 @@ int chess_make_move(chess_game_t *g, const char *from, const char *to, 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; + if (g == NULL || 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; + } + g->draw_offered_by = CHESS_COLOR_NONE; } -void chess_offer_draw(chess_game_t *g, struct hybbx_session *who) +int 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; + chess_color_t me; + + if (g == NULL || who == NULL || g->result != CHESS_RESULT_NONE) { + return 0; + } + me = chess_player_color(g, who); + if (me == CHESS_COLOR_NONE) { + return 0; + } + if (g->draw_offered_by != CHESS_COLOR_NONE && + g->draw_offered_by != me) { + g->result = CHESS_DRAW_AGREEMENT; + g->draw_offered_by = CHESS_COLOR_NONE; + return 1; + } + g->draw_offered_by = me; + return 0; } /* ── Board rendering ───────────────────────────────────────── */ @@ -478,75 +788,120 @@ 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; + unsigned r; + unsigned f; - if (label && label[0]) hybbx_session_write_line(s, label); - hybbx_session_write_line(s, " +---+---+---+---+---+---+---+---+"); + if (label != NULL && label[0] != '\0') { + hybbx_session_write_line(s, label); + } + hybbx_session_write_line(s, " a b c d e f g h"); for (r = 0; r < 8; r++) { unsigned rk = 7 - r; - int n = snprintf(line, sizeof(line), "%u |", rk + 1); + 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); + char ch = (p == CHESS_EMPTY) ? '.' : p; + + n += snprintf(line + n, sizeof(line) - (size_t)n, "%c ", ch); } + n += snprintf(line + n, sizeof(line) - (size_t)n, "%u", rk + 1); hybbx_session_write_line(s, line); - hybbx_session_write_line(s, " +---+---+---+---+---+---+---+---+"); } - hybbx_session_write_line(s, " a b c d e f g h"); + hybbx_session_write_line(s, " a b c d e f g h"); + hybbx_session_write_line(s, " (White=UPPER Black=lower)"); } 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; + char label[192]; + const char *turn_name; + + if (g == NULL || s == NULL) { + return; + } + turn_name = (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; + 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-1/2 Stalemate"; + break; + case CHESS_DRAW_AGREEMENT: + rs = "1/2-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); + g->game_id, g->white_name, + g->black_session ? 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); + snprintf(label, sizeof(label), + "[%s] %s(W) vs %s(B) %s to move%s%s Move %u", + g->game_id, g->white_name, + g->black_session ? g->black_name : "...", turn_name, + g->last_move_san[0] ? " last=" : "", + g->last_move_san[0] ? g->last_move_san : "", + g->move_number); } render_board(&g->board, s, label); + if (g->draw_offered_by != CHESS_COLOR_NONE && + g->result == CHESS_RESULT_NONE) { + snprintf(label, sizeof(label), "Draw offered by %s — /chess draw to accept", + color_name(g->draw_offered_by)); + hybbx_session_write_line(s, label); + } } -/* ── Tick — 15s board broadcast ────────────────────────────── */ +void chess_broadcast_board(chess_game_t *g) +{ + unsigned j; + + if (g == NULL) { + return; + } + if (g->white_session != NULL) { + chess_send_board(g, g->white_session); + } + if (g->black_session != NULL) { + chess_send_board(g, g->black_session); + } + for (j = 0; j < g->spectator_count; j++) { + chess_send_board(g, g->spectators[j]); + } +} + +/* ── Tick — board refresh for spectators ───────────────────── */ void chess_tick(struct hybbx_service *service) { - unsigned i, j; + unsigned i; + unsigned 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; + + if (!g->active || g->black_session == NULL || + g->result != CHESS_RESULT_NONE || g->spectator_count == 0) { + continue; + } g->tick_counter++; - if (g->tick_counter < CHESS_REFRESH_SEC) continue; + 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++) + 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 index 3e57223..fdfcfed 100644 --- a/plugins/entertain/chess_cmd.c +++ b/plugins/entertain/chess_cmd.c @@ -1,39 +1,201 @@ /* * Entertain plugin — chess_cmd.c - * /chess command handler (join/leave/list/move/resign/draw/clear/status/board). + * /chess · /play · /mv command handler. */ #include "entertain_chess.h" #include "hybbx/session.h" #include "hybbx/service.h" #include "hybbx/command.h" +#include <ctype.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++; + if (a == NULL || b == NULL) { + return 0; + } + while (*a != '\0' && *b != '\0') { + char ca = (*a >= 'A' && *a <= 'Z') ? (char)(*a + 32) : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? (char)(*b + 32) : *b; + + if (ca != cb) { + return 0; + } + a++; + b++; } return *a == *b; } +static void chess_help(hybbx_session_t *session) +{ + char line[96]; + + hybbx_session_write_line(session, "Chess (Entertain area):"); + hybbx_session_write_line(session, " /chess new Create game (you = White)"); + hybbx_session_write_line(session, " /chess join [id] Join as Black"); + hybbx_session_write_line(session, " /chess watch [id] Spectate"); + hybbx_session_write_line(session, " /chess board Show your board"); + hybbx_session_write_line(session, " /chess move e2 e4 Move (or e2e4)"); + hybbx_session_write_line(session, " /mv e2 e4 Move alias"); + hybbx_session_write_line(session, " /chess move e7 e8 q Promote (q/r/b/n)"); + hybbx_session_write_line(session, " /chess resign Resign"); + hybbx_session_write_line(session, " /chess draw Offer / accept draw"); + hybbx_session_write_line(session, " /chess list List games"); + hybbx_session_write_line(session, " /chess leave Stop spectating"); + hybbx_session_write_line(session, " /chess clear Free finished game slots"); + hybbx_session_write_line(session, " /play … Alias for /chess"); + snprintf(line, sizeof(line), + "Notes: no castling/en passant. Spectator refresh %us.", + CHESS_REFRESH_SEC); + hybbx_session_write_line(session, line); +} + +static void announce_result(chess_game_t *g) +{ + char line[128]; + const char *res; + unsigned j; + + if (g == NULL || g->result == CHESS_RESULT_NONE) { + return; + } + switch (g->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 agreed"; + break; + default: + res = "Game over"; + break; + } + snprintf(line, sizeof(line), "[%s] %s", g->game_id, res); + if (g->white_session != NULL) { + hybbx_session_write_line(g->white_session, line); + } + if (g->black_session != NULL) { + hybbx_session_write_line(g->black_session, line); + } + for (j = 0; j < g->spectator_count; j++) { + hybbx_session_write_line(g->spectators[j], line); + } +} + +static int parse_move_args(const hybbx_parsed_command_t *cmd, unsigned start, + char *from, size_t from_len, char *to, size_t to_len, + char *promo) +{ + const char *a; + const char *b; + + *promo = 0; + if (cmd == NULL || from == NULL || to == NULL || from_len < 3 || to_len < 3) { + return 0; + } + + if (cmd->argc < start + 1) { + return 0; + } + a = cmd->argv[start]; + if (a == NULL) { + return 0; + } + + /* Compact: e2e4 or e7e8q */ + if (strlen(a) >= 4 && cmd->argc == start + 1) { + if (!isalpha((unsigned char)a[0]) || !isdigit((unsigned char)a[1]) || + !isalpha((unsigned char)a[2]) || !isdigit((unsigned char)a[3])) { + return 0; + } + from[0] = (char)tolower((unsigned char)a[0]); + from[1] = a[1]; + from[2] = '\0'; + to[0] = (char)tolower((unsigned char)a[2]); + to[1] = a[3]; + to[2] = '\0'; + if (a[4] != '\0') { + *promo = a[4]; + } + return 1; + } + + if (cmd->argc < start + 2) { + return 0; + } + b = cmd->argv[start + 1]; + if (b == NULL) { + return 0; + } + snprintf(from, from_len, "%s", a); + snprintf(to, to_len, "%s", b); + if (cmd->argc >= start + 3 && cmd->argv[start + 2] != NULL && + cmd->argv[start + 2][0] != '\0') { + *promo = cmd->argv[start + 2][0]; + } + return 1; +} + +static hybbx_result_t do_move(hybbx_session_t *session, + const hybbx_parsed_command_t *cmd, + unsigned arg_start) +{ + chess_game_t *game; + char from[8]; + char to[8]; + char promo = 0; + char err[64]; + char line[96]; + + if (!parse_move_args(cmd, arg_start, from, sizeof(from), to, sizeof(to), + &promo)) { + hybbx_session_write_line(session, + "Usage: /chess move e2 e4 | /mv e2 e4 | /mv e2e4"); + return HYBBX_ERR_INVALID; + } + + game = chess_find_player_game(session); + if (game == NULL || game->result != CHESS_RESULT_NONE) { + hybbx_session_write_line(session, "No active game. /chess new or /chess join"); + return HYBBX_ERR_DENIED; + } + if (chess_player_color(game, session) != game->turn) { + hybbx_session_write_line(session, "Not your turn."); + return HYBBX_ERR_DENIED; + } + if (!chess_make_move(game, from, to, promo, err, sizeof(err))) { + snprintf(line, sizeof(line), "Illegal: %s", err); + hybbx_session_write_line(session, line); + return HYBBX_ERR_INVALID; + } + chess_broadcast_board(game); + if (game->result != CHESS_RESULT_NONE) { + announce_result(game); + } + return HYBBX_OK; +} + 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]; + char line[160]; + int mv_shortcut = 0; + (void)service; if (!hybbx_session_logged_in(session)) { @@ -41,134 +203,168 @@ hybbx_result_t entertain_cmd_chess(struct hybbx_service *service, return HYBBX_ERR_DENIED; } - sub = (cmd->argc >= 1) ? cmd->argv[0] : "help"; + /* /mv … → treat as move with args at argv[0..] */ + if (cmd->verb != NULL && ieq(cmd->verb, "mv")) { + mv_shortcut = 1; + sub = "move"; + } else { + 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); + chess_help(session); return HYBBX_OK; } - /* ── status ────────────────────────────────────────────── */ if (ieq(sub, "status")) { - snprintf(line, sizeof(line), "Chess: %u max games", chess_max_games()); + snprintf(line, sizeof(line), "Chess: max %u concurrent games", + chess_max_games()); hybbx_session_write_line(session, line); return HYBBX_OK; } - /* ── list ──────────────────────────────────────────────── */ if (ieq(sub, "list") || ieq(sub, "ls")) { unsigned found = 0; + unsigned i; + hybbx_session_write_line(session, "Chess games:"); - for (unsigned i = 0; i < chess_max_games(); i++) { + for (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"; + const char *st; + + if (g == NULL) { + continue; + } + if (g->result != CHESS_RESULT_NONE) { + st = "finished"; + } else if (g->black_session == NULL) { + st = "waiting"; + } else { + st = "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."); + 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; + + if (chess_find_player_game(session) != NULL) { + hybbx_session_write_line(session, + "Already in a game. /chess resign or wait for finish + /chess clear"); + 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); + g = chess_create_game(session, hybbx_session_username(session)); + if (g == NULL) { + hybbx_session_write_line(session, + "No free game slots. /chess clear to free finished games."); + 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..."); + hybbx_session_write_line(session, "Waiting for Black: /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; - } + const char *id = (!mv_shortcut && cmd->argc >= 2) ? cmd->argv[1] : NULL; + chess_game_t *g; + + if (chess_find_player_game(session) != NULL) { + hybbx_session_write_line(session, "Already in a game."); + return HYBBX_ERR_DENIED; } - chess_game_t *g = NULL; - if (id) { + if (id != NULL) { 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; } + if (g == NULL || !g->active) { + hybbx_session_write_line(session, "Game not found."); + return HYBBX_ERR_NOT_FOUND; + } + if (g->black_session != NULL) { + hybbx_session_write_line(session, "Game is full."); + return HYBBX_ERR_DENIED; + } + if (g->result != CHESS_RESULT_NONE) { + hybbx_session_write_line(session, "Game already finished."); + 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; } + if (g == NULL) { + hybbx_session_write_line(session, "No open games. /chess new"); + return HYBBX_ERR_NOT_FOUND; + } + } + if (!chess_join_game(g, session, hybbx_session_username(session))) { + hybbx_session_write_line(session, "Cannot join that game."); + return HYBBX_ERR_DENIED; } - chess_join_game(g, session, hybbx_session_username(session)); - snprintf(line, sizeof(line), "Joined %s as Black. Game on!", g->game_id); + snprintf(line, sizeof(line), "Joined %s as Black. White to move.", + g->game_id); hybbx_session_write_line(session, line); - chess_send_board(g, g->white_session); - chess_send_board(g, session); + if (g->white_session != NULL) { + snprintf(line, sizeof(line), "[%s] %s joined as Black.", + g->game_id, g->black_name); + hybbx_session_write_line(g->white_session, line); + } + chess_broadcast_board(g); return HYBBX_OK; } - /* ── watch ─────────────────────────────────────────────── */ if (ieq(sub, "watch") || ieq(sub, "spectate")) { - const char *id = (cmd->argc >= 2) ? cmd->argv[1] : NULL; + const char *id = (!mv_shortcut && cmd->argc >= 2) ? cmd->argv[1] : NULL; chess_game_t *g = NULL; - if (id) { + + if (id != NULL) { g = chess_find_game_by_id(id); } else { - for (unsigned i = 0; i < chess_max_games(); i++) { + unsigned i; + + for (i = 0; i < chess_max_games(); i++) { chess_game_t *t = chess_get_game(i); - if (t && t->black_session) { g = t; break; } + + if (t != NULL && t->black_session != NULL && + t->result == CHESS_RESULT_NONE) { + 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); + if (g == NULL || !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, + "Cannot watch (player or spectator list full)."); + return HYBBX_ERR_DENIED; + } + snprintf(line, sizeof(line), + "Watching %s. Board refreshes every %us for spectators.", + 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++) { + unsigned i; + + for (i = 0; i < chess_max_games(); i++) { chess_game_t *g = chess_get_game(i); - if (g && chess_is_spectator(g, session)) { + + if (g != NULL && chess_is_spectator(g, session)) { chess_remove_spectator(g, session); hybbx_session_write_line(session, "Stopped watching."); return HYBBX_OK; @@ -178,97 +374,99 @@ hybbx_result_t entertain_cmd_chess(struct hybbx_service *service, return HYBBX_ERR_NOT_FOUND; } - /* ── board ─────────────────────────────────────────────── */ if (ieq(sub, "board") || ieq(sub, "show")) { - for (unsigned i = 0; i < chess_max_games(); i++) { + unsigned i; + + for (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))) { + + if (g != NULL && + (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."); + 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; + if (ieq(sub, "move") || ieq(sub, "m")) { + return do_move(session, cmd, mv_shortcut ? 0u : 1u); } - /* ── 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; } + chess_game_t *game = chess_find_player_game(session); + const char *who; + + if (game == NULL) { + hybbx_session_write_line(session, "Not in a game."); + return HYBBX_ERR_NOT_FOUND; + } + if (game->result != CHESS_RESULT_NONE) { + hybbx_session_write_line(session, "Game already finished."); + return HYBBX_ERR_DENIED; } - if (!game) { hybbx_session_write_line(session, "Not in a game."); return HYBBX_ERR_NOT_FOUND; } + who = hybbx_session_username(session); 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); + snprintf(line, sizeof(line), "[%s] %s resigned.", game->game_id, + who != NULL ? who : "?"); + if (game->white_session != NULL) { + hybbx_session_write_line(game->white_session, line); + } + if (game->black_session != NULL) { + 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); + } + } + announce_result(game); 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; } + chess_game_t *game = chess_find_player_game(session); + struct hybbx_session *other; + int agreed; + + if (game == NULL) { + hybbx_session_write_line(session, "Not in a game."); + return HYBBX_ERR_NOT_FOUND; + } + if (game->result != CHESS_RESULT_NONE) { + hybbx_session_write_line(session, "Game already finished."); + return HYBBX_ERR_DENIED; + } + agreed = chess_offer_draw(game, session); + if (agreed) { + announce_result(game); + chess_broadcast_board(game); + return HYBBX_OK; + } + hybbx_session_write_line(session, "Draw offered. Opponent: /chess draw"); + other = (game->white_session == session) ? game->black_session + : game->white_session; + if (other != NULL) { + snprintf(line, sizeof(line), + "[%s] Draw offered by %s — /chess draw to accept", + game->game_id, + hybbx_session_username(session) + ? hybbx_session_username(session) + : "?"); + hybbx_session_write_line(other, line); } - 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."); + unsigned n = chess_clear_finished(); + + snprintf(line, sizeof(line), "Cleared %u finished game(s).", n); + hybbx_session_write_line(session, line); return HYBBX_OK; } diff --git a/plugins/entertain/entertain.c b/plugins/entertain/entertain.c index cf3e123..f8dfa6a 100644 --- a/plugins/entertain/entertain.c +++ b/plugins/entertain/entertain.c @@ -1,7 +1,7 @@ /* * Entertain plugin — entertain.c - * Plugin registration: init, shutdown, tick, on_command. - * Chess is the first entertainment feature; more can be added. + * Feature plugin (not a transport): init / tick / on_command. + * Chess is the first Entertain feature. Main / standalone Main only. */ #include "hybbx/plugin.h" #include "hybbx/service.h" @@ -13,16 +13,26 @@ 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 int verb_is_chess(const char *verb) +{ + if (verb == NULL) { + return 0; + } + return strcmp(verb, "chess") == 0 || + strcmp(verb, "play") == 0 || + strcmp(verb, "mv") == 0; +} + 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); + hybbx_log_info("[entertain] plugin loaded — chess_max_games=%u", + (unsigned)CHESS_DEFAULT_MAX); return HYBBX_OK; } @@ -46,7 +56,7 @@ static hybbx_result_t entertain_on_command(struct hybbx_service *service, return HYBBX_ERR_NOT_FOUND; } - if (strcmp(cmd->verb, "chess") == 0 || strcmp(cmd->verb, "play") == 0) { + if (verb_is_chess(cmd->verb)) { return entertain_cmd_chess(service, session, cmd); } @@ -54,14 +64,14 @@ static hybbx_result_t entertain_on_command(struct hybbx_service *service, } 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, + .name = "entertain", + .kind = 0, /* feature plugin — not a wire transport */ + .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 index 9d7026c..55731b3 100644 --- a/plugins/entertain/entertain_chess.h +++ b/plugins/entertain/entertain_chess.h @@ -1,22 +1,24 @@ /* - * Entertain plugin — chess.h (internal) - * Chess game engine for HyBBX Entertain area. + * Entertain plugin — chess (internal) + * Chess for HyBBX Entertain area (Main only). */ #ifndef ENTERTAIN_CHESS_H #define ENTERTAIN_CHESS_H #include "hybbx/types.h" +#include <stddef.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_REFRESH_SEC 30u +#define CHESS_DEFAULT_MAX 8u #define CHESS_NAME_MAX 32 -#define CHESS_SAN_MAX 8 +#define CHESS_SAN_MAX 16 #define CHESS_EMPTY '.' typedef enum { @@ -52,6 +54,7 @@ typedef struct chess_game { char last_move_san[CHESS_SAN_MAX]; unsigned tick_counter; char game_id[8]; + chess_color_t draw_offered_by; /* NONE = no pending offer */ } chess_game_t; /* Lifecycle */ @@ -64,25 +67,26 @@ 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); +chess_game_t *chess_find_player_game(const struct hybbx_session *s); 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); +void chess_release_game(chess_game_t *g); +unsigned chess_clear_finished(void); /* 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_broadcast_board(chess_game_t *g); void chess_resign(chess_game_t *g, struct hybbx_session *who); -void chess_offer_draw(chess_game_t *g, struct hybbx_session *who); +/** Returns 1 if draw agreed, 0 if offer pending / rejected. */ +int chess_offer_draw(chess_game_t *g, struct hybbx_session *who); -/* Tick — call from plugin tick every 1s */ +/* 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 c64352e..566f281 100644 --- a/share/commands.yaml +++ b/share/commands.yaml @@ -305,14 +305,14 @@ commands: monitor: group: monitor min: Sysop - line1: "/monitor on|off|list|cmds|meet user" - line2: "Help: Live log monitor and sysop admin menu. More: /mon" + line1: "/monitor on|off|list|cmds|meet <user> [force]" + line2: "Help: Live log follow + online list; /monitor cmds for admin verbs. Alias: /mon" chess: group: entertain - min: Guest - line1: "/chess new|join|watch|move|resign|draw|list|board" - line2: "Help: Play chess. Two players + spectators. More: /play /mv" + min: User + line1: "/chess new|join|watch|move|board|resign|draw|list|leave|clear" + line2: "Help: Entertain chess (Main). Moves: /chess move e2 e4 or /mv e2e4. Alias: /play" diff --git a/share/hybbx-main.ini.example b/share/hybbx-main.ini.example index 8573620..473f79c 100644 --- a/share/hybbx-main.ini.example +++ b/share/hybbx-main.ini.example @@ -56,6 +56,8 @@ ssh = yes websocket = yes circuit = yes mains_proxy = no +; Entertain (chess) — Main only; set yes to load plugin +entertain = yes [transport.telnet] bind = 0.0.0.0 diff --git a/share/hybbx-standalone.ini.example b/share/hybbx-standalone.ini.example index 86cff32..3353892 100644 --- a/share/hybbx-standalone.ini.example +++ b/share/hybbx-standalone.ini.example @@ -61,6 +61,8 @@ baycom = yes tmodem = no ssh = yes circuit = yes +; Entertain (chess) — Main only +entertain = yes [max25] check = yes diff --git a/share/hybbxd-main.ini.example b/share/hybbxd-main.ini.example index efcadf6..1988b41 100644 --- a/share/hybbxd-main.ini.example +++ b/share/hybbxd-main.ini.example @@ -58,6 +58,8 @@ ardop = no crdop = no circuit = yes mains_proxy = no +; Entertain (chess) — Main / standalone Main only; AmigaOS build omits plugin +entertain = yes [transport.websocket] bind = 127.0.0.1 diff --git a/src/core/command.c b/src/core/command.c index 3614821..44485c7 100644 --- a/src/core/command.c +++ b/src/core/command.c @@ -2160,11 +2160,19 @@ static void monitor_list_visitor(hybbx_session_t *session, void *userdata) static void monitor_show_cmds(hybbx_session_t *session) { - hybbx_session_write_line(session, "Monitor admin commands:"); + hybbx_session_write_line(session, "Monitor:"); hybbx_session_write_line(session, - " /monitor meet <user> [force] — conference invite (20s) or force"); + " /monitor on|off — start/stop live log follow"); hybbx_session_write_line(session, - "Run any of these as normal /verb (stay invisible while Sysop hidden):"); + " /monitor list — status + visible online users"); + hybbx_session_write_line(session, + " /monitor meet <user> [force] — conference invite (or force)"); + hybbx_session_write_line(session, + " /monitor cmds — this menu + Sysop verbs"); + hybbx_session_write_line(session, + "While monitor is on (or invisible-sysop=yes), Sysop stays hidden from /who."); + hybbx_session_write_line(session, + "Sysop verbs (use as normal /verb):"); hybbx_commands_registry_show_sysop_cmds(session); } @@ -2199,7 +2207,8 @@ static hybbx_result_t cmd_monitor(hybbx_service_t *service, return hybbx_monitor_set_active(session, 0); } - if (str_ieq(sub, "cmd") || str_ieq(sub, "cmds") || str_ieq(sub, "commands")) { + if (str_ieq(sub, "cmd") || str_ieq(sub, "cmds") || str_ieq(sub, "commands") || + str_ieq(sub, "help") || str_ieq(sub, "?")) { monitor_show_cmds(session); return HYBBX_OK; } @@ -2227,7 +2236,7 @@ static hybbx_result_t cmd_monitor(hybbx_service_t *service, if (!str_ieq(sub, "list") && cmd->argc >= 1) { hybbx_session_write_line(session, - "Usage: /monitor on|off|list|cmds|meet <user> [force]"); + "Usage: /monitor on|off|list|cmds|meet <user> [force] — /help monitor"); return HYBBX_ERR_INVALID; } diff --git a/src/core/instance.c b/src/core/instance.c index 4640678..0b9f717 100644 --- a/src/core/instance.c +++ b/src/core/instance.c @@ -108,6 +108,11 @@ int hybbx_instance_plugin_allowed(const char *plugin_name) return role != HYBBX_INSTANCE_MAIN; } + /* Entertain (chess, …) is Main / standalone Main only. */ + if (str_ieq(plugin_name, "entertain")) { + return role == HYBBX_INSTANCE_MAIN; + } + switch (role) { case HYBBX_INSTANCE_MAIN: if (str_ieq(plugin_name, "baycom") || @@ -155,7 +160,7 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks hybbx_instance_binary_name()); hybbx_log_info("[instance] binary=%s role=%s standalone=yes user_bbx=%s " "telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s crdop=%s " - "websocket=%s circuit=%s mains_proxy=%s", + "websocket=%s circuit=%s mains_proxy=%s entertain=%s", hybbx_instance_binary_name(), hybbx_instance_role_name(), hybbx_instance_offers_user_bbx() ? "yes" : "no", @@ -167,7 +172,8 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks hybbx_bool_to_string(networks->crdop), hybbx_bool_to_string(networks->websocket), hybbx_bool_to_string(networks->circuit), - hybbx_bool_to_string(networks->mains_proxy)); + hybbx_bool_to_string(networks->mains_proxy), + hybbx_bool_to_string(networks->entertain)); return HYBBX_OK; } @@ -212,6 +218,10 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks break; case HYBBX_INSTANCE_SECONDARY: + if (networks->entertain) { + hybbx_log_warn("[instance] hybbxsd: entertain Main-only — forcing off"); + networks->entertain = 0; + } if (networks->telnet) { hybbx_log_warn("[instance] hybbxsd: no user telnet — forcing off"); networks->telnet = 0; @@ -233,6 +243,10 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks break; case HYBBX_INSTANCE_PROXY: + if (networks->entertain) { + hybbx_log_warn("[instance] hybbxpd: entertain Main-only — forcing off"); + networks->entertain = 0; + } if (networks->websocket) { hybbx_log_warn("[instance] hybbxpd: WebSocket forbidden — forcing off"); networks->websocket = 0; @@ -254,7 +268,7 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks hybbx_log_info("[instance] binary=%s role=%s user_bbx=%s " "telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s crdop=%s " - "websocket=%s circuit=%s mains_proxy=%s", + "websocket=%s circuit=%s mains_proxy=%s entertain=%s", hybbx_instance_binary_name(), hybbx_instance_role_name(), hybbx_instance_offers_user_bbx() ? "yes" : "no", @@ -266,7 +280,8 @@ hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks hybbx_bool_to_string(networks->crdop), hybbx_bool_to_string(networks->websocket), hybbx_bool_to_string(networks->circuit), - hybbx_bool_to_string(networks->mains_proxy)); + hybbx_bool_to_string(networks->mains_proxy), + hybbx_bool_to_string(networks->entertain)); return HYBBX_OK; } diff --git a/src/core/networks.c b/src/core/networks.c index c52554d..f61479e 100644 --- a/src/core/networks.c +++ b/src/core/networks.c @@ -54,6 +54,7 @@ void hybbx_networks_config_defaults(hybbx_networks_config_t *networks) networks->websocket = 0; networks->circuit = 1; networks->mains_proxy = 0; + networks->entertain = 0; } void hybbx_networks_config_apply(hybbx_networks_config_t *networks, @@ -106,8 +107,13 @@ void hybbx_networks_config_apply(hybbx_networks_config_t *networks, "mains_proxy", 0); } + if (networks_has_key(config, "entertain")) { + networks->entertain = hybbx_config_get_bool(config, "networks", + "entertain", 0); + } + hybbx_log_info("[networks] telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s " - "crdop=%s websocket=%s circuit=%s mains_proxy=%s", + "crdop=%s websocket=%s circuit=%s mains_proxy=%s entertain=%s", hybbx_bool_to_string(networks->telnet), hybbx_bool_to_string(networks->ssh), hybbx_bool_to_string(networks->ax25), @@ -116,7 +122,8 @@ void hybbx_networks_config_apply(hybbx_networks_config_t *networks, hybbx_bool_to_string(networks->crdop), hybbx_bool_to_string(networks->websocket), hybbx_bool_to_string(networks->circuit), - hybbx_bool_to_string(networks->mains_proxy)); + hybbx_bool_to_string(networks->mains_proxy), + hybbx_bool_to_string(networks->entertain)); } int hybbx_networks_is_static_transport(const char *plugin_name) @@ -187,5 +194,9 @@ int hybbx_networks_transport_wanted(const char *plugin_name, return networks->mains_proxy; } + if (str_ieq(plugin_name, "entertain")) { + return networks->entertain; + } + return 0; } diff --git a/src/core/service.c b/src/core/service.c index 20934a7..dda6311 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -918,11 +918,8 @@ hybbx_result_t hybbx_service_start_transport(hybbx_service_t *service, return HYBBX_ERR_BUSY; } - if (active->plugin->start == NULL) { - return HYBBX_ERR_UNSUPPORTED; - } - - { + /* Feature plugins (e.g. entertain) may have init/tick/on_command only. */ + if (active->plugin->start != NULL) { hybbx_result_t rc = active->plugin->start(config); if (rc != HYBBX_OK) { return rc; @@ -1040,8 +1037,9 @@ hybbx_result_t hybbx_service_try_plugin_command(hybbx_service_t *service, 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; + /* NOT_FOUND = not this plugin; any other result = handled. */ + if (rc != HYBBX_ERR_NOT_FOUND) { + return rc; } } } |
