Contents

1. Introduction
2. The main loop
3. Locations
4. Objects
5. Inventory
6. Passages
7. Distance
8. North, east, south, west
9. Code generation
10. More attributes
11. Conditions
12. Open and close
13. The parser
14. Multiple nouns
15. Light and dark
16. Savegame
17. Test automation
18. Abbreviations
19. Conversations
20. Combat
21. Multi-player
22. Client-server
23. Database
24. Speech
25. JavaScript

How to program a text adventure in C

by Ruud Helderman <r.helderman@hccnet.nl>

Licensed under MIT License

22. Client-server

It would be perfectly retro to have a single person type in every command for every player. What we made in the previous chapter, might already be enough to run your own play-by-mail game. But in the 21st century, ‘multi-player’ has become synonymous with online gaming. So let’s see if we can make that happen!

To make our game multi-user, we will turn it into a TCP server. Each user will connect to the server with a standard Telnet client. Sounds old-school? It is. But it’s probably closest to how the early MUDs were made. And it’s fun to write your own server almost from scratch.

So basically, our program lilcave should be running continuously on a computer. The computer should always be online; it should never be turned off. In the early days of the internet, that typically meant the program would be running on a university mini or mainframe, under the supervision of the super user (or more likely, covertly scheduled by a smart student).

With today’s hardware, any PC will do fine running such a game. But since it is an online game, there are a few things to keep in mind.

Implementing a TCP server in C is pretty straightforward. It involves writing quite a bit of boilerplate code around Berkeley sockets, the de facto standard for a client-server application. Fortunately, code samples can be found anywhere on the internet; even in the Wikipedia article about select.

server.h
  1. extern void server(void (*action)(char *, int));
server.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <netinet/in.h>
  5. #include "break.h"
  6. #include "object.h"
  7. #include "print.h"
  8. #include "outbuf.h"
  9. #include "telnet.h"
  10. #include "client.h"
  11. #include "tcp.h"
  12. #define PORT 18811
  13. void server(void (*action)(char *, int))
  14. {
  15. CLIENT *client;
  16. int i;
  17. struct sockaddr_in address;
  18. int listener = tcpListen(&address, PORT);
  19. if (listener == -1) return;
  20. for (clientInit(), breakInit(); breakTest(); )
  21. {
  22. fd_set fds;
  23. int fd = listener;
  24. FD_ZERO(&fds);
  25. FD_SET(listener, &fds);
  26. for (i = 0; (client = clientGet(i)) != NULL; i++)
  27. {
  28. if (client->fd != -1) FD_SET(client->fd, &fds);
  29. if (client->fd > fd) fd = client->fd;
  30. }
  31. if (tcpSelect(fd + 1, &fds) == -1) break;
  32. if (FD_ISSET(listener, &fds))
  33. {
  34. fd = tcpAccept(&address, listener);
  35. if (fd == -1) break;
  36. printConsole("Socket %d connected.\n", fd);
  37. outbufClear();
  38. telnetConfigure();
  39. outbufFormat("Welcome to Little Cave Adventure.\n");
  40. client = clientGetFree();
  41. if (client != NULL)
  42. {
  43. client->fd = fd;
  44. client->obj = nobody;
  45. telnetInit(&client->inbuf);
  46. telnetAppendPrompt(&client->inbuf);
  47. outbufFlush(fd);
  48. }
  49. else
  50. {
  51. outbufFormat("All sockets occupied, please try again later.\n");
  52. outbufFlush(fd);
  53. tcpDisconnect(fd);
  54. }
  55. }
  56. for (i = 0; breakTest() && (client = clientGet(i)) != NULL; i++)
  57. {
  58. if (FD_ISSET(client->fd, &fds))
  59. {
  60. static char buffer[1024];
  61. int len = read(client->fd, buffer, sizeof buffer);
  62. if (len > 0)
  63. {
  64. player = client->obj;
  65. printSetCurrent(client->fd);
  66. telnetParse(&client->inbuf, client->fd, action, buffer, len);
  67. if (player != client->obj)
  68. {
  69. printConsole("Socket %d is %s.\n", fd, player->description);
  70. client->obj = player;
  71. }
  72. }
  73. else if (len == 0)
  74. {
  75. tcpDisconnect(client->fd);
  76. client->fd = -1;
  77. }
  78. }
  79. }
  80. }
  81. printConsole("Interrupted by signal %d.\n", breakSignalNumber());
  82. for (i = 0; (client = clientGet(i)) != NULL; i++)
  83. {
  84. tcpDisconnect(client->fd);
  85. }
  86. tcpClose(listener, PORT);
  87. }

