/*
* Empire - A multi-player, client/server Internet based war game.
- * Copyright (C) 1986-2005, Dave Pare, Jeff Bailey, Thomas Ruschak,
+ * Copyright (C) 1986-2008, Dave Pare, Jeff Bailey, Thomas Ruschak,
* Ken Stevens, Steve McClure
*
* This program is free software; you can redistribute it and/or modify
*
* ---
*
- * See the "LEGAL", "LICENSE", "CREDITS" and "README" files for all the
- * related information and legal notices. It is expected that any future
- * projects/authors will amend these files as needed.
+ * See files README, COPYING and CREDITS in the root of the source
+ * tree for related information and legal notices. It is expected
+ * that future projects/authors will amend these files as needed.
*
* ---
*
- * pr.c: Use to do output to a player
+ * pr.c: Output to players
*
* Known contributors to this file:
* Dave Pare, 1986, 1989
* Steve McClure, 1998-2000
+ * Ron Koenderink, 2005
+ * Markus Armbruster, 2005-2007
*/
+
/*
- * The pr routine historically arranged for nonbuffered i/o
- * because stdio didn't used to automatically flush stdout before
- * it read something from stdin. Now pr() prepends an "output id"
- * in front of each line of text, informing the user interface
- * what sort of item it is seeing; prompt, noecho prompt,
- * more input data, etc.
+ * Player output is fully buffered. It can block only if the
+ * receiving player is the current player and his last command doesn't
+ * have the C_MOD flag. Output to another player must not block
+ * because that player could be gone when the printing thread wakes
+ * up, and the code isn't prepared for that. Output within C_MOD
+ * command never blocks, so that such commands can print freely
+ * without yielding the processor.
+ *
+ * Each line of output starts with an identification character
+ * encoding the output id, followed by space. Ids less than 10 are
+ * encoded as decimal digits, and larger ids as lower case letters,
+ * starting with 'a'. Symbolic names for ids are defined in proto.h.
*/
-#include <string.h>
-#include <fcntl.h>
-#include <ctype.h>
+#include <config.h>
+
#include <stdarg.h>
-#include "proto.h"
-#include "misc.h"
-#include "player.h"
-#include "nat.h"
+#include <stdlib.h>
+#include "com.h"
#include "empio.h"
#include "file.h"
-#include "com.h"
-#include "tel.h"
-#include "server.h"
+#include "journal.h"
+#include "misc.h"
+#include "nat.h"
+#include "player.h"
+#include "proto.h"
#include "prototypes.h"
+#include "server.h"
+#include "tel.h"
+static void pr_player(struct player *pl, int id, char *buf);
+static void upr_player(struct player *pl, int id, char *buf);
static void outid(struct player *pl, int n);
-/*VARARGS*/
+/*
+ * Print to current player similar to printf().
+ * Use printf-style FORMAT with the optional arguments.
+ * Note: `to print' without further qualifications means sending
+ * C_DATA text.
+ */
void
pr(char *format, ...)
{
(void)vsprintf(buf, format, ap);
va_end(ap);
if (player->flags & PF_UTF8)
+ /* normal text needs to be converted to user text */
upr_player(player, C_DATA, buf);
else
+ /* normal text and user text are identical */
pr_player(player, C_DATA, buf);
}
+/*
+ * Print UTF-8 text BUF to current player.
+ */
void
-uprnf(char *buf /* buf is message text */)
+uprnf(char *buf)
{
- /*
- * Translate to ASCII if the client is not in UTF mode
- */
- if (!(player->flags & PF_UTF8))
- prtoascii(buf);
-
- pr_player(player, C_DATA, buf);
+ char *p;
+
+ if (!(player->flags & PF_UTF8)) {
+ p = malloc(strlen(buf) + 1);
+ copy_utf8_to_ascii_no_funny(p, buf);
+ pr_player(player, C_DATA, p);
+ free(p);
+ } else
+ pr_player(player, C_DATA, buf);
}
-/*VARARGS*/
+/*
+ * Send some text to P with id ID, line-buffered.
+ * Format text to send using printf-style FORMAT and optional
+ * arguments. It is assumed to be already user text. Plain ASCII and
+ * text received from the same player are fine, for anything else the
+ * caller has to deal with output filtering.
+ * If a partial line is buffered, terminate it with a newline first.
+ */
void
-pr_id(struct player *p, int id, s_char *format, ...)
+pr_id(struct player *p, int id, char *format, ...)
{
- s_char buf[4096];
+ char buf[4096];
va_list ap;
if (p->curid >= 0) {
io_puts(p->iop, "\n");
+ journal_output(p, p->curid, "\n");
p->curid = -1;
}
va_start(ap, format);
pr_player(p, id, buf);
}
+/*
+ * Send C_FLASH text to PL.
+ * Format text to send using printf-style FORMAT and optional
+ * arguments. It is assumed to be UTF-8.
+ * Initiate an output queue flush, but do not wait for it to complete.
+ */
void
-pr_flash(struct player *pl, char *format
- /* format is message text */, ...)
+pr_flash(struct player *pl, char *format, ...)
{
- char buf[4096]; /* buf is message text */
+ char buf[4096]; /* UTF-8 */
va_list ap;
if (pl->state != PS_PLAYING)
va_start(ap, format);
(void)vsprintf(buf, format, ap);
va_end(ap);
- /*
- * Translate to ASCII if the client is not in UTF mode
- */
if (!(pl->flags & PF_UTF8))
- prtoascii(buf);
+ copy_utf8_to_ascii_no_funny(buf, buf);
pr_player(pl, C_FLASH, buf);
io_output(pl->iop, IO_NOWAIT);
}
+/*
+ * Send C_INFORM text to PL.
+ * Format text to send using printf-style FORMAT and optional
+ * arguments. It is assumed to be plain ASCII.
+ * Initiate an output queue flush, but do not wait for it to complete.
+ */
void
-pr_inform(struct player *pl, s_char *format, ...)
+pr_inform(struct player *pl, char *format, ...)
{
- s_char buf[4096];
+ char buf[4096];
va_list ap;
if (pl->state != PS_PLAYING)
io_output(pl->iop, IO_NOWAIT);
}
+/*
+ * Send C_FLASH text to everyone.
+ * Format text to send using printf-style FORMAT and optional
+ * arguments. It is assumed to be plain ASCII.
+ * Prefix text it with a header suitable for broadcast from deity.
+ * Initiate an output queue flush, but do not wait for it to complete.
+ */
void
-pr_wall(s_char *format, ...)
+pr_wall(char *format, ...)
{
- s_char buf[4096];
+ time_t now;
+ struct tm *tm;
+ char buf[4096]; /* UTF-8 */
+ int n;
struct player *p;
va_list ap;
+ time(&now);
+ tm = localtime(&now);
+ n = sprintf(buf, "BROADCAST from %s @ %02d:%02d: ",
+ getnatp(0)->nat_cnam, tm->tm_hour, tm->tm_min);
+
va_start(ap, format);
- (void)vsprintf(buf, format, ap);
+ (void)vsprintf(buf + n, format, ap);
va_end(ap);
for (p = player_next(0); p; p = player_next(p)) {
if (p->state != PS_PLAYING)
}
}
-void
-pr_player(struct player *pl, int id, s_char *buf)
+/*
+ * Send ID text BUF to PL, line-buffered.
+ * BUF is user text.
+ * If a partial line with different id is buffered, terminate it with
+ * a newline first.
+ */
+static void
+pr_player(struct player *pl, int id, char *buf)
{
- register s_char *p;
- register s_char *bp;
- register int len;
+ char *p;
+ char *bp;
+ int len;
bp = buf;
while (*bp != '\0') {
if (pl->curid == -1)
outid(pl, id);
p = strchr(bp, '\n');
- if (p != 0) {
+ if (p != NULL) {
len = (p - bp) + 1;
- if (pl->command && (pl->command->c_flags & C_MOD))
+ if ((pl->command && (pl->command->c_flags & C_MOD)) ||
+ (player != pl))
io_write(pl->iop, bp, len, IO_NOWAIT);
else
io_write(pl->iop, bp, len, IO_WAIT);
bp += len;
}
}
+ journal_output(pl, id, buf);
}
-void
-upr_player(struct player *pl, int id, char *buf
- /* buf is message text */)
+/*
+ * Send ID text BUF to PL, line-buffered.
+ * This function translates from normal text to user text.
+ * If a partial line with different id is buffered, terminate it with
+ * a newline first.
+ */
+static void
+upr_player(struct player *pl, int id, char *buf)
{
- register char *bp; /* bp is message text */
- register int standout = 0;
- char printbuf[2]; /* bp is message text */
+ char *bp;
+ int standout = 0;
+ char printbuf[2];
+ char ch;
printbuf[0] = '\0';
printbuf[1] = '\0';
bp = buf;
- while (*bp != '\0') {
+ while ((ch = *bp++)) {
if (pl->curid != -1 && pl->curid != id) {
io_puts(pl->iop, "\n");
pl->curid = -1;
if (pl->curid == -1)
outid(pl, id);
- if (*bp < 0) { /* looking for standout bit 0x80 */
+ if (ch & 0x80) {
if (standout == 0) {
printbuf[0] = 0x0e;
io_puts(pl->iop, printbuf);
standout = 1;
}
- *bp &= 0x7f;
+ ch &= 0x7f;
} else {
if (standout == 1) {
printbuf[0] = 0x0f;
standout = 0;
}
}
- if (*bp == '\n') {
- if (pl->command && (pl->command->c_flags & C_MOD))
- io_write(pl->iop, bp, 1, IO_NOWAIT);
+ if (ch == '\n') {
+ if ((pl->command && (pl->command->c_flags & C_MOD)) ||
+ (player != pl))
+ io_write(pl->iop, &ch, 1, IO_NOWAIT);
else
- io_write(pl->iop, bp, 1, IO_WAIT);
+ io_write(pl->iop, &ch, 1, IO_WAIT);
pl->curid = -1;
} else {
- printbuf[0] = *bp;
+ printbuf[0] = ch;
io_puts(pl->iop, printbuf);
}
- bp++;
}
+ journal_output(pl, id, buf);
}
/*
- * highlighted characters have hex 80 or'ed in
- * with them to designate their highlightedness
- */
-void
-pr_hilite(s_char *buf)
-{
- register s_char *bp;
- register s_char c;
- s_char *p;
-
- p = (s_char *)malloc(strlen(buf) + 1);
- strcpy(p, buf);
- for (bp = p; 0 != (c = *bp); bp++)
- if (isprint(c))
- *bp |= 0x80;
- pr(p);
- free(p);
-}
-
-/*
- * output hex code + space
+ * Send id N to PL.
+ * This runs always at the beginning of a line.
*/
static void
outid(struct player *pl, int n)
{
- s_char c;
- s_char buf[3];
+ char buf[3];
+
+ if (CANT_HAPPEN(n > C_LAST))
+ n = C_DATA;
- if (n > C_LAST) {
- logerror("outid: %d not valid code\n", n);
- return;
- }
if (n >= 10)
- c = 'a' - 10 + n;
+ buf[0] = 'a' - 10 + n;
else
- c = '0' + n;
- buf[0] = c;
+ buf[0] = '0' + n;
buf[1] = ' ';
buf[2] = '\0';
io_puts(pl->iop, buf);
pl->curid = n;
}
+/*
+ * Send redirection request REDIR to the current player.
+ * REDIR is UTF-8, but non-ASCII characters can occur only if the
+ * player sent them. Therefore, it is also user text.
+ */
void
-prredir(s_char *redir)
+prredir(char *redir)
{
pr_id(player, *redir == '>' ? C_REDIR : C_PIPE, "%s\n", redir);
}
+/*
+ * Send script execute request FILE to the current player.
+ * REDIR is UTF-8, but non-ASCII characters can occur only if the
+ * player sent them. Therefore, it is also user text.
+ */
void
-prexec(s_char *file)
+prexec(char *file)
{
pr_id(player, C_EXECUTE, "%s\n", file);
}
+/*
+ * Send a command prompt to the current player.
+ */
void
prprompt(int min, int btu)
{
pr_id(player, C_PROMPT, "%d %d\n", min, btu);
}
-void
-showvers(int vers)
-{
- pr_id(player, C_INIT, "%d\n", vers);
-}
-
+/*
+ * Prompt for a line of non-command input.
+ * Send C_FLUSH prompt PROMPT to the current player.
+ * Read a line of input into BUF[SIZE] and convert it to ASCII.
+ * This may block for input, yielding the processor. Flush buffered
+ * output when blocking, to make sure player sees the prompt.
+ * Return number of bytes in BUF[], not counting the terminating 0,
+ * or -1 on error.
+ */
int
-prmptrd(char *prompt, char *str, int size)
+prmptrd(char *prompt, char *buf, int size)
{
int r;
- char *cp;
+
+ if (CANT_HAPPEN(!prompt))
+ prompt = "? ";
pr_id(player, C_FLUSH, "%s\n", prompt);
- if ((r = recvclient(str, size)) < 0)
+ if ((r = recvclient(buf, size)) < 0)
return r;
time(&player->curup);
- if (*str == 0)
+ if (*buf == 0)
return 1;
- for(cp = str; 0 != *cp; ++cp) {
- if ((*cp >= 0x0 && *cp < 0x20 && *cp != '\t') ||
- *cp == 0x7f || *cp & 0x80)
- *cp = '?';
- }
- return strlen(str);
+ if (player->flags & PF_UTF8)
+ return copy_utf8_to_ascii_no_funny(buf, buf);
+ return copy_ascii_no_funny(buf, buf);
}
+/*
+ * Prompt for a line of non-command, UTF-8 input.
+ * Send C_FLUSH prompt PROMPT to the current player.
+ * Read a line of input into BUF[SIZE], replacing funny characters by
+ * '?'. The result is UTF-8.
+ * This may block for input, yielding the processor. Flush buffered
+ * output when blocking, to make sure player sees the prompt.
+ * Return number of bytes in BUF[], not counting the terminating 0,
+ * or -1 on error.
+ */
int
-uprmptrd(char *prompt, char *str /* str is message text */, int size)
+uprmptrd(char *prompt, char *buf, int size)
{
int r;
- char *cp; /* cp is message text */
+
+ if (CANT_HAPPEN(!prompt))
+ prompt = "? ";
pr_id(player, C_FLUSH, "%s\n", prompt);
- if ((r = recvclient(str, size)) < 0)
+ if ((r = recvclient(buf, size)) < 0)
return r;
time(&player->curup);
- if (*str == 0)
+ if (*buf == 0)
return 1;
-
- for(cp = str; 0 != *cp; ++cp) {
- if ((*cp >= 0x0 && *cp < 0x20 && *cp != '\t') ||
- *cp == 0x7f)
- *cp = '?';
- else if (!(player->flags & PF_UTF8) && (*cp & 0x80))
- *cp = '?';
- }
- return strlen(str);
+ if (player->flags & PF_UTF8)
+ return copy_utf8_no_funny(buf, buf);
+ return copy_ascii_no_funny(buf, buf);
}
+/*
+ * Print the current time in ctime() format.
+ */
void
prdate(void)
{
}
/*
- * print x,y formatting as country
+ * Print coordinates X, Y for COUNTRY.
+ * FORMAT must be a printf-style format string that converts exactly
+ * two int values.
*/
void
-prxy(s_char *format, coord x, coord y, natid country)
+prxy(char *format, coord x, coord y, natid country)
{
- s_char buf[255];
struct natstr *np;
np = getnatp(country);
- sprintf(buf, format, xrel(np, x), yrel(np, y));
- pr(buf);
+ pr(format, xrel(np, x), yrel(np, y));
}
-/*VARARGS*/
+/*
+ * Print to country CN similar to printf().
+ * Use printf-style FORMAT with the optional arguments.
+ * Output is buffered until a newline arrives.
+ * If CN is the current player and we're not in the update, print just
+ * like pr(). Else print into a bulletin.
+ * Because printing like pr() requires normal text, and bulletins
+ * require user text, only plain ASCII is allowed.
+ */
void
-PR(int cn, s_char *format, ...)
+PR(int cn, char *format, ...)
{
/* XXX should really do this on a per-nation basis */
- static s_char longline[MAXNOC][512];
+ static char longline[MAXNOC][512];
int newline;
va_list ap;
- s_char buf[1024];
+ char buf[1024];
va_start(ap, format);
(void)vsprintf(buf, format, ap);
newline = strrchr(buf, '\n') ? 1 : 0;
strcat(longline[cn], buf);
if (newline) {
- if (update_pending || (cn && cn != player->cnum))
+ if (update_running || (cn && cn != player->cnum))
typed_wu(0, cn, longline[cn], TEL_BULLETIN);
else
pr_player(player, C_DATA, longline[cn]);
}
}
+/*
+ * Print the current time in ctime() format to country CN.
+ * If CN is the current player and we're not in the update, print just
+ * like prdate(). Else print into a bulletin.
+ */
void
PRdate(natid cn)
{
PR(cn, ctime(&now));
}
+/*
+ * Sound the current player's bell.
+ */
void
pr_beep(void)
{
pr("\07");
}
+/*
+ * Print to country CN similar to printf().
+ * Use printf-style FORMAT with the optional arguments.
+ * If CN is zero, don't print anything.
+ * Else, if CN is the current player and we're not in the update,
+ * print just like pr(). Else print into a bulletin.
+ * Because printing like pr() requires normal text, and bulletins
+ * require user text, only plain ASCII is allowed.
+ */
void
-mpr(int cn, s_char *format, ...)
+mpr(int cn, char *format, ...)
{
- s_char buf[4096];
+ char buf[4096];
va_list ap;
+ if (!cn)
+ return;
va_start(ap, format);
(void)vsprintf(buf, format, ap);
va_end(ap);
- if (cn) {
- if (update_pending || cn != player->cnum)
- typed_wu(0, cn, buf, TEL_BULLETIN);
+ if (update_running || cn != player->cnum)
+ typed_wu(0, cn, buf, TEL_BULLETIN);
+ else
+ pr_player(player, C_DATA, buf);
+}
+
+/*
+ * Copy SRC without funny characters to DST.
+ * Drop control characters, except for '\t'.
+ * Replace non-ASCII characters by '?'.
+ * Return length of DST.
+ * DST must have space. If it overlaps SRC, then DST <= SRC must
+ * hold.
+ */
+size_t
+copy_ascii_no_funny(char *dst, char *src)
+{
+ char *p;
+ unsigned char ch;
+
+ p = dst;
+ while ((ch = *src++)) {
+ if ((ch < 0x20 && ch != '\t' && ch != '\n') || ch == 0x7f)
+ ; /* ignore funny control */
+ else if (ch > 0x7f)
+ *p++ = '?'; /* replace non-ASCII */
else
- pr_player(player, C_DATA, buf);
+ *p++ = ch;
}
+ *p = 0;
+
+ return p - dst;
}
-void
-prtoascii(char *buf /* buf is message text */)
+/*
+ * Copy UTF-8 SRC without funny characters to DST.
+ * Drop control characters, except for '\t'.
+ * FIXME Replace malformed UTF-8 sequences by '?'.
+ * Return byte length of DST.
+ * DST must have space. If it overlaps SRC, then DST <= SRC must
+ * hold.
+ */
+size_t
+copy_utf8_no_funny(char *dst, char *src)
{
- char *pbuf; /* pbuf is message text */
-
- for(pbuf = buf; *pbuf != 0; pbuf++)
- if ((*pbuf & 0xc0) == 0xc0)
- *pbuf = '?';
- else if (*pbuf & 0x80) {
- memmove(pbuf,pbuf+1,strlen(pbuf)-1);
- pbuf--;
- }
+ char *p;
+ unsigned char ch;
+
+ p = dst;
+ while ((ch = *src++)) {
+ /* FIXME do the right thing for malformed and overlong sequences */
+ if ((ch < 0x20 && ch != '\t' && ch != '\n') || ch == 0x7f)
+ ; /* ignore funny control */
+ else
+ *p++ = ch;
+ }
+ *p = 0;
+
+ return p - dst;
+}
+
+/*
+ * Copy UTF-8 SRC without funny characters to ASCII DST.
+ * Drop control characters, except for '\t'.
+ * Replace non-ASCII characters by '?'.
+ * Return length of DST.
+ * DST must have space. If it overlaps SRC, then DST <= SRC must
+ * hold.
+ */
+size_t
+copy_utf8_to_ascii_no_funny(char *dst, char *src)
+{
+ char *p;
+ unsigned char ch;
+
+ p = dst;
+ while ((ch = *src++)) {
+ /* FIXME do the right thing for malformed and overlong sequences */
+ if ((ch < 0x20 && ch != '\t' && ch != '\n') || ch == 0x7f)
+ ; /* ignore funny control */
+ else if (ch > 0x7f) {
+ *p++ = '?'; /* replace non-ASCII */
+ while ((*src++ & 0xc0) == 0x80) ;
+ } else
+ *p++ = ch;
+ }
+ *p = 0;
+
+ return p - dst;
+}
+
+/*
+ * Return byte-index of the N-th UTF-8 character in UTF-8 string S.
+ * If S doesn't have that many characters, return its length instead.
+ */
+int
+ufindpfx(char *s, int n)
+{
+ int i = 0;
+
+ while (n && s[i])
+ {
+ if ((s[i++] & 0xc0) == 0xc0)
+ while ((s[i] & 0xc0) == 0x80)
+ i++;
+ --n;
+ }
+ return i;
}