code duplication
[netris.git] / curses.c
1 /*
2  * Netris -- A free networked version of T*tris
3  * Copyright (C) 1994-1996,1999  Mark H. Weaver <mhw@netris.org>
4  * 
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  * 
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18  */
19
20 #include "netris.h"
21
22 #include <sys/types.h>
23 #include <unistd.h>
24 #include <curses.h>
25 #include <string.h>
26 #include <stdlib.h>
27
28 #include "client.h"
29 #include "curses.h"
30 #include "util.h"
31 #include "board.h"
32 #include "msg.h"
33
34 #ifdef NCURSES_VERSION
35 # define HAVE_NCURSES
36 #endif
37
38 window_t window[MAX_SCREENS];
39
40 static MyEventType KeyGenFunc(EventGenRec *gen, MyEvent *event);
41 static EventGenRec keyGen = {
42         NULL, 0, FT_read, STDIN_FILENO, KeyGenFunc, EM_key
43 };
44
45 static int statusYPos, statusXPos;
46 static int messageYPos, messageXPos, messageHeight, messageWidth;
47 WINDOW *msgwin;
48 static int haveColor;
49
50 #define MSG_HEIGHT 64  //max history
51 static char *message[MSG_HEIGHT];
52 static char messages[MSG_HEIGHT][MSG_WIDTH];
53
54 static char *term_vi;  /* String to make cursor invisible */
55 static char *term_ve;  /* String to make cursor visible */
56
57 void InitScreens(void)
58 {
59         MySigSet oldMask;
60
61         GetTermcapInfo();
62
63         /*
64          * Block signals while initializing curses.  Otherwise a badly timed
65          * Ctrl-C during initialization might leave the terminal in a bad state.
66          */
67         BlockSignals(&oldMask, SIGINT, 0);
68         initscr();  //start curses
69
70 #ifdef CURSES_HACK
71         {
72                 extern char *CS;
73
74                 CS = 0;
75         }
76 #endif
77
78 #ifdef HAVE_NCURSES
79         haveColor = Sets.color && has_colors();
80         if (haveColor) {
81                 static struct {
82                         char type;
83                         short color;
84                 } myColorTable[] = {
85                         { BT_T, COLOR_WHITE },
86                         { BT_I, COLOR_BLUE },
87                         { BT_O, COLOR_MAGENTA },
88                         { BT_L, COLOR_CYAN },
89                         { BT_J, COLOR_YELLOW },
90                         { BT_S, COLOR_GREEN },
91                         { BT_Z, COLOR_RED },
92                         { BT_none, 0 }
93                 }; //myColorTable
94                 int i = 0;
95
96                 start_color();
97                 if (can_change_color()) {
98                         init_color (COLOR_YELLOW, 1000, 1000, 0);
99                 } //I've never worked on a color-changable terminal, so no idea..
100                 for (i = 0; myColorTable[i].type != BT_none; ++i)
101                         init_pair(myColorTable[i].type, COLOR_BLACK,
102                                 myColorTable[i].color);
103         } //haveColor
104 #else
105         haveColor = 0;
106 #endif
107
108         AtExit(CleanupScreens);        //restore everything when done
109         RestoreSignals(NULL, &oldMask);
110
111         cbreak();                      //no line buffering
112         noecho();
113 //      keypad(stdscr, TRUE);          //get arrow/functionkeys 'n stuff
114         OutputTermStr(term_vi, 0);
115         AddEventGen(&keyGen);          //key handler
116         signal(SIGWINCH, CatchWinCh);  //handle window resize
117 //  ioctl(STDIN_FILENO, KDSKBMODE, K_MEDIUMRAW);
118         standend();                    //normal text
119
120         memset(messages, 0, sizeof(messages)); //empty messages
121         {
122                 int i;
123                 for (i = 0; i<MSG_HEIGHT; i++)
124                         message[i] = messages[i];  //set pointers
125         }
126 }
127
128 void CleanupScreens(void)
129 {
130         RemoveEventGen(&keyGen);
131         endwin();                      //end curses
132         OutputTermStr(term_ve, 1);
133 }
134
135 static void GetTermcapInfo(void)
136 {
137         char *term, *buf, *data;
138         int bufSize = 8192;
139         char scratch[1024];
140
141         if (!(term = getenv("TERM")))
142                 return;
143         if (tgetent(scratch, term) == 1) {
144                 /*
145                  * Make the buffer HUGE, since tgetstr is unsafe.
146                  * Allocate it on the heap too.
147                  */
148                 data = buf = malloc(bufSize);
149
150                 /*
151                  * There is no standard include file for tgetstr, no prototype
152                  * definitions.  I like casting better than using my own prototypes
153                  * because if I guess the prototype, I might be wrong, especially
154                  * with regards to "const".
155                  */
156                 term_vi = (char *)tgetstr("vi", &data);
157                 term_ve = (char *)tgetstr("ve", &data);
158
159                 /* Okay, so I'm paranoid; I just don't like unsafe routines */
160                 if (data > buf + bufSize)
161                         fatal("tgetstr overflow, you must have a very sick termcap");
162
163                 /* Trim off the unused portion of buffer */
164                 buf = realloc(buf, data - buf);
165         }
166
167         /*
168          * If that fails, use hardcoded vt220 codes.
169          * They don't seem to do anything bad on vt100's, so
170          * we'll try them just in case they work.
171          */
172         if (!term_vi || !term_ve) {
173                 static char *vts[] = {
174                         "vt100", "vt101", "vt102",
175                         "vt200", "vt220", "vt300",
176                         "vt320", "vt400", "vt420",
177                         "screen", "xterm", NULL
178                 };
179                 int i;
180
181                 for (i = 0; vts[i]; i++)
182                         if (!strcmp(term, vts[i])) {
183                                 term_vi = "\033[?25l";
184                                 term_ve = "\033[?25h";
185                                 break;
186                         }
187         }
188         if (!term_vi || !term_ve)
189                 term_vi = term_ve = NULL;
190 }
191
192 static void OutputTermStr(char *str, int flush)
193 {
194         if (str) {
195                 fputs(str, stdout);
196                 if (flush) fflush(stdout);
197         }
198 }
199
200 static void DrawTitle(void)
201 {
202         int rows, cols;
203         char *s;
204
205 #ifdef HAVE_NCURSES
206         attrset(A_REVERSE);
207 #else
208         standout();
209 #endif
210         getmaxyx(stdscr, rows, cols);
211         s = malloc(cols + 1);
212         sprintf(s, " " MSG_TITLE " %s", version_string);
213         const int titlelen = strlen(s);
214         memset(&s[titlelen], ' ', cols - titlelen); // pad
215         if (cols > titlelen + 1 + strlen(MSG_TITLESUB))
216                 memcpy(&s[cols - 1 - strlen(MSG_TITLESUB)], MSG_TITLESUB, sizeof(MSG_TITLESUB) - 1);
217         memcpy(&s[cols], "\0", 1);
218         mvaddstr(0, 0, s);
219         free(s);
220         standend();     //normal text
221 }
222
223 static void window_border(int x1, int y1, int x2, int y2)
224 { //draw grid
225         int y, x;
226
227         for (y = y1 + 1; y < y2; y++) {
228                 mvaddch(y, x1, Sets.ascii ? '|' : ACS_VLINE); //left
229                 mvaddch(y, x2, Sets.ascii ? '|' : ACS_VLINE); //right
230         } //draw vertical lines
231         move(y1, x1); //top
232         addch(Sets.ascii ? '+' : ACS_ULCORNER);
233         for (x = x1 + 1; x < x2; x++)
234                 addch(Sets.ascii ? '-' : ACS_HLINE);
235         addch(Sets.ascii ? '+' : ACS_URCORNER);
236         move(y2, x1); //bottom
237         addch(Sets.ascii ? '+' : ACS_LLCORNER);
238         for (x = x1 + 1; x < x2; x++)
239                 addch(Sets.ascii ? '-' : ACS_HLINE);
240         addch(Sets.ascii ? '+' : ACS_LRCORNER);
241 }
242
243 void window_draw(int player)
244 { //draw field for player
245         if (!window[player].shown) return; 
246         window_border(window[player].posx - 1, window[player].posy - Players[player].boardVisible,
247                 window[player].posx + window[player].size * Players[player].boardWidth, window[player].posy + 1);
248         {
249                 char s[window[player].size * Players[player].boardWidth + 1];
250
251                 if (Players[player].host && Players[player].host[0])
252                         snprintf(s, sizeof(s), " %s <%s> ",
253                                 Players[player].name, Players[player].host);
254                 else snprintf(s, sizeof(s), " %s ", Players[player].name);
255                 s[sizeof(s)] = 0;
256                 if (haveColor && Players[player].team > 0 && Players[player].team <= 7)
257                         attrset(A_REVERSE | COLOR_PAIR(Players[player].team + 1));
258                 mvaddstr(1, window[player].posx, s);
259                 if (haveColor) standend();
260         } //display playername/host
261
262         {
263                 int x, y;
264                 for (y = 0; y <= Players[player].boardVisible; y++)
265                         for (x = 0; x <= Players[player].boardWidth; x++)
266                                 block_draw_window(player, y, x, block_get(player, y, x));
267         } //draw field
268
269         window_msg_status(player);
270 }
271
272 void screen_setup(void)
273 { //calculate positions of all fields
274         int i, prev;
275         int y, x;
276         int spaceavail;
277
278         clear();
279         DrawTitle();
280         getmaxyx(stdscr, y, x);
281         window[me].size = 2;
282         window[me].posx = 1;
283         window[me].posy = 21;
284         window[me].shown = 1;
285         statusXPos = window[me].size * Players[me].boardWidth + 3;
286         statusYPos = 21;
287         status_draw(me, Players[me].score);
288
289         messageXPos = 2;
290         messageYPos = 24;
291         messageWidth  = MIN(x - messageXPos - 2, MSG_WIDTH);
292         messageHeight = MIN(y - messageYPos - 1, MSG_HEIGHT);
293         if (messageHeight < 3) {
294                 messageWidth = MIN(x - statusXPos - 18, 27);
295                 messageHeight = y - 3;
296                 messageXPos = statusXPos + 16;
297                 messageYPos = 2;
298         } //messagebox doesn't fit below
299         window_border(messageXPos - 2, messageYPos - 1,
300                 messageXPos + messageWidth + 1, messageYPos+messageHeight);
301         if (msgwin = subwin(stdscr, messageHeight, messageWidth,
302                             messageYPos, messageXPos))
303                 scrollok(msgwin, 1);  //allow scrolling
304         wmove(msgwin, messageHeight - 2, 0);
305         for (i = messageHeight - 2; i >= 0; i--) //display message history
306                 msg_draw(message[i]);
307
308         spaceavail = x;
309         for (i = 1; i <= maxPlayer; i++)
310                 spaceavail -= Players[i].boardWidth+2;
311         prev = me;
312         for (i = 1; i < MAX_SCREENS; i++) if (i != me) {
313                 window[i].posy = 21;
314                 window[i].posx =
315                         window[prev].posx + 2 + window[prev].size * Players[prev].boardWidth;
316                 if (prev == me) {
317                         window[i].posx += 15; //scorebar
318                         if (messageYPos < 24)
319                                 window[i].posx += messageWidth + 4; //messagebox
320                         spaceavail -= window[i].posx - 3;
321                 } //stuff before second player
322                 if (spaceavail >= 0) {
323                         window[i].size = 2;
324                         spaceavail -= Players[i].boardWidth;
325                 } //not enough space, half width
326                 else
327                         window[i].size = 1;
328                 if (x < window[i].posx + 1 + window[i].size * Players[i].boardWidth)
329                         window[i].shown = 0; //field doesn't fit on screen
330                 else
331                         window[i].shown = 1;
332                 prev = i;
333         }
334         for (i = 1; i <= maxPlayer; i++)
335                 window_draw(i);
336 }
337
338 static void msg_draw(char *p)
339 {
340         char s[MSG_WIDTH];
341         char *psearch;
342         char c;
343
344         memcpy(s, p, sizeof(s)-1);
345         s[MSG_WIDTH-1] = 0;
346         p = s;
347         while (psearch = strchr(p, '\\')) {
348                 *psearch = '\0';
349                 waddstr(msgwin, p);
350                 c = atoi(++psearch) + 1;
351                 if (haveColor) wattrset(msgwin, A_REVERSE | COLOR_PAIR(c));
352                 p = ++psearch;
353         } //search for color escapes (\)
354         waddstr(msgwin, p);
355         if (haveColor) wstandend(msgwin);
356         waddch(msgwin, '\n');
357 }
358
359 void msg_add(char *fmt, ...)
360 { //print game/bot message
361         va_list args;
362         char s[MSG_WIDTH];
363         char *p;
364         int i;
365
366         if (!messageHeight) return;
367         va_start(args, fmt);
368         vsnprintf(s, sizeof(s), fmt, args);
369         va_end(args);
370         p = message[MSG_HEIGHT - 1]; //save last pointer
371         for (i = MSG_HEIGHT - 1; i > 0; i--)
372                 message[i] = message[i - 1]; //scroll history
373         message[0] = p;
374         strcpy(p, s);
375
376         wmove(msgwin, messageHeight - 1, 0);
377         msg_draw(s);
378         wclrtoeol(msgwin);
379         wrefresh(msgwin);
380 }
381
382 void msg_add_char(char c, int x, char *s)
383 { //show single typed character
384         if (c == 27) {
385                 mvwaddch(msgwin, messageHeight-1, (x+1) % (messageWidth-1), ' ');
386         } //escape
387         else {
388                 if (c == 13 || c == 127) //enter/backspace
389                         mvwaddch(msgwin, messageHeight - 1, (x+2) % (messageWidth-1),
390                                 x >= messageWidth-3 ? s[x - messageWidth + 3] : ' ');
391                 else //any character
392                         mvwaddch(msgwin, messageHeight - 1, x % (messageWidth-1), c);
393                 mvwaddch(msgwin, messageHeight - 1, (x+1) % (messageWidth-1), '_');
394         } //typing mode
395         wrefresh(msgwin);
396 }
397
398 static void block_draw_2(int y, int x, unsigned char type)
399 { //display block on screen
400         move(y, x);
401         if (type == BT_none) addstr("  ");
402         else if (type == BT_shadow) addstr("::");
403         else {
404 #ifdef HAVE_NCURSES
405                 if (Sets.standout) {
406                         attrset(haveColor ? COLOR_PAIR(type & 15) : A_REVERSE);
407                 }
408 #endif
409                 switch (Sets.drawstyle) {
410                 case 2: // ascii horizontally grouped
411                         switch (type & 0xC0) {
412                         case 0x40:  // right neighbour
413                                 addstr("[["); break;
414                         case 0x80: // left
415                                 addstr("]]"); break;
416                         default:  // both/none
417                                 addstr("[]"); break;
418                         } // horizontal stickiness
419                         break;
420                 case 3: // curses grouped
421                         switch (type & 0xF0) {
422                         case 0x10: addch(ACS_ULCORNER); addch(ACS_URCORNER); break; // top end
423                         case 0x20: addch(ACS_LLCORNER); addch(ACS_LRCORNER); break; // bottom end
424                         case 0x30: addch(ACS_VLINE);    addch(ACS_VLINE);    break; // vertical middle
425                         case 0x40: addch('[');          addch(ACS_HLINE);    break; // left end
426                         case 0x50: addch(ACS_ULCORNER); addch(ACS_TTEE);     break; // top left corner
427                         case 0x60: addch(ACS_LLCORNER); addch(ACS_BTEE);     break; // bottom left corner
428                         case 0x70: addch(ACS_LTEE);     addch(ACS_PLUS);     break; // vertical+right
429                         case 0x80: addch(ACS_HLINE);    addch(']');          break; // right end
430                         case 0x90: addch(ACS_TTEE);     addch(ACS_URCORNER); break; // top right corner
431                         case 0xA0: addch(ACS_BTEE);     addch(ACS_LRCORNER); break; // bottom right corner
432                         case 0xB0: addch(ACS_PLUS);     addch(ACS_RTEE);     break; // vertical+left
433                         case 0xC0: addch(ACS_HLINE);    addch(ACS_HLINE);    break; // horizontal middle
434                         case 0xD0: addch(ACS_TTEE);     addch(ACS_TTEE);     break; // horizontal+down
435                         case 0xE0: addch(ACS_BTEE);     addch(ACS_BTEE);     break; // horizontal+up
436                         default:   addstr("[]");                             break; // all/none
437                         } // neighbours
438                         break;
439                 default: // non-grouped
440                         addstr("[]");
441                         break;
442                 }
443 #ifdef HAVE_NCURSES
444                 if (Sets.standout) standend();
445 #endif
446         } //display one brick
447 }
448
449 static void block_draw_1(int y, int x, unsigned char type)
450 { //display block small
451         move(y, x);
452         if (type == BT_none) addch(' ');
453         else if (type == BT_shadow) addch(':');
454         else {
455 #ifdef HAVE_NCURSES
456                 if (Sets.standout) {
457                         attrset(haveColor ? COLOR_PAIR(type & 15) : A_REVERSE);
458                 }
459 #endif
460                 if ((type & 0xC0) == 0x40)
461                         addch('[');
462                 else if ((type & 0xC0) == 0x80)
463                         addch(']');
464                 else
465                         addch('|');
466 #ifdef HAVE_NCURSES
467                 if (Sets.standout) standend();
468 #endif
469         } //display one brick
470 }
471
472 void block_draw_window(int player, int y, int x, unsigned char type)
473 {
474         if (y >= 0 && y < Players[player].boardVisible
475          && x >= 0 && x < Players[player].boardWidth) {
476                 if (window[player].size > 1)
477                         block_draw_2(window[player].posy - y, window[player].posx + 2*x, type);
478                 else
479                         block_draw_1(window[player].posy - y, window[player].posx + x, type);
480         } //on screen
481 }
482 void block_draw_status(int y, int x, unsigned char type)
483 { //Draw block at specified position next to field
484         block_draw_2(20 - y, 2 * x, type);
485 }
486
487 void status_draw(int player, struct score_t score)
488 { //show score stuff
489         float timer;
490
491         mvaddstr(13, statusXPos, MSG_NEXT " ");
492         mvaddstr(14, statusXPos + 5,  "        ");
493         shape_iterate(Players[player].nextShape, player, 8,
494                 statusXPos/2 + (Players[player].nextShape/4 == 5 ? 3 : 4),
495                 block_iter_set_status); //draw; BT_I one more to the left
496         mvprintw(3, statusXPos, MSG_LEVEL, score.level);
497         mvprintw(5, statusXPos, MSG_SCORE, score.score);
498         mvprintw(6, statusXPos, MSG_LINES, score.lines);
499         timer = CurTimeval() / 1e6;
500         if (timer > 4) {
501                 mvprintw(9, statusXPos, MSG_PPM, score.pieces * 60 / timer);
502                 if (score.lines > 0) {
503                         mvprintw(7, statusXPos, MSG_YIELD, 100 * score.adds / score.lines);
504                         mvprintw(10, statusXPos, MSG_APM, score.adds * 60 / timer);
505                 }
506         } //display [ap]pm
507         else {
508                 int i;
509                 for (i = 7; i <= 10; i++)
510                         mvaddstr(i, statusXPos, "             ");
511         } //too early to display stats, remove old..
512 }
513
514 void window_msg(int player, char *message)
515 { //put a message over player's field
516         if (!window[player].shown) return;
517         if (message) {
518                 const int fieldsize = Players[player].boardWidth * window[player].size;
519                 const int centered = (fieldsize - strlen(message)) / 2;
520                 char s[fieldsize + 1];
521
522                 memset(s, ' ', fieldsize);
523                 memcpy(&s[centered], message, strlen(message));
524                 s[fieldsize] = 0;
525 #ifdef HAVE_NCURSES
526                 attrset(A_REVERSE);
527 #else
528                 standout();
529 #endif
530                 mvprintw(window[player].posy - Players[player].boardVisible / 2,
531                         window[player].posx, "%s", s);
532                 standend();
533         } //display
534         else {
535                 int x, y;
536                 y = Players[player].boardVisible / 2;
537                 for (x = 0; x <= Players[player].boardWidth; x++)
538                         block_draw_window(player, y, x, block_get(player, y, x));
539         } //restore field
540 }
541
542 void window_msg_wide(int player, char *message)
543 {
544         int i;
545         char *messagewide = malloc(strlen(message) * 2); // max += strlen - 1
546         const int fieldsize = Players[player].boardWidth * window[player].size;
547
548         const bool sep = strchr(message, ' ') != NULL;
549                 // whitespace to pad at convenience
550         const bool pad = strlen(message) * 2 - sep <= fieldsize;
551                 // (space to) put whitespace between all characters
552         bool odd = fieldsize & 1;
553                 // odd number of characters (center off; try to change padding at sep)
554         if (!pad) odd ^= strlen(message) & 1;
555                 // for odd strings, check for even fieldsize instead
556
557         if (pad || (sep && odd && strlen(message) < fieldsize)) {
558                 // generate padded message in messagewide
559                 for (i = 0; ; message++) {
560                         messagewide[i++] = *message;
561                         if (message[1] == 0) {
562                                 messagewide[i] = 0;
563                                 break;
564                         }
565                         if (pad ? (*message != ' ' || odd) : (*message == ' ' && odd)) {
566                                 // add padding if wide; different padding at space if odd
567                                 messagewide[i++] = ' ';
568                                 odd = 0;
569                         }
570                 }
571                 message = messagewide;
572         }
573         window_msg(player, message);
574 }
575
576 void window_msg_status(int player)
577 { //put status (pause, readiness, game over) over player's field
578         if (Players[player].alive > 0)
579                 if (Players[player].flags & SCF_paused)
580                         if (Game.started > 1)
581                                 window_msg_wide(player, MSG_PLAYER_PAUSE);
582                         else
583                                 window_msg_wide(player, MSG_PLAYER_JOIN);
584                 else
585                         if (Game.started > 1)
586                                 window_msg(player, NULL);
587                         else
588                                 window_msg_wide(player, MSG_PLAYER_START);
589         else if (!Players[player].alive)
590                 window_msg_wide(player, MSG_PLAYER_STOP);
591         else
592                 window_msg_wide(player, MSG_PLAYER_PART);
593 }
594
595
596 void status_tick(void)
597 { //display timer
598         mvprintw(statusYPos, statusXPos, MSG_TIME, CurTimeval() / 1e6);
599 }
600
601 void ScheduleFullRedraw(void)
602 {
603         touchwin(stdscr);
604 }
605
606 static void CatchWinCh(int sig)
607 { //handle window resize
608         endwin();      //exit curses
609         refresh();     //and reinit display (with different sizes)
610         screen_setup();//manually redraw everything
611         refresh();     //refresh
612 }
613
614 static MyEventType KeyGenFunc(EventGenRec *gen, MyEvent *event)
615 { //read keypresses
616         if (MyRead(gen->fd, &event->u.key, 1))
617                 return E_key;
618         else
619                 return E_none;
620 }
621