RBBS port to the C128

Started by Blacklord, Yesterday at 08:09 AM

Previous topic - Next topic

Blacklord and 1 Guest are viewing this topic.

Blacklord

One of the things I've always wanted to do is write a BBS for theC128 (several years ago we had a thread about this). So - I've started porting a CP/M version of RBBs (3.7 from 1981) over.

It's going to be heavily customised :

Although it will run on a real 128, the software is written to run under VICE (on Windows)

It has a custom tcp server (TCPSRV.EXE), utilises Swiftlink emulation, CMD hard disk emulation, JiffyDOS ROMS.

As I complete a task I'll update here.


Blacklord

Interim code for tcpsrv.c (compiles under Microsoft Visual Studio 2026):

/*
 * tcpsrv.c
 *
 * RBBS-128 TCP/VICE communications bridge.
 *
 * Port 23232:
 *     VICE X128 / SwiftLink connection
 *
 * Port 6400:
 *     Incoming BBS caller connection
 *
 * Data flow:
 *
 *     Caller <----> TCPSRV <----> X128
 *
 * Internal control messages sent only to X128:
 *
 *     #CONNECT
 *     #TIME:DD/MM/YYYY HH:MM:SS
 *     #DISCONNECT
 *
 * Internal control messages are deliberately sent more
 * slowly than normal caller traffic because BASIC is
 * polling the emulated 6551 ACIA one character at a time.
 *
 * Usage:
 *
 *     tcpsrv.exe
 */

#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>

#pragma comment(lib, "Ws2_32.lib")

#define X128_PORT       23232
#define CALLER_PORT      6400
#define BUFFER_SIZE      4096

#define INVALID_CLIENT INVALID_SOCKET


/*
 * Send an entire buffer without pacing.
 *
 * Used for normal TCP destinations such as the caller.
 */
static int send_all(SOCKET socket,
                    const char *buffer,
                    int length)
{
    int total;
    int sent;

    total = 0;

    while (total < length)
    {
        sent = send(socket,
                    buffer + total,
                    length - total,
                    0);

        if (sent == SOCKET_ERROR)
        {
            return SOCKET_ERROR;
        }

        if (sent == 0)
        {
            return 0;
        }

        total += sent;
    }

    return total;
}


/*
 * Send a zero-terminated string without pacing.
 */
static int send_text(SOCKET socket,
                     const char *text)
{
    return send_all(socket,
                    text,
                    (int)strlen(text));
}


/*
 * Send ordinary caller/console data to the C128.
 *
 * 10 ms pacing is used so a pasted line is not dumped
 * into the emulated ACIA in one instantaneous TCP burst.
 */
static int send_to_x128(SOCKET socket,
                        const char *buffer,
                        int length)
{
    int i;
    int result;

    for (i = 0; i < length; i++)
    {
        result = send(socket,
                      buffer + i,
                      1,
                      0);

        if (result == SOCKET_ERROR)
        {
            return SOCKET_ERROR;
        }

        if (result == 0)
        {
            return 0;
        }

        Sleep(50);
    }

    return length;
}


/*
 * Send internal TCPSRV control messages to the C128.
 *
 * These are deliberately much slower than caller data
 * while we prove reliable reception in BASIC.
 */
static int send_control_to_x128(SOCKET socket,
                                const char *buffer,
                                int length)
{
    int i;
    int result;

    for (i = 0; i < length; i++)
    {
        result = send(socket,
                      buffer + i,
                      1,
                      0);

        if (result == SOCKET_ERROR)
        {
            return SOCKET_ERROR;
        }

        if (result == 0)
        {
            return 0;
        }

        /*
         * Conservative test value.
         * Reduce later once the real RBBS line reader
         * has been proven reliable.
         */
        Sleep(100);
    }

    return length;
}


/*
 * Generate host local date/time.
 *
 * Australian display order:
 *
 *     DD/MM/YYYY HH:MM:SS
 */
static void make_time_message(char *buffer,
                              size_t size)
{
    SYSTEMTIME st;

    GetLocalTime(&st);

    snprintf(buffer,
             size,
             "#TIME:%02u/%02u/%04u %02u:%02u:%02u\r\n",
             (unsigned int)st.wDay,
             (unsigned int)st.wMonth,
             (unsigned int)st.wYear,
             (unsigned int)st.wHour,
             (unsigned int)st.wMinute,
             (unsigned int)st.wSecond);
}


/*
 * Create a listening TCP socket.
 *
 * Both listeners currently bind to localhost only.
 */