Explanation:

For readability, I implemented a number of functions in a separate module.

tcp.h
  1. extern int tcpListen(struct sockaddr_in *addr, uint16_t port);
  2. extern void tcpClose(int fd, uint16_t port);
  3. extern int tcpSelect(int nfds, fd_set *readfds);
  4. extern int tcpAccept(struct sockaddr_in *addr, int listener);
  5. extern void tcpDisconnect(int fd);
  6. extern void tcpSend(int fd, const char *data, int len);
tcp.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <errno.h>
  5. #include <netinet/in.h>
  6. #include "object.h"
  7. #include "print.h"
  8. static int assert(const char *name, int retval)
  9. {
  10. if (retval == -1) perror(name);
  11. return retval;
  12. }
  13. static struct sockaddr *setPort(struct sockaddr_in *addr, uint16_t port)
  14. {
  15. addr->sin_family = AF_INET;
  16. addr->sin_addr.s_addr = INADDR_ANY;
  17. addr->sin_port = htons(port);
  18. return (struct sockaddr *)addr;
  19. }
  20. int tcpListen(struct sockaddr_in *addr, uint16_t port)
  21. {
  22. int fd = assert("socket", socket(AF_INET, SOCK_STREAM, 0));
  23. if (fd != -1)
  24. {
  25. int opt = 1;
  26. if (-1 != assert("setsockopt", setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
  27. (char *)&opt, sizeof opt)) &&
  28. -1 != assert("bind", bind(fd, setPort(addr, port), sizeof *addr)) &&
  29. -1 != assert("listen", listen(fd, 3)))
  30. {
  31. printConsole("Listening to port %u.\n", (unsigned int)port);
  32. }
  33. else
  34. {
  35. close(fd);
  36. fd = -1;
  37. }
  38. }
  39. return fd;
  40. }
  41. void tcpClose(int fd, uint16_t port)
  42. {
  43. close(fd);
  44. printConsole("No longer listening to port %u.\n", (unsigned int)port);
  45. }
  46. int tcpSelect(int nfds, fd_set *readfds)
  47. {
  48. return assert("select", select(nfds, readfds, NULL, NULL, NULL));
  49. }
  50. int tcpAccept(struct sockaddr_in *addr, int listener)
  51. {
  52. socklen_t len = sizeof *addr;
  53. return assert("accept", accept(listener, (struct sockaddr *)addr, &len));
  54. }
  55. void tcpDisconnect(int fd)
  56. {
  57. if (fd != -1)
  58. {
  59. close(fd);
  60. printConsole("Socket %d disconnected.\n", fd);
  61. }
  62. }
  63. void tcpSend(int fd, const char *data, int len)
  64. {
  65. int written;
  66. while (len > 0 && ((written = write(fd, data, len)) >= 0 || errno == EINTR))
  67. {
  68. if (written > 0) data += written, len -= written;
  69. }
  70. }

Explanation:

The following module provides access to an array of structs holding information about the clients that have connected to the game. For each client, there is a file descriptor (the socket), an object (the player character) and an input buffer (see telnet.h below).

client.h
  1. typedef struct
  2. {
  3. int fd;
  4. OBJECT *obj;
  5. INBUF inbuf;
  6. }
  7. CLIENT;
  8. extern void clientInit(void);
  9. extern CLIENT *clientGet(int i);
  10. extern CLIENT *clientGetFree(void);
