aboutsummaryrefslogtreecommitdiffstats
path: root/c/irc.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-18 00:38:38 +0200
committeruser <user@clank>2026-07-18 00:38:38 +0200
commitba2d7ae1d7b319645b10aaa31a7f664f9f80c25c (patch)
treee01e6c7c710a661defe2515ff9657f08cddcb283 /c/irc.c
parenttools: gbtype prints hub errors to stderr and exits 1 (diff)
downloadgbos-ba2d7ae1d7b319645b10aaa31a7f664f9f80c25c.tar.gz
gbos-ba2d7ae1d7b319645b10aaa31a7f664f9f80c25c.tar.xz
gbos-ba2d7ae1d7b319645b10aaa31a7f664f9f80c25c.zip
refactor: c/ -> usr/, compiled program blobs out of the source tree
Userland sources live in usr/ (fits the Unix theme better than 'c'). SDCC output .bin blobs land in build/usr/ with the other build artifacts instead of littering the source dir; programs.asm INCBINs them from there. Byte-identical ROM.
Diffstat (limited to 'c/irc.c')
-rw-r--r--c/irc.c422
1 files changed, 0 insertions, 422 deletions
diff --git a/c/irc.c b/c/irc.c
deleted file mode 100644
index 250738a..0000000
--- a/c/irc.c
+++ /dev/null
@@ -1,422 +0,0 @@
-#include "resolve.h"
-/* irc HOST [NICK] - a bitchx/irssi-flavored IRC client on 40x18 columns.
- The kernel owns TCP; this owns the protocol and the screen.
-
- UI: messages scroll above; the bottom line is an irssi-style input line
- "[#chan] text_" redrawn in place with \r (the terminal has no cursor
- addressing - \r + overprint is the entire trick). Long input scrolls
- horizontally like irssi. Keys come from the OSK (SELECT toggles it) and
- from the console ring (pollcon), so a hub can type into the client too.
-
- Formats, straight from the elders:
- <nick> text channel message * nick text CTCP ACTION
- <nick:#c> text msg to a non-current channel
- *nick* text private msg >target< text outbound /msg
- -nick- text notice -!- text server/status */
-
-/* seg first + large: the arg string lives at 0xA000 and the first static
- lands on it; args are copied out in main() before seg is ever written. */
-static unsigned char seg[220]; /* one TCP segment from the kernel */
-static char line[240]; /* server line accumulator */
-static unsigned char llen;
-static char input[140]; /* the input line being typed */
-static unsigned char inlen;
-static unsigned char drawn; /* chars currently drawn on input row */
-static char nick[16];
-static char target[32]; /* current channel/query, "" = none */
-static char sbuf[200]; /* outgoing IRC line builder */
-static unsigned char slen;
-static unsigned char namebuf[64];
-static unsigned char dst[4];
-static unsigned char sock;
-static unsigned char quitting;
-/* A WRITABLE empty string for parse defaults. handle() writes terminators
- into pfx (bang) and txt (strip_fmt); a read-only "" literal lives in ROM
- ($4000-$5FFF), where a write is an MBC5 *RAM-bank-select* - it would swap
- the whole $A000 data bank out from under every static (instant garbage).
- A prefix-less or empty-trailing server line is enough to trigger it. */
-static char empty[1];
-
-/* ---- tiny string helpers (SDCC libc is us) ------------------------------- */
-static unsigned char sceq(const char *a, const char *b) { /* case-insens. */
- unsigned char ca, cb;
- for (;;) {
- ca = (unsigned char)*a++; cb = (unsigned char)*b++;
- if (ca >= 'a' && ca <= 'z') ca -= 32;
- if (cb >= 'a' && cb <= 'z') cb -= 32;
- if (ca != cb) return 0;
- if (!ca) return 1;
- }
-}
-static void scpy(char *d, const char *s, unsigned char max) {
- unsigned char i;
- for (i = 0; s[i] && i < (unsigned char)(max - 1); i++) d[i] = s[i];
- d[i] = 0;
-}
-
-/* ---- outgoing IRC lines --------------------------------------------------- */
-static void sc(const char *s) { /* append to sbuf */
- while (*s && slen < 197) sbuf[slen++] = *s++;
-}
-static void sfin(void) { /* CRLF + send */
- sbuf[slen++] = '\r'; sbuf[slen++] = '\n';
- net_send(sock, sbuf, slen);
- slen = 0;
-}
-
-/* ---- the input line ------------------------------------------------------- */
-/* erase what's drawn on the bottom row, park at col 0 */
-static void row_clear(void) {
- unsigned char i;
- putc('\r');
- for (i = 0; i < drawn; i++) putc(' ');
- putc('\r');
- drawn = 0;
-}
-/* redraw "[target] tail-of-input_"; keep total < 40 so it never wraps */
-static void row_draw(void) {
- unsigned char pl, vis, i;
- const char *t = target[0] ? target : "(status)";
- row_clear();
- putc('[');
- for (pl = 0; t[pl] && pl < 12; pl++) putc(t[pl]);
- putc(']'); putc(' ');
- pl += 3; /* prompt width */
- vis = 39 - pl - 1; /* room for text (cursor gets 1) */
- i = (inlen > vis) ? (unsigned char)(inlen - vis) : 0;
- for (; i < inlen; i++) putc(input[i]);
- drawn = pl + ((inlen > vis) ? vis : inlen);
-}
-/* print a message line above the input line */
-static void say(const char *s) {
- row_clear();
- puts(s); nl();
- row_draw();
-}
-static void say2(const char *a, const char *b) {
- row_clear();
- puts(a); puts(b); nl();
- row_draw();
-}
-static void say3(const char *a, const char *b, const char *c) {
- row_clear();
- puts(a); puts(b); puts(c); nl();
- row_draw();
-}
-
-/* ---- server line parsing -------------------------------------------------- */
-/* split "pfx!user@host" -> pfx nick only (in place) */
-static void bang(char *p) {
- while (*p && *p != '!') p++;
- *p = 0;
-}
-static unsigned char isnum(const char *c) {
- return c[0] >= '0' && c[0] <= '9';
-}
-static unsigned char ishex(char c) {
- return (c>='0'&&c<='9')||(c>='a'&&c<='f')||(c>='A'&&c<='F');
-}
-
-/* Strip mIRC/IRC formatting in place - crucially their *arguments*, or a
- color logo (a real MOTD!) leaves its \x03 color numbers on screen as digit
- soup. \x03 = color (up to 2 digits[,up to 2 digits]); \x04 = hex color
- (up to 6 hex[,6 hex]); \x02 bold \x0f reset \x11 mono \x16 reverse
- \x1d italic \x1e strike \x1f underline are lone toggles. Leaves \x01
- (CTCP) intact for the PRIVMSG path. */
-static void strip_fmt(char *s) {
- char *w = s, *r = s;
- unsigned char c, d;
- while (*r) {
- c = (unsigned char)*r;
- if (c == 3) {
- r++; d = 0;
- while (*r>='0'&&*r<='9'&&d<2) { r++; d++; }
- if (*r==','&&r[1]>='0'&&r[1]<='9') { r++; d=0; while(*r>='0'&&*r<='9'&&d<2){r++;d++;} }
- continue;
- }
- if (c == 4) {
- r++; d = 0;
- while (ishex(*r)&&d<6) { r++; d++; }
- if (*r==','&&ishex(r[1])) { r++; d=0; while(ishex(*r)&&d<6){r++;d++;} }
- continue;
- }
- if (c==2||c==15||c==17||c==22||c==29||c==30||c==31) { r++; continue; }
- *w++ = *r++;
- }
- *w = 0;
-}
-
-static void handle(char *l) {
- char *pfx = empty, *cmd, *arg, *txt = empty;
- char *p = l;
-
- if (*p == ':') { /* :prefix */
- pfx = ++p;
- while (*p && *p != ' ') p++;
- if (*p) *p++ = 0;
- }
- cmd = p; /* command */
- while (*p && *p != ' ') p++;
- if (*p) *p++ = 0;
- arg = p; /* params, then :trailing */
- while (*p) {
- if (*p == ':' && (p == arg || p[-1] == 0)) { txt = p + 1; p[-1] = 0; break; }
- if (*p == ' ') *p = 0;
- p++;
- }
- /* arg = first param (NUL-joined list), txt = trailing */
- strip_fmt(txt); /* kill color/format codes + args */
-
- if (sceq(cmd, "PING")) {
- slen = 0; sc("PONG :"); sc(txt[0] ? txt : arg); sfin();
- say("-!- Ping? Pong!");
- return;
- }
- bang(pfx);
- if (sceq(cmd, "PRIVMSG")) {
- char *dest = arg;
- unsigned char act = 0;
- if (txt[0] == 1) { /* CTCP */
- char *e = txt + 1;
- while (*e && *e != 1) e++;
- *e = 0; txt++;
- if (txt[0]=='A'&&txt[1]=='C'&&txt[2]=='T'&&txt[3]=='I'&&txt[4]=='O'&&txt[5]=='N'&&txt[6]==' ') {
- act = 1; txt += 7;
- } else if (txt[0]=='V') { /* VERSION */
- slen = 0; sc("NOTICE "); sc(pfx); sc(" :\001VERSION gbos:sl0pboy:GameBoyColor\001"); sfin();
- return;
- } else return;
- }
- row_clear();
- if (act) { puts("* "); puts(pfx); putc(' '); }
- else if (sceq(dest, nick)) { putc('*'); puts(pfx); puts("* "); }
- else if (sceq(dest, target)) { putc('<'); puts(pfx); puts("> "); }
- else { putc('<'); puts(pfx); putc(':'); puts(dest); puts("> "); }
- puts(txt); nl();
- row_draw();
- return;
- }
- if (sceq(cmd, "NOTICE")) {
- row_clear();
- putc('-'); puts(pfx[0] ? pfx : "server"); puts("- "); puts(txt); nl();
- row_draw();
- return;
- }
- if (sceq(cmd, "JOIN")) {
- char *ch = txt[0] ? txt : arg;
- if (sceq(pfx, nick)) {
- scpy(target, ch, sizeof(target));
- say2("-!- now talking in ", target);
- } else say3("-!- ", pfx, " has joined");
- return;
- }
- if (sceq(cmd, "PART")) {
- if (sceq(pfx, nick)) { target[0] = 0; say("-!- you left"); }
- else say3("-!- ", pfx, " has left");
- return;
- }
- if (sceq(cmd, "QUIT")) { say3("-!- ", pfx, " has quit"); return; }
- if (sceq(cmd, "KICK")) {
- char *ch = arg, *who = arg;
- while (*who) who++;
- who++; /* param 2: the victim */
- if (sceq(who, nick)) { target[0] = 0; say2("-!- you were kicked from ", ch); }
- else say3("-!- ", who, " was kicked");
- return;
- }
- if (sceq(cmd, "NICK")) {
- char *nn = txt[0] ? txt : arg;
- if (sceq(pfx, nick)) scpy(nick, nn, sizeof(nick));
- row_clear();
- puts("-!- "); puts(pfx); puts(" is now known as "); puts(nn); nl();
- row_draw();
- return;
- }
- if (isnum(cmd)) {
- if (cmd[0]=='4'&&cmd[1]=='3'&&cmd[2]=='3') { /* nick in use */
- unsigned char n = strlen(nick);
- if (n < 14) { nick[n] = '_'; nick[n+1] = 0; }
- slen = 0; sc("NICK "); sc(nick); sfin();
- say2("-!- nick in use, trying ", nick);
- return;
- }
- if (cmd[0]=='3'&&cmd[1]=='3'&&cmd[2]=='2') { /* topic */
- say2("-!- topic: ", txt);
- return;
- }
- if (cmd[0]=='3'&&cmd[1]=='5'&&cmd[2]=='3') { /* names */
- say2("-!- users: ", txt);
- return;
- }
- if (cmd[1]=='3'&&cmd[0]=='3') return; /* 333 topic-by: skip */
- if (cmd[0]=='0'&&cmd[1]=='0'&&(cmd[2]=='4'||cmd[2]=='5')) return; /* MYINFO/ISUPPORT noise */
- if (txt[0]) say2("-!- ", txt); /* motd & friends */
- return;
- }
- /* anything else: show it raw-ish, bitchx would too */
- if (txt[0]) say3(cmd, " ", txt);
-}
-
-/* accumulate TCP bytes into lines */
-static void feed(unsigned char *b, unsigned char n) {
- unsigned char i;
- for (i = 0; i < n; i++) {
- char c = (char)b[i];
- if (c == '\r') continue;
- if (c == '\n') { line[llen] = 0; if (llen) handle(line); llen = 0; }
- else if (llen < 239) line[llen++] = c;
- }
-}
-
-/* ---- typed input ---------------------------------------------------------- */
-static void run_cmd(char *t) {
- char *a = t;
- while (*a && *a != ' ') a++;
- if (*a) *a++ = 0; /* t = /cmd, a = rest */
-
- if (sceq(t, "/join") || sceq(t, "/j")) {
- if (!*a) { say("-!- join what?"); return; }
- slen = 0; sc("JOIN "); sc(a); sfin();
- return;
- }
- if (sceq(t, "/part") || sceq(t, "/p")) {
- slen = 0; sc("PART "); sc(*a ? a : target); sfin();
- return;
- }
- if (sceq(t, "/msg")) {
- char *m = a;
- while (*m && *m != ' ') m++;
- if (*m) *m++ = 0;
- if (!*a || !*m) { say("-!- /msg nick text"); return; }
- slen = 0; sc("PRIVMSG "); sc(a); sc(" :"); sc(m); sfin();
- row_clear();
- putc('>'); puts(a); puts("< "); puts(m); nl();
- row_draw();
- return;
- }
- if (sceq(t, "/me")) {
- if (!target[0]) { say("-!- no channel"); return; }
- slen = 0; sc("PRIVMSG "); sc(target); sc(" :\001ACTION "); sc(a); sc("\001"); sfin();
- row_clear();
- puts("* "); puts(nick); putc(' '); puts(a); nl();
- row_draw();
- return;
- }
- if (sceq(t, "/nick")) {
- if (!*a) { say("-!- /nick newnick"); return; }
- slen = 0; sc("NICK "); sc(a); sfin();
- return;
- }
- if (sceq(t, "/quit")) {
- slen = 0; sc("QUIT :"); sc(*a ? a : "sl0pboy out"); sfin();
- quitting = 1;
- return;
- }
- if (sceq(t, "/raw") || sceq(t, "/quote")) {
- slen = 0; sc(a); sfin();
- return;
- }
- say("-!- commands: /join /part /msg /me /nick /quit /raw");
-}
-
-static void submit(void) {
- input[inlen] = 0;
- if (!inlen) return;
- if (input[0] == '/') {
- inlen = 0;
- run_cmd(input);
- row_draw();
- return;
- }
- if (!target[0]) { inlen = 0; say("-!- join a channel first (/join #gb)"); return; }
- slen = 0; sc("PRIVMSG "); sc(target); sc(" :"); sc(input); sfin();
- row_clear();
- putc('<'); puts(nick); puts("> "); puts(input); nl();
- inlen = 0;
- row_draw();
-}
-
-static void key(unsigned char c) {
- if (c == '\n' || c == '\r') { submit(); return; }
- if (c == 8 || c == 127) {
- if (inlen) { inlen--; row_draw(); }
- return;
- }
- if (c < 32) return;
- if (inlen < 139) { input[inlen++] = (char)c; row_draw(); }
-}
-
-/* ---- main ----------------------------------------------------------------- */
-void main(void) {
- char *argp;
- unsigned char i, n, idle;
-
- /* gbos does not zero C statics (crt0 zeroes nothing; fork inherits the
- parent's RAM bank) - initialize every piece of state explicitly */
- llen = inlen = drawn = slen = quitting = 0;
- target[0] = 0;
- empty[0] = 0; /* gbos doesn't zero BSS */
-
- argp = getargs(); /* copy args before seg is used */
- for (i = 0; argp[i] && i < 63; i++) namebuf[i] = (unsigned char)argp[i];
- namebuf[i] = 0;
-
- /* split "HOST [NICK]" */
- scpy(nick, "sl0pboy", sizeof(nick));
- for (i = 0; namebuf[i]; i++)
- if (namebuf[i] == ' ') {
- namebuf[i] = 0;
- if (namebuf[i+1]) scpy(nick, (char *)namebuf + i + 1, sizeof(nick));
- break;
- }
- if (!namebuf[0]) {
- puts("usage: irc HOST [NICK] (port 6667)"); nl();
- puts(" then: /join #chan, SELECT = keyboard"); nl();
- sexit(1);
- }
-
- if (!resolve((char *)namebuf, dst)) {
- puts("irc: cannot resolve "); puts((char *)namebuf); nl(); sexit(1);
- }
-
- sock = net_socket(SOCK_TCP);
- if (sock == 0xFF) { puts("irc: no socket"); nl(); sexit(1); }
- /* vary the local port per run (DIV timer noise): a fixed port can hit a
- stale half-dead connection at the server after an unclean exit */
- net_bind(sock, 41000 + ((unsigned int)*(volatile unsigned char *)0xFF04 << 2) + getpid());
- puts("-!- connecting to "); puts((char *)namebuf); puts(":6667"); nl();
- if (net_connect(sock, dst, 6667) == 0xFF) {
- puts("irc: connect failed"); nl(); net_close(sock); sexit(1);
- }
-
- slen = 0; sc("NICK "); sc(nick); sfin();
- slen = 0; sc("USER gbos 0 * :sl0pboy on gbos"); sfin();
- puts("-!- registering as "); puts(nick);
- puts(" (SELECT = keyboard)"); nl();
- row_draw();
-
- idle = 0;
- for (;;) {
- n = net_recv_nb(sock, seg, (unsigned char)sizeof(seg));
- if (n == 0) { say("-!- disconnected"); break; }
- if (n != 0xFE && n != 0xFF) feed(seg, n); /* render a segment if any */
- if (quitting) { say("-!- quit"); break; }
-
- /* Poll input EVERY pass, even mid-flood: otherwise a big MOTD (or a
- * busy channel) starves the keyboard for its whole duration - the OSK
- * won't even open, and keystrokes are dropped. That's what made
- * '/join #sl0p right after connect' look broken. */
- i = pollin(); /* OSK */
- if (!i) i = pollcon(); /* hub-injected console bytes */
- if (i) key(i);
-
- if (n == 0xFE || n == 0xFF) { /* nothing received this pass */
- if (!i && ++idle >= 4) { msleep(20); idle = 4; }
- } else {
- idle = 0; /* actively receiving; don't sleep */
- }
- }
- net_close(sock);
- row_clear();
- sexit(0);
-}