static SOCKET create_listener(int port)
{
    SOCKET s;
    struct sockaddr_in address;
    int reuse;
    int result;

    s = socket(AF_INET,
               SOCK_STREAM,
               IPPROTO_TCP);

    if (s == INVALID_SOCKET)
    {
        return INVALID_SOCKET;
    }

    reuse = 1;

    setsockopt(s,
               SOL_SOCKET,
               SO_REUSEADDR,
               (const char *)&reuse,
               sizeof(reuse));

    ZeroMemory(&address,
               sizeof(address));

    address.sin_family = AF_INET;

    address.sin_addr.s_addr =
        htonl(INADDR_LOOPBACK);

    address.sin_port =
        htons((unsigned short)port);

    result = bind(s,
                  (struct sockaddr *)&address,
                  sizeof(address));

    if (result == SOCKET_ERROR)
    {
        closesocket(s);
        return INVALID_SOCKET;
    }

    result = listen(s,
                    4);

    if (result == SOCKET_ERROR)
    {
        closesocket(s);
        return INVALID_SOCKET;
    }

    return s;
}


/*
 * Tell the C128 that a caller has connected.
 *
 * Also send the current Windows host date/time.
 */
static int notify_connect(SOCKET x128Socket)
{
    SYSTEMTIME st;
    char message[128];

    GetLocalTime(&st);

    snprintf(message,
             sizeof(message),
             "#CONNECT:%02u/%02u/%04u %02u:%02u:%02u\r\n",
             (unsigned int)st.wDay,
             (unsigned int)st.wMonth,
             (unsigned int)st.wYear,
             (unsigned int)st.wHour,
             (unsigned int)st.wMinute,
             (unsigned int)st.wSecond);

    if (send_control_to_x128(x128Socket,
                             message,
                             (int)strlen(message))
        == SOCKET_ERROR)
    {
        return SOCKET_ERROR;
    }

    return 0;
}

/*
 * Tell the C128 that the caller disconnected.
 */
static void notify_disconnect(SOCKET x128Socket)
{
    const char *message =
        "#DISCONNECT\r\n";

    send_control_to_x128(x128Socket,
                         message,
                         (int)strlen(message));
}


/*
 * Reject an additional caller while one is online.
 */
static void reject_busy_caller(SOCKET socket)
{
    const char *message =
        "\r\n"
        "RBBS-128 is currently in use.\r\n"
        "Please try again later.\r\n";

    send_text(socket,
              message);

    shutdown(socket,
             SD_BOTH);

    closesocket(socket);
}