client.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "object.h"
  4. #include "telnet.h"
  5. #include "client.h"
  6. #define MAX_CLIENTS 30
  7. static CLIENT clients[MAX_CLIENTS];
  8. void clientInit(void)
  9. {
  10. int i;
  11. for (i = 0; i < MAX_CLIENTS; i++) clients[i].fd = -1;
  12. }
  13. CLIENT *clientGet(int i)
  14. {
  15. return i < MAX_CLIENTS ? clients + i : NULL;
  16. }
  17. CLIENT *clientGetFree(void)
  18. {
  19. CLIENT *client;
  20. int i;
  21. for (i = 0; (client = clientGet(i)) != NULL && client->fd != -1; i++);
  22. return client;
  23. }

Explanation:

The following module handles the peculiarities of the Telnet protocol. It also takes care of buffering client input (i.e. everything typed in by the users), and it prevents server output (which could be pushed at any time by activity from other users) getting mixed up with echo on the same text line in the client’s Telnet window.

telnet.h
  1. typedef struct
  2. {
  3. bool iac;
  4. int negotiate;
  5. unsigned index;
  6. char data[100];
  7. }
  8. INBUF;
  9. extern void telnetInit(INBUF *inbuf);
  10. extern void telnetConfigure(void);
  11. extern void telnetInsertSpaces(INBUF *inbuf);
  12. extern void telnetDeleteSpaces(INBUF *inbuf);
  13. extern void telnetAppendPrompt(INBUF *inbuf);
  14. extern void telnetDeletePrompt(INBUF *inbuf);
  15. extern void telnetParse(INBUF *inbuf, int fd, void (*action)(char *, int),
  16. const char *data, int length);
telnet.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include "outbuf.h"
  4. #include "telnet.h"
  5. static const char prompt[] = "--> ";
  6. void telnetInit(INBUF *inbuf)
  7. {
  8. inbuf->iac = false;
  9. inbuf->negotiate = 0;
  10. inbuf->index = 0;
  11. }
  12. void telnetConfigure(void)
  13. {
  14. static const char config[] = {
  15. '\xFF', '\xFD', 34, '\xFF', '\xFA', 34, 1, 0, '\xFF', '\xF0',
  16. '\xFF', '\xFB', 1
  17. };
  18. outbufBytes(config, sizeof config);
  19. }
  20. void telnetInsertSpaces(INBUF *inbuf)
  21. {
  22. outbufInsertString(0, "\r\r");
  23. outbufInsertSpaces(1, inbuf->data, inbuf->index);
  24. outbufInsertSpaces(1, prompt, sizeof prompt - 1);
  25. }
  26. void telnetDeleteSpaces(INBUF *inbuf)
  27. {
  28. outbufMove(inbuf->index + sizeof prompt + 1, 0);
  29. }
  30. void telnetAppendPrompt(INBUF *inbuf)
  31. {
  32. outbufBytes(prompt, sizeof prompt - 1);
  33. outbufBytes(inbuf->data, inbuf->index);
  34. }
  35. void telnetDeletePrompt(INBUF *inbuf)
  36. {
  37. outbufRewind(inbuf->index + sizeof prompt - 1);
  38. }
  39. void telnetParse(INBUF *inbuf, int fd, void (*action)(char *, int),
  40. const char *data, int length)
  41. {
  42. int i;
  43. outbufClear();
  44. for (i = 0; i < length; i++)
  45. {
  46. int c = data[i];
  47. if (c == '\xFF' || inbuf->iac)
  48. {
  49. if (c == '\xF0' || inbuf->negotiate != '\xFA') inbuf->negotiate = c;
  50. inbuf->iac = !inbuf->iac;
  51. }
  52. else if (inbuf->negotiate >= '\xFA' && inbuf->negotiate <= '\xFE')
  53. {
  54. if (inbuf->negotiate != '\xFA') inbuf->negotiate = 0;
  55. }
  56. else if (c == '\r')
  57. {
  58. outbufFormat("\n");
  59. outbufFlush(fd);
  60. inbuf->data[inbuf->index] = '\0';
  61. (*action)(inbuf->data, sizeof inbuf->data);
  62. inbuf->index = 0;
  63. outbufClear();
  64. outbufBytes(prompt, sizeof prompt - 1);
  65. }
  66. else if (c == '\b' || c == '\x7F')
  67. {
  68. if (inbuf->index > 0)
  69. {
  70. outbufByte('\b');
  71. outbufAsSpace(inbuf->data[--inbuf->index]);
  72. outbufByte('\b');
  73. }
  74. }
  75. else if (c >= ' ' && c < '\x7F')
  76. {
  77. if (inbuf->index < sizeof inbuf->data - 1)
  78. {
  79. outbufByte(c);
  80. inbuf->data[inbuf->index++] = c;
  81. }
  82. }
  83. }
  84. outbufFlush(fd);
  85. }

