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.
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;
}
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.
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