int main(void)
{
    WSADATA wsaData;

    SOCKET x128Listen;
    SOCKET callerListen;

    SOCKET x128Socket;
    SOCKET callerSocket;
    SOCKET newSocket;

    fd_set readSet;

    struct timeval timeout;

    unsigned char buffer[BUFFER_SIZE];

    int received;
    int result;

    unsigned char ch;


    /*
     * Initialize Winsock.
     */
    result = WSAStartup(MAKEWORD(2, 2),
                        &wsaData);

    if (result != 0)
    {
        fprintf(stderr,
                "tcpsrv: WSAStartup failed: %d\n",
                result);

        return 1;
    }


    /*
     * Create X128 listener.
     */
    x128Listen =
        create_listener(X128_PORT);

    if (x128Listen == INVALID_SOCKET)
    {
        fprintf(stderr,
                "tcpsrv: unable to listen on port %d: %d\n",
                X128_PORT,
                WSAGetLastError());

        WSACleanup();

        return 1;
    }


    /*
     * Create caller listener.
     */
    callerListen =
        create_listener(CALLER_PORT);

    if (callerListen == INVALID_SOCKET)
    {
        fprintf(stderr,
                "tcpsrv: unable to listen on port %d: %d\n",
                CALLER_PORT,
                WSAGetLastError());

        closesocket(x128Listen);
        WSACleanup();

        return 1;
    }


    fprintf(stderr,
            "tcpsrv: X128 listening on TCP port %d\n",
            X128_PORT);

    fprintf(stderr,
            "tcpsrv: callers listening on TCP port %d\n",
            CALLER_PORT);


    /*
     * Persistent server loop.
     *
     * If X128 disconnects, TCPSRV stays alive and waits
     * for X128 to reconnect.
     */
    for (;;)
    {
        /*
         * Wait for VICE/X128.
         */
        x128Socket =
            accept(x128Listen,
                   NULL,
                   NULL);

        if (x128Socket == INVALID_SOCKET)
        {
            fprintf(stderr,
                    "tcpsrv: X128 accept failed: %d\n",
                    WSAGetLastError());

            break;
        }

        fprintf(stderr,
                "tcpsrv: X128 connected\n");

        callerSocket = INVALID_CLIENT;


        /*
         * Service X128 and caller connections.
         */
        for (;;)
        {
            FD_ZERO(&readSet);

            FD_SET(x128Socket,
                   &readSet);

            FD_SET(callerListen,
                   &readSet);

            if (callerSocket != INVALID_CLIENT)
            {
                FD_SET(callerSocket,
                       &readSet);
            }


            /*
             * Short timeout allows Windows console
             * keyboard polling.
             */
            timeout.tv_sec = 0;
            timeout.tv_usec = 50000;


            result =
                select(0,
                       &readSet,
                       NULL,
                       NULL,
                       &timeout);

            if (result == SOCKET_ERROR)
            {
                fprintf(stderr,
                        "tcpsrv: select failed: %d\n",
                        WSAGetLastError());

                break;
            }


            /*
             * Incoming BBS caller.
             */
            if (FD_ISSET(callerListen,
                         &readSet))
            {
                newSocket =
                    accept(callerListen,
                           NULL,
                           NULL);

                if (newSocket != INVALID_SOCKET)
                {
                    if (callerSocket != INVALID_CLIENT)
                    {
                        reject_busy_caller(newSocket);
                    }
                    else
                    {
                        callerSocket = newSocket;

                        fprintf(stderr,
                                "tcpsrv: caller connected\n");


                        /*
                         * Signal carrier and provide
                         * date/time to RBBS-128.
                         */
                        if (notify_connect(x128Socket)
                            == SOCKET_ERROR)
                        {
                            fprintf(stderr,
                                    "tcpsrv: lost X128 while notifying connection\n");

                            break;
                        }
                    }
                }
            }


            /*
             * X128 -> local console and caller.
             */
            if (FD_ISSET(x128Socket,
                         &readSet))
            {
                received =
                    recv(x128Socket,
                         (char *)buffer,
                         sizeof(buffer),
                         0);

                if (received == 0)
                {
                    fprintf(stderr,
                            "tcpsrv: X128 disconnected\n");

                    break;
                }

                if (received == SOCKET_ERROR)
                {
                    fprintf(stderr,
                            "tcpsrv: X128 recv failed: %d\n",
                            WSAGetLastError());

                    break;
                }


                /*
                 * Always show C128 output locally.
                 */
                fwrite(buffer,
                       1,
                       received,
                       stdout);

                fflush(stdout);


                /*
                 * Forward C128 output to the caller.
                 */
                if (callerSocket != INVALID_CLIENT)
                {
                    if (send_all(callerSocket,
                                 (const char *)buffer,
                                 received)
                        == SOCKET_ERROR)
                    {
                        fprintf(stderr,
                                "tcpsrv: caller send failed\n");

                        shutdown(callerSocket,
                                 SD_BOTH);

                        closesocket(callerSocket);

                        callerSocket =
                            INVALID_CLIENT;

                        notify_disconnect(x128Socket);

                        fprintf(stderr,
                                "tcpsrv: caller disconnected\n");
                    }
                }
            }


            /*
             * Caller -> X128.
             */
            if (callerSocket != INVALID_CLIENT &&
                FD_ISSET(callerSocket,
                         &readSet))
            {
                received =
                    recv(callerSocket,
                         (char *)buffer,
                         sizeof(buffer),
                         0);

                if (received == 0)
                {
                    /*
                     * Normal caller disconnect.
                     */
                    shutdown(callerSocket,
                             SD_BOTH);

                    closesocket(callerSocket);

                    callerSocket =
                        INVALID_CLIENT;

                    notify_disconnect(x128Socket);

                    fprintf(stderr,
                            "tcpsrv: caller disconnected\n");
                }
                else if (received == SOCKET_ERROR)
                {
                    /*
                     * Abnormal caller disconnect.
                     */
                    shutdown(callerSocket,
                             SD_BOTH);

                    closesocket(callerSocket);

                    callerSocket =
                        INVALID_CLIENT;

                    notify_disconnect(x128Socket);

                    fprintf(stderr,
                            "tcpsrv: caller connection lost\n");
                }
                else
                {
                    /*
                     * Ordinary caller data uses the
                     * faster 10 ms pacing.
                     */
                    if (send_to_x128(x128Socket,
                                     (const char *)buffer,
                                     received)
                        == SOCKET_ERROR)
                    {
                        fprintf(stderr,
                                "tcpsrv: X128 send failed\n");

                        break;
                    }
                }
            }


            /*
             * Windows console keyboard -> X128.
             *
             * Retained for development and future
             * SYSOP use.
             */
            while (_kbhit())
            {
                ch =
                    (unsigned char)_getch();


                /*
                 * Ignore Windows extended keys.
                 */
                if (ch == 0 || ch == 224)
                {
                    if (_kbhit())
                    {
                        (void)_getch();
                    }

                    continue;
                }


                /*
                 * Console input uses normal 10 ms
                 * C128 pacing.
                 */
                if (send_to_x128(x128Socket,
                                 (const char *)&ch,
                                 1)
                    == SOCKET_ERROR)
                {
                    fprintf(stderr,
                            "tcpsrv: X128 keyboard send failed\n");

                    break;
                }
            }
        }


        /*
         * Clean up caller if still connected.
         */
        if (callerSocket != INVALID_CLIENT)
        {
            shutdown(callerSocket,
                     SD_BOTH);

            closesocket(callerSocket);

            callerSocket =
                INVALID_CLIENT;
        }


        /*
         * Clean up X128 connection.
         */
        shutdown(x128Socket,
                 SD_BOTH);

        closesocket(x128Socket);


        /*
         * Stay alive and wait for X128 again.
         */
        fprintf(stderr,
                "tcpsrv: waiting for X128\n");
    }


    /*
     * Final shutdown.
     */
    closesocket(callerListen);
    closesocket(x128Listen);

    WSACleanup();

    return 0;
}

