/*
* Empire - A multi-player, client/server Internet based war game.
* Copyright (C) 1986-2021, Dave Pare, Jeff Bailey, Thomas Ruschak,
* Ken Stevens, Steve McClure, Markus Armbruster
*
* Empire is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* ---
*
* 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: Output to players
*
* Known contributors to this file:
* Dave Pare, 1986, 1989
* Steve McClure, 1998-2000
* Ron Koenderink, 2005
* Markus Armbruster, 2005-2012
*/
/*
* 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
#include
#include
#include "empio.h"
#include "journal.h"
#include "misc.h"
#include "nat.h"
#include "player.h"
#include "proto.h"
#include "prototypes.h"
#include "update.h"
#include "xy.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);
static void player_output_some(void);
/*
* 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, ...)
{
char buf[4096];
va_list ap;
va_start(ap, 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)
{
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);
}
/*
* 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, char *format, ...)
{
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);
(void)vsprintf(buf, format, ap);
va_end(ap);
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, ...)
{
char buf[4096]; /* UTF-8 */
va_list ap;
if (pl->state != PS_PLAYING)
return;
va_start(ap, format);
(void)vsprintf(buf, format, ap);
va_end(ap);
if (!(pl->flags & PF_UTF8))
copy_utf8_to_ascii_no_funny(buf, buf);
pr_player(pl, C_FLASH, buf);
io_output(pl->iop, 0);
}
/*
* 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, char *format, ...)
{
char buf[4096];
va_list ap;
if (pl->state != PS_PLAYING)
return;
va_start(ap, format);
(void)vsprintf(buf, format, ap);
va_end(ap);
pr_player(pl, C_INFORM, buf);
io_output(pl->iop, 0);
}
/*
* 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(char *format, ...)
{
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 + n, format, ap);
va_end(ap);
for (p = player_next(NULL); p; p = player_next(p)) {
if (p->state != PS_PLAYING)
continue;
pr_player(p, C_FLASH, buf);
io_output(p->iop, 0);
}
}
/*
* 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)
{
char *p;
char *bp;
int len;
journal_output(pl, id, buf);
bp = buf;
while (*bp != '\0') {
if (pl->curid != -1 && pl->curid != id) {
io_puts(pl->iop, "\n");
pl->curid = -1;
}
if (pl->curid == -1)
outid(pl, id);
p = strchr(bp, '\n');
if (p != NULL) {
len = (p - bp) + 1;
io_write(pl->iop, bp, len);
bp += len;
pl->curid = -1;
} else {
len = io_puts(pl->iop, bp);
bp += len;
}
}
if (player == pl)
player_output_some();
}
/*
* 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)
{
char *bp;
int standout = 0;
char printbuf[2];
char ch;
journal_output(pl, id, buf);
printbuf[0] = '\0';
printbuf[1] = '\0';
bp = buf;
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 (ch & 0x80) {
if (standout == 0) {
printbuf[0] = 0x0e;
io_puts(pl->iop, printbuf);
standout = 1;
}
ch &= 0x7f;
} else {
if (standout == 1) {
printbuf[0] = 0x0f;
io_puts(pl->iop, printbuf);
standout = 0;
}
}
if (ch == '\n') {
io_write(pl->iop, &ch, 1);
pl->curid = -1;
} else {
printbuf[0] = ch;
io_puts(pl->iop, printbuf);
}
}
if (player == pl)
player_output_some();
}
/*
* Send ID @n to @pl.
* This runs always at the beginning of a line.
*/
static void
outid(struct player *pl, int n)
{
char buf[3];
if (CANT_HAPPEN(n > C_LAST))
n = C_DATA;
if (n >= 10)
buf[0] = 'a' - 10 + n;
else
buf[0] = '0' + n;
buf[1] = ' ';
buf[2] = '\0';
io_puts(pl->iop, buf);
pl->curid = n;
}
static void
player_output_some(void)
{
time_t deadline = player_io_deadline(player, 1);
while (io_output_if_queue_long(player->iop, deadline) > 0)
;
}
/*
* 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(char *redir)
{
pr_id(player, *redir == '>' ? C_REDIR : C_PIPE, "%s\n", redir);
}
/*
* Send script execute request @file to the current player.
* @file is UTF-8, but non-ASCII characters can occur only if the
* player sent them. Therefore, it is also user text.
*/
void
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);
}
/*
* 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 *buf, int size)
{
int r;
if (CANT_HAPPEN(!prompt))
prompt = "? ";
pr_id(player, C_FLUSH, "%s\n", prompt);
if ((r = recvclient(buf, size)) < 0)
return r;
time(&player->curup);
if (*buf == 0)
return 1;
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 *buf, int size)
{
int r;
if (CANT_HAPPEN(!prompt))
prompt = "? ";
pr_id(player, C_FLUSH, "%s\n", prompt);
if ((r = recvclient(buf, size)) < 0)
return r;
time(&player->curup);
if (*buf == 0)
return 1;
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)
{
time_t now;
(void)time(&now);
pr("%s", ctime(&now));
}
/*
* Print coordinates @x,@y.
* @format must be a printf-style format string that converts exactly
* two int values.
*/
void
prxy(char *format, coord x, coord y)
{
struct natstr *np;
np = getnatp(player->cnum);
pr(format, xrel(np, x), yrel(np, y));
}
/*
* Sound the current player's bell.
*/
void
pr_beep(void)
{
struct natstr *np = getnatp(player->cnum);
if (np->nat_flags & NF_BEEP)
pr("\07");
}
/*
* Print complete lines to country @cn similar to printf().
* Use printf-style @format with the optional arguments. @format must
* end with '\n'.
* 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, char *format, ...)
{
char buf[4096];
va_list ap;
CANT_HAPPEN(!format[0] || format[strlen(format) - 1] != '\n');
if (!cn)
return;
va_start(ap, format);
(void)vsprintf(buf, format, ap);
va_end(ap);
if (update_running || cn != player->cnum)
wu(0, cn, "%s", buf);
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
*p++ = ch;
}
*p = 0;
return p - dst;
}
/*
* 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 *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)
src++;
} 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;
}