Explanation:

Output produced by the game, should be written to sockets rather than standard output. This is less straightforward than it seems. When two or more players are in the same room, then the actions of one player should be reported to all. So output may need to go to more than one client.

print.h
  1. extern void printSetCurrent(int fd);
  2. extern void printConsole(const char *format, ...);
  3. extern void printPrivate(const char *format, ...);
  4. extern void printSee(const char *format, ...);
  5. extern void printAny(OBJECT *obj1, OBJECT *obj2, const char *sense,
  6. const char *format, ...);
print.c
  1. #include <stdarg.h>
  2. #include <stdbool.h>
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include "object.h"
  6. #include "outbuf.h"
  7. #include "telnet.h"
  8. #include "client.h"
  9. #define VA(print) va_list ap; va_start(ap, format); print; va_end(ap)
  10. static int currentSocket = STDOUT_FILENO;
  11. static bool filter(OBJECT *obj, OBJECT *obj1, OBJECT *obj2)
  12. {
  13. return obj2 == NULL ? obj == obj1 : obj != obj1 && obj != obj2;
  14. }
  15. static void printDemux(OBJECT *obj1, OBJECT *obj2)
  16. {
  17. if (currentSocket == STDOUT_FILENO || player == nobody)
  18. {
  19. if (filter(player, obj1, obj2)) outbufFlush(currentSocket);
  20. }
  21. else
  22. {
  23. CLIENT *client;
  24. int i;
  25. for (i = 0; (client = clientGet(i)) != NULL; i++)
  26. {
  27. if (client->fd != -1 && client->obj->location == player->location &&
  28. filter(client->obj, obj1, obj2))
  29. {
  30. if (client->fd == currentSocket)
  31. {
  32. outbufFlush(client->fd);
  33. }
  34. else
  35. {
  36. telnetInsertSpaces(&client->inbuf);
  37. telnetAppendPrompt(&client->inbuf);
  38. outbufFlush(client->fd);
  39. telnetDeletePrompt(&client->inbuf);
  40. telnetDeleteSpaces(&client->inbuf);
  41. }
  42. }
  43. }
  44. }
  45. }
  46. static void printObserve(OBJECT *obj1, OBJECT *obj2, const char *sense,
  47. const char *format, va_list ap)
  48. {
  49. outbufClear();
  50. outbufFormatVar(format, ap);
  51. if (obj1 != obj2)
  52. {
  53. printDemux(obj1, NULL);
  54. }
  55. if (sense != NULL)
  56. {
  57. outbufInsertString(3, obj1->description);
  58. outbufInsertString(3, sense);
  59. printDemux(obj2, obj1);
  60. }
  61. }
  62. static void printFd(int fd, const char *format, va_list ap)
  63. {
  64. outbufClear();
  65. outbufFormatVar(format, ap);
  66. outbufFlush(fd);
  67. }
  68. void printSetCurrent(int fd)
  69. {
  70. currentSocket = fd;
  71. }
  72. void printConsole(const char *format, ...)
  73. {
  74. VA(printFd(STDOUT_FILENO, format, ap));
  75. }
  76. void printPrivate(const char *format, ...)
  77. {
  78. VA(printFd(currentSocket, format, ap));
  79. }
  80. void printSee(const char *format, ...)
  81. {
  82. VA(printObserve(player, NULL, " see ", format, ap));
  83. }
  84. void printAny(OBJECT *obj1, OBJECT *obj2, const char *sense,
  85. const char *format, ...)
  86. {
  87. VA(printObserve(obj1, obj2, sense, format, ap));
  88. }

Explanation:

The game engine is never sending output directly to any sockets or other file handles. Instead, everything is sent to an output buffer. This makes it possible to manipulate the text before sending it off to the various sockets, as seen in telnet.c and print.c (above).