Blacklord

Diffs to the original 1981 (v3.7) CP/M version so far :

16 message areas (the original had 1), private messages removed to their own message base rather than having one large, monolithic message base.

Blacklord

Interim code (15/8/2026) (WIP)

10 rem rbbs-128 1.0 main program
20 da=56832:sa=56833:ca=56834:ta=56835
30 sd=8:dd=9:fd=10
40 fast
45 dim m(200,2),ms$(10),ya$(16),ys$(16),ym$(16),yt$(16)
50 graphic 5
60 poke ta,30:poke ca,11
62 c1$=chr$(35)+chr$(67)+chr$(79)+chr$(78)+chr$(78)+chr$(69)+chr$(67)+chr$(84)+chr$(58)
64 c3$=chr$(35)+chr$(68)+chr$(73)+chr$(83)+chr$(67)+chr$(79)+chr$(78)+chr$(78)+chr$(69)+chr$(67)+chr$(84)
70 scnclr
80 print "rbbs-128 1.0"
90 print
100 print "commodore 128 remote bulletin board v1.0"
110 print
120 gosub 8300:gosub 10200:goto 7000

1000 rem caller login
1010 ur=0:hm=0:mf$="":pw$="":ci$=""
1020 a1$="enter your first name: ":n=1:gosub 5040
1030 c=1:ml=20:gosub 5170
1040 if cd then goto 7900
1050 n$=b$
1060 if n$="" then 1020
1070 if n$<"a" or len(n$)=1 then 1020
1080 ck=0
1090 for ix=1 to len(n$)
1100 if mid$(n$,ix,1)=" " then ck=1
1110 next ix
1120 if ck=0 then 1150
1130 a$="please do not use spaces in your name":gosub 5040
1140 gosub 5040:goto 1020
1150 a1$="enter your last name:  ":n=1:gosub 5040
1160 c=1:ml=20:gosub 5170
1170 if cd then goto 7900
1180 o$=b$
1190 if o$="" then 1020
1200 if o$<"a" or len(o$)=1 then 1020
1210 ck=0
1220 for ix=1 to len(o$)
1230 if mid$(o$,ix,1)=" " then ck=1
1240 next ix
1250 if ck=0 then 1280
1260 a$="please do not use spaces in your name":gosub 5040
1270 gosub 5040:goto 1150
1280 a$="checking user file...":gosub 5040
1290 gosub 2000
1300 if ok=0 then 1400
1310 a1$="enter your password: ":n=1:gosub 5040
1320 c=2:ml=20:gosub 5170
1330 if cd then goto 7900
1340 if b$=pw$ then 1370
1350 a$="incorrect password":gosub 5040
1360 goto 1310
1370 a$="login accepted":gosub 5040
1380 a$="welcome "+n$+" "+o$:gosub 5040
1390 goto 1800

1400 rem new caller
1410 a$="new user registration":gosub 5040
1420 gosub 2200
1430 if cd then goto 7900
1440 if ok=0 then 1020
1450 a$="registration complete":gosub 5040
1460 a$="welcome "+n$+" "+o$:gosub 5040

1800 rem login complete
1810 lg=-1
1820 gosub 8700
1830 gosub 5040
1840 a$="you are caller number:"+str$(tc):gosub 5040
1850 a$="rbbs-128 login complete.":gosub 5040
1860 a$="date/time: "+dd$+"/"+mo$+"/"+yr$+" "+tm$:gosub 5040
1870 gosub 9000
1880 goto 4000

2000 rem check rel users file
2010 ok=0:pw$="":ci$="":hm=0:ur=0:mf$=""
2020 open 15,dd,15
2030 open 2,dd,2,"users"
2040 rn=1:gosub 8000:gosub 8100
2050 nu=val(r$)
2060 if nu<1 then 2160
2070 for ur=2 to nu+1
2080 rn=ur:gosub 8000:gosub 8100
2090 u$=mid$(r$,3)
2100 gosub 2700
2110 if f1$=n$ and f2$=o$ then 2140
2120 next ur
2130 goto 2160
2140 mf$=left$(r$,1)
2150 pw$=f4$:ci$=f3$:hm=val(f5$):ok=-1
2160 close 2
2170 close 15
2180 return

2200 rem new user registration
2210 ok=0
2220 a1$="city/state: ":n=1:gosub 5040
2230 c=1:ml=30:gosub 5170
2240 if cd then return
2250 ci$=b$
2260 if len(ci$)<2 then 2220
2270 a1$="choose a password: ":n=1:gosub 5040
2280 c=2:ml=20:gosub 5170
2290 if cd then return
2300 p1$=b$
2310 if len(p1$)<3 then 2270
2320 a1$="enter password again: ":n=1:gosub 5040
2330 c=2:ml=20:gosub 5170
2340 if cd then return
2350 if b$=p1$ then 2390
2360 a$="passwords do not match":gosub 5040
2370 gosub 5040
2380 goto 2270
2390 gosub 5040
2400 a$="name: "+n$+" "+o$:gosub 5040
2410 a$="city/state: "+ci$:gosub 5040
2420 a1$="is this correct (y/n)? ":n=1:gosub 5040
2430 c=1:ml=1:gosub 5170
2440 if cd then return
2450 if b$="" then 2420
2460 if asc(b$)=89 then 2500
2470 if asc(b$)=78 then 2220
2480 goto 2420

2500 rem append new rel user
2510 ok=0
2520 open 15,dd,15
2530 open 2,dd,2,"users"
2540 rn=1:gosub 8000:gosub 8100
2550 nu=val(r$)
2560 rn=nu+2:ur=rn
2570 gosub 8000
2580 r$="  "+n$+";"+o$+";"+ci$+";"+p1$+";0"
2590 gosub 8200
2600 rn=1:gosub 8000
2610 r$=str$(nu+1)
2620 gosub 8200
2630 close 2
2640 input#15,en,em$,et,es
2650 close 15
2660 if en<>0 then 2690
2670 pw$=p1$:hm=0:mf$=" ":ok=-1
2680 return
2690 a$="error writing users file":gosub 5040
2695 return

2700 rem split user record
2710 f1$="":f2$="":f3$="":f4$="":f5$=""
2720 fi=1
2730 for ix=1 to len(u$)
2740 q$=mid$(u$,ix,1)
2750 if q$=";" then fi=fi+1:goto 2810
2760 if fi=1 then f1$=f1$+q$
2770 if fi=2 then f2$=f2$+q$
2780 if fi=3 then f3$=f3$+q$
2790 if fi=4 then f4$=f4$+q$
2800 if fi=5 then f5$=f5$+q$
2810 next ix
2820 return

2900 rem update current user record
2910 if ur=0 then return
2920 if mf$="" then mf$=" "
2930 open 15,dd,15
2940 open 2,dd,2,"users"
2950 rn=ur:gosub 8000
2960 r$=mf$+" "+n$+";"+o$+";"+ci$+";"+pw$+";"+str$(hm)
2970 gosub 8200
2980 close 2
2990 close 15
2995 return

3000 rem sysop functions - reserved
3010 return