outbuf.h
  1. extern void outbufClear(void);
  2. extern void outbufRewind(int len);
  3. extern void outbufByte(char c);
  4. extern void outbufBytes(const char *data, int length);
  5. extern void outbufAsSpace(char c);
  6. extern void outbufFormatVar(const char *format, va_list ap);
  7. extern void outbufFormat(const char *format, ...);
  8. extern bool outbufStartsWith(const char *prefix, int len);
  9. extern bool outbufMove(int from, int to);
  10. extern void outbufInsertString(int pos, const char *string);
  11. extern void outbufInsertSpaces(int pos, const char *data, int len);
  12. extern void outbufFlush(int fd);
outbuf.c
  1. #include <ctype.h>
  2. #include <stdarg.h>
  3. #include <stdbool.h>
  4. #include <stdio.h>
  5. #include <string.h>
  6. #include <netinet/in.h>
  7. #include "tcp.h"
  8. #define MAX_LEN 4095
  9. static int outbufLen;
  10. static char outbufData[MAX_LEN + 1];
  11. static int asSpace(int c)
  12. {
  13. return isspace(c) ? c : ' ';
  14. }
  15. void outbufClear(void)
  16. {
  17. outbufLen = 0;
  18. }
  19. void outbufRewind(int len)
  20. {
  21. outbufLen -= len;
  22. }
  23. void outbufByte(char c)
  24. {
  25. if (outbufLen < MAX_LEN) outbufData[outbufLen++] = c;
  26. }
  27. void outbufBytes(const char *data, int length)
  28. {
  29. for (; length > 0; length--) outbufByte(*data++);
  30. }
  31. void outbufAsSpace(char c)
  32. {
  33. outbufByte(asSpace(c));
  34. }
  35. void outbufFormatVar(const char *format, va_list ap)
  36. {
  37. char *ptr = outbufData + outbufLen;
  38. outbufLen += vsnprintf(ptr, sizeof outbufData - outbufLen, format, ap);
  39. if (outbufLen > MAX_LEN) outbufLen = MAX_LEN;
  40. while (outbufLen < MAX_LEN && (ptr = strchr(ptr, '\n')) != NULL)
  41. {
  42. memmove(ptr + 1, ptr, outbufData + ++outbufLen - ptr);
  43. *ptr = '\r';
  44. ptr += 2;
  45. }
  46. }
  47. void outbufFormat(const char *format, ...)
  48. {
  49. va_list ap;
  50. va_start(ap, format);
  51. outbufFormatVar(format, ap);
  52. va_end(ap);
  53. }
  54. bool outbufStartsWith(const char *prefix, int len)
  55. {
  56. return outbufLen >= len && strncmp(outbufData, prefix, len) == 0;
  57. }
  58. bool outbufMove(int from, int to)
  59. {
  60. outbufLen += to - from;
  61. if (outbufLen > MAX_LEN) outbufLen = MAX_LEN;
  62. if (to < outbufLen)
  63. {
  64. memmove(outbufData + to, outbufData + from, outbufLen - to);
  65. }
  66. return to <= outbufLen;
  67. }
  68. void outbufInsertString(int pos, const char *string)
  69. {
  70. int len = strlen(string);
  71. if (outbufMove(pos, pos + len)) strncpy(outbufData + pos, string, len);
  72. }
  73. void outbufInsertSpaces(int pos, const char *data, int len)
  74. {
  75. if (outbufMove(pos, pos + len))
  76. {
  77. int i;
  78. for (i = 0; i < len; i++) outbufData[pos + i] = asSpace(data[i]);
  79. }
  80. }
  81. void outbufFlush(int fd)
  82. {
  83. tcpSend(fd, outbufData, outbufLen);
  84. }

Explanation:

The following module allows for a graceful exit from the server’s main loop in case the process is forced to stop by an outside signal.

break.h
  1. extern void breakInit(void);
  2. extern bool breakTest(void);
  3. extern int breakSignalNumber(void);