4000 rem temporary command loop
4010 gosub 5040
4020 a1$=n$+"? command (a,g,?): ":n=1:gosub 5040
4030 c=1:ml=1:gosub 5170
4040 if cd then goto 7900
4050 if b$="" then 4000
4060 if b$="a" then gosub 9000:goto 4000
4070 if b$="g" then 4200
4080 if b$="?" then 4100
4090 a$="i don't understand '"+b$+"'":gosub 5040
4095 goto 4000

4100 rem temporary help
4110 a$="a - change message area":gosub 5040
4120 a$="g - goodbye":gosub 5040
4130 a$="? - this help":gosub 5040
4140 goto 4000

4200 rem temporary goodbye
4210 gosub 5040
4220 a$="thanks for calling, "+n$+".":gosub 5040
4230 a$="please call again!":gosub 5040
4240 if ur>0 then gosub 2900
4250 lg=0
4260 a$="please disconnect.":gosub 5040
4270 goto 7800

5040 rem rbbs-128 output service
5050 if a1$="" then 5080
5060 a$=a1$
5070 a1$=""
5080 if len(a$)=0 then 5140
5090 for ix=1 to len(a$)
5100 ch=asc(mid$(a$,ix,1))
5110 gosub 5800
5120 gosub 5700
5130 next ix
5135 pp$=a$
5140 if n=1 then 5160
5150 ch=13:gosub 5800:ch=10:gosub 5800:print
5160 a$="":n=0:return

5170 rem rbbs-128 line input service
5180 b$=""
5190 gosub 6210
5200 return

5600 rem initialize swiftlink acia
5610 poke ta,30
5620 poke ca,11
5630 return

5700 rem display ascii byte on c128
5710 x=ch
5720 if x>=97 and x<=122 then x=x-32
5730 print chr$(x);
5740 return

5800 rem send ascii byte to tcpsrv
5810 if (peek(sa) and 16)=0 then 5810
5820 poke da,ch
5830 return

6000 rem get serial byte
6010 if (peek(sa) and 8)=0 then 6010
6020 ch=peek(da)
6030 return

6100 rem get raw tcpsrv control line
6110 ct$=""
6120 gosub 6000
6130 if ch=10 then 6120
6140 if ch=13 then 6180
6150 if ch<32 or ch>126 then 6120
6160 ct$=ct$+chr$(ch)
6170 goto 6120
6180 return

6210 rem rbbs-128 character line editor
6220 b$=""
6230 gosub 6000
6240 if ch=10 then 6230
6250 if ch=13 then 6520
6260 if ch=8 then 6400
6270 if ch=127 then 6400
6280 if ch=18 then 6450
6290 if ch=21 then 6480
6300 if ch=24 then 6480
6310 if ch=12 then 6450
6320 if ch<32 then 6230
6330 if ch>126 then 6230
6340 if len(b$)>=ml then 6230
6350 x=ch
6360 if x>=97 and x<=122 then x=x-32
6370 b$=b$+chr$(x)
6380 if c=1 then gosub 6680
6390 goto 6230

6400 rem delete last character
6410 if len(b$)=0 then 6230
6420 b$=left$(b$,len(b$)-1)
6430 if c=2 then 6230
6440 ch=8:gosub 6680
6445 goto 6230

6450 rem ctrl-r or ctrl-l - retype line
6455 if c=2 then 6230
6460 print
6465 if len(b$)=0 then 6230
6470 for ix=1 to len(b$)
6472 ch=asc(mid$(b$,ix,1))
6474 gosub 6680
6476 next ix
6478 goto 6230

6480 rem ctrl-u or ctrl-x - erase line
6485 if len(b$)=0 then 6230
6490 if c=2 then b$="":goto 6230
6495 for ix=1 to len(b$)
6500 ch=8:gosub 6680
6505 next ix
6510 b$=""
6515 goto 6230

6520 rem finish input line
6525 if right$(b$,len(c3$))=c3$ then cd=-1:return
6530 if c=1 then ch=13:gosub 6600
6535 if c=2 then print
6540 ch=13:gosub 5800
6545 ch=10:gosub 5800
6550 return

6600 rem echo ascii byte locally
6610 if ch=13 then print:return
6620 if ch=10 then return
6630 if ch=8 then print chr$(20);:return
6640 x=ch
6650 if x>=97 and x<=122 then x=x-32
6660 print chr$(x);
6670 return

6680 rem echo ascii byte local and remote
6685 gosub 5800
6690 gosub 6600
6695 return

6700 rem display ascii string locally
6710 for ix=1 to len(a$)
6720 x=asc(mid$(a$,ix,1))
6730 if x>=97 and x<=122 then x=x-32
6740 print chr$(x);
6750 next ix
6760 print
6770 return

7000 rem waiting for call
7010 cd=0:lg=0:ur=0
7020 print
7030 print "waiting for call..."
7040 gosub 6100
7050 p=instr(ct$,c1$)
7060 if p>0 then 7200
7070 goto 7040

7200 rem process connect and host date/time
7210 dt$=mid$(ct$,p+len(c1$))
7220 if len(dt$)<19 then 7300
7230 dd$=left$(dt$,2)
7240 mo$=mid$(dt$,4,2)
7250 yr$=mid$(dt$,7,4)
7260 tm$=mid$(dt$,12,8)
7270 ti$=mid$(dt$,12,2)+mid$(dt$,15,2)+mid$(dt$,18,2)
7280 print
7285 print "caller connected"
7290 print "date: ";dd$;"/";mo$;"/";yr$
7292 print "time: ";tm$
7295 goto 7400

7300 rem invalid time packet
7310 print "invalid time packet"
7320 goto 7800

7400 rem start rbbs caller session
7410 cd=0
7420 scnclr
7430 a$="rbbs-128 1.0":gosub 5040
7440 gosub 5040
7450 a$="commodore 128 remote bulletin board":gosub 5040
7460 gosub 5040
7470 goto 1000

7800 rem wait for caller disconnect
7810 gosub 6100
7820 if instr(ct$,c3$)>0 then 7900
7830 goto 7810

7900 rem caller disconnected cleanup
7910 if lg and ur>0 then gosub 2900
7920 cd=0:lg=0:ur=0
7930 n$="":o$="":pw$="":ci$="":mf$=""
7940 scnclr
7950 print "rbbs-128 1.0"
7960 print
7970 print "caller disconnected"
7980 goto 7000

8000 rem position 62 byte user record
8010 record#2,(rn),1
8020 return

8100 rem read 62 byte rel record
8110 r$=""
8120 for ri=1 to 62
8130 get#2,q$
8140 if q$="" then q$=chr$(0)
8150 r$=r$+q$
8160 next ri
8170 return

8200 rem write 62 byte rel record
8210 if len(r$)>=62 then 8240
8220 r$=r$+" "
8230 goto 8210
8240 if len(r$)>62 then r$=left$(r$,62)
8250 print#2,r$;
8260 return

8300 rem read system counters
8310 tc=0:mg=0:ls=0:rc=0
8320 dopen#4,"counters",d0,u9
8330 record#4,1,1
8340 r$=""
8350 for ri=1 to 32
8360 get#4,q$
8370 if q$="" then q$=" "
8380 r$=r$+q$
8390 next ri
8400 dclose#4
8410 z1$="":z2$="":z3$="":z4$=""
8420 zi=1
8430 for ix=1 to len(r$)
8440 q$=mid$(r$,ix,1)
8450 if q$=";" then zi=zi+1:goto 8500
8460 if zi=1 then z1$=z1$+q$
8470 if zi=2 then z2$=z2$+q$
8480 if zi=3 then z3$=z3$+q$
8490 if zi=4 then z4$=z4$+q$
8500 next ix
8510 tc=val(z1$):mg=val(z2$):ls=val(z3$):rc=val(z4$)
8520 return

8600 rem save system counters
8610 r$=str$(tc)+";"+str$(mg)+";"+str$(ls)+";"+str$(rc)
8620 if len(r$)>=32 then 8650
8630 r$=r$+" "
8640 goto 8620
8650 if len(r$)>32 then r$=left$(r$,32)
8660 dopen#4,"counters",d0,u9
8670 record#4,1,1
8680 print#4,r$;
8690 dclose#4
8695 return

8700 rem record successful caller
8710 tc=tc+1
8720 rc=rc+1
8730 r$=dd$+"/"+mo$+"/"+yr$+" "+tm$+";"+n$+";"+o$+";"+ci$
8740 if len(r$)>=96 then 8770
8750 r$=r$+" "
8760 goto 8740
8770 if len(r$)>96 then r$=left$(r$,96)
8780 dopen#5,"callers",d0,u9
8790 record#5,rc+1,1
8800 print#5,r$;
8810 record#5,1,1
8820 r$=str$(rc)
8830 if len(r$)>=96 then 8860
8840 r$=r$+" "
8850 goto 8830
8860 if len(r$)>96 then r$=left$(r$,96)
8870 print#5,r$;
8880 dclose#5
8890 gosub 8600
8895 return