break.c
  1. #include <stdbool.h>
  2. #include <string.h>
  3. #include <signal.h>
  4. static volatile sig_atomic_t done = 0;
  5. static void handler(int signum)
  6. {
  7. done = signum;
  8. }
  9. void breakInit(void)
  10. {
  11. struct sigaction action;
  12. memset(&action, 0, sizeof(action));
  13. action.sa_handler = handler;
  14. sigaction(SIGINT, &action, NULL);
  15. sigaction(SIGTERM, &action, NULL);
  16. }
  17. bool breakTest(void)
  18. {
  19. return done == 0;
  20. }
  21. int breakSignalNumber(void)
  22. {
  23. return done;
  24. }

Explanation:

So we now have a new multi-user game loop (the TCP server), but that does not mean we will discard the original single-user game loop. We still need this loop to resume a saved game, as discussed in chapter 16. After the log file of an earlier game session has been processed, it is even possible to enter additional commands manually, but in most cases, you will want to exit this loop right away by entering ‘quit’ or pressing EOF. The program will then continue to start up its TCP server.

main.c
  1. #include <stdbool.h>
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include "object.h"
  5. #include "print.h"
  6. #include "expand.h"
  7. #include "parsexec.h"
  8. #include "turn.h"
  9. #include "server.h"
  10. static char input[100] = "look around";
  11. static bool getFromFP(FILE *fp)
  12. {
  13. bool ok = fgets(input, sizeof input, fp) != NULL;
  14. if (ok) input[strcspn(input, "\n")] = '\0';
  15. return ok;
  16. }
  17. static bool getInput(const char *filename)
  18. {
  19. static FILE *fp = NULL;
  20. bool ok;
  21. if (fp == NULL)
  22. {
  23. if (filename != NULL) fp = fopen(filename, "rt");
  24. if (fp == NULL) fp = stdin;
  25. }
  26. else if (fp == stdin && filename != NULL)
  27. {
  28. FILE *out = fopen(filename, "at");
  29. if (out != NULL)
  30. {
  31. fprintf(out, "%s\n", input);
  32. fclose(out);
  33. }
  34. }
  35. printConsole("\n--> ");
  36. ok = getFromFP(fp);
  37. if (fp != stdin)
  38. {
  39. if (ok)
  40. {
  41. printConsole("%s\n", input);
  42. }
  43. else
  44. {
  45. fclose(fp);
  46. ok = getFromFP(fp = stdin);
  47. }
  48. }
  49. return ok;
  50. }
  51. static bool processInput(char *ptr, int size)
  52. {
  53. return turn(parseAndExecute(expand(ptr, size)));
  54. }
  55. static void processInputAndLog(char *ptr, int size)
  56. {
  57. static FILE *fp = NULL;
  58. if (size > 0)
  59. {
  60. if (fp != NULL && player != nobody)
  61. {
  62. static OBJECT *lastPlayer = NULL;
  63. if (player != lastPlayer)
  64. {
  65. fprintf(fp, "play %s\n", (lastPlayer = player)->description);
  66. }
  67. fprintf(fp, "%s\n", ptr);
  68. fflush(fp);
  69. }
  70. processInput(ptr, size);
  71. }
  72. else
  73. {
  74. if (fp != NULL) fclose(fp);
  75. fp = ptr == NULL ? NULL : fopen(ptr, "at");
  76. }
  77. }
  78. int main(int argc, char *argv[])
  79. {
  80. (void)argc;
  81. printConsole("Welcome to Little Cave Adventure.\n");
  82. printConsole("You are in single-user mode; enter 'quit' for multi-user.\n");
  83. player = nobody;
  84. while (processInput(input, sizeof input) && getInput(argv[1]));
  85. printConsole("\nGoing into multi-user mode; press ^C to stop.\n");
  86. processInputAndLog(argv[1], 0);
  87. server(processInputAndLog);
  88. processInputAndLog(NULL, 0);
  89. printConsole("\nBye!\n");
  90. return 0;
  91. }

Explanation:

By today’s standards, our program can hardly be called a mature game server. It has several flaws:

It would make more sense to turn the whole game into a web application. For that, it’s probably best to port the program to a different programming language, for example JavaScript. We will give that a shot in chapter 25.


⭳   Download source code 🌀   Run on Repl.it

Next chapter: 23. Database