9000 rem message area selector
9010 gosub 5040
9020 a$="message areas":gosub 5040
9030 gosub 5040
9040 if na<1 then 9290
9050 for ai=1 to na
9060 a$=str$(ai)+" - "+ya$(ai):gosub 5040
9070 next ai
9080 gosub 5040
9090 a1$="select area (1-"+mid$(str$(na),2)+"): ":n=1:gosub 5040
9092 c=1:ml=3:gosub 5170
9094 if cd then return
9096 if b$="" then 9090
9098 ma=val(b$)
9100 if ma<1 or ma>na then 9090
9110 an$=ya$(ma)
9120 sn$=ys$(ma)
9130 mn$=ym$(ma)
9140 at$=yt$(ma)
9150 gosub 9900
9160 gosub 5040
9170 a$="current area: "+an$:gosub 5040
9180 a$="messages:"+str$(mc):gosub 5040
9190 return
9290 a$="no message areas configured.":gosub 5040
9295 return

9600 rem read 64 byte area record
9610 r$=""
9620 for ri=1 to 64
9630 get#6,q$
9640 if q$="" then q$=" "
9650 r$=r$+q$
9660 next ri
9670 return

9700 rem split message area definition
9710 an$="":sn$="":mn$="":at$=""
9720 fi=1
9730 for ix=1 to len(r$)
9740 q$=mid$(r$,ix,1)
9750 if q$=";" then fi=fi+1:goto 9800
9760 if fi=1 then an$=an$+q$
9770 if fi=2 then sn$=sn$+q$
9780 if fi=3 then mn$=mn$+q$
9790 if fi=4 then at$=at$+q$
9800 next ix
9810 gosub 9820
9815 return

9820 rem trim area fields
9830 if len(an$)>0 and right$(an$,1)=" " then an$=left$(an$,len(an$)-1):goto 9830
9840 if len(sn$)>0 and right$(sn$,1)=" " then sn$=left$(sn$,len(sn$)-1):goto 9840
9850 if len(mn$)>0 and right$(mn$,1)=" " then mn$=left$(mn$,len(mn$)-1):goto 9850
9860 if len(at$)>0 and right$(at$,1)=" " then at$=left$(at$,len(at$)-1):goto 9860
9870 return

9900 rem load active area summary index
9910 mz=0:mx=0:iu=0:lm=0:ac=0
9920 dopen#7,(sn$),d0,u9
9930 re=1
9940 record#7,re,1
9950 gosub 10100
9960 g=val(r$)
9970 if g>9998 then 10060
9980 if g>lm then lm=g
9990 mz=mz+1
10000 if g<>0 then ac=ac+1
10010 if mz<=200 then m(mz,1)=g
10020 if g<>0 and iu=0 then iu=g
10030 re=re+5
10040 record#7,re,1
10050 gosub 10100
10052 if mz<=200 then m(mz,2)=val(r$)
10054 mx=mx+val(r$)+6
10056 re=re+1
10058 goto 9940
10060 dclose#7
10070 mc=ac
10080 return

10100 rem read 30 byte summary record
10110 r$=""
10120 for ri=1 to 30
10130 get#7,q$
10140 if q$="" then q$=" "
10150 r$=r$+q$
10160 next ri
10170 return

10200 rem load message areas at startup
10210 na=0
10220 dopen#6,"msgareas",d0,u9
10230 record#6,1,1
10240 gosub 9600
10250 na=val(r$)
10260 if na>16 then na=16
10270 if na<1 then 10380
10280 for ai=1 to na
10290 record#6,ai+1,1
10300 gosub 9600
10310 gosub 9700
10320 ya$(ai)=an$
10330 ys$(ai)=sn$
10340 ym$(ai)=mn$
10350 yt$(ai)=at$
10360 next ai
10380 dclose#6
10390 return

10400 rem per user area last-read pointers - next stage
10410 return

11000 rem common message engine - future
11010 return

14000 rem caller log and statistics - future
14010 return

15000 rem bulletins news and help - future
15010 return

16000 rem file area system - future
16010 return

17000 rem punter file transfers - future
17010 return

18000 rem doors and external programs - future
18010 return

19000 rem sysop chat and status window - future
19010 return

20000 rem ansi terminal support - future
20010 return

21000 rem reserved expansion
21010 return