0e841ef2066bf2e9e4079cdce1adde389aae8df3
[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                         if (haveColor) attrset(COLOR_PAIR(type & 15));
407                         else attrset(A_REVERSE);
408                 }
409 #endif
410                 switch (Sets.drawstyle) {
411                 case 2:
412                         switch (type & 192) {
413                         case 64:  //right neighbour
414                                 addstr("[["); break;
415                         case 128: //left
416                                 addstr("]]"); break;
417                         default:  //both/none
418                                 addstr("[]"); break;
419                         } //horizontal stickiness
420                         break; //ascii horizontally grouped
421                 case 3:
422                         switch (type & 240) {
423                         case 48:
424                                 addstr("||"); break; //middle
425                         case 64: case 80: case 96:
426                                 addstr("[="); break; //left end
427                         case 112:
428                                 addstr("|="); break;
429                         case 128: case 144: case 160:
430                                 addstr("=]"); break; //right end
431                         case 176:
432                                 addstr("=|"); break;
433                         case 192: case 208: case 224:
434                                 addstr("=="); break;
435                         default:
436                                 addstr("[]"); break; //top/bottom/mid
437                         } //neighbours
438                         break; //ascii semi-grouped
439                 case 7:
440                         switch (type & 240) {
441                         case  16: addch(ACS_ULCORNER); addch(ACS_URCORNER); break;//top end
442                         case  32: addch(ACS_LLCORNER); addch(ACS_LRCORNER); break;//bottom end
443                         case  48: addch(ACS_VLINE); addch(ACS_VLINE); break;    //vertical middle
444                         case  64: addch('['); addch(ACS_HLINE); break;          //left end
445                         case  80: addch(ACS_ULCORNER); addch(ACS_TTEE); break;  //top left corner
446                         case  96: addch(ACS_LLCORNER); addch(ACS_BTEE); break;  //bottom left corner
447                         case 112: addch(ACS_LTEE); addch(ACS_PLUS); break;      //vertical+right
448                         case 128: addch(ACS_HLINE); addch(']'); break;          //right end
449                         case 144: addch(ACS_TTEE); addch(ACS_URCORNER); break;  //top right corner
450                         case 160: addch(ACS_BTEE); addch(ACS_LRCORNER); break;  //bottom right corner
451                         case 176: addch(ACS_PLUS); addch(ACS_RTEE); break;      //vertical+left
452                         case 192: addch(ACS_HLINE); addch(ACS_HLINE); break;    //horizontal middle
453                         case 208: addch(ACS_TTEE); addch(ACS_TTEE); break;      //horizontal+down
454                         case 224: addch(ACS_BTEE); addch(ACS_BTEE); break;      //horizontal+up
455                         default:  addstr("[]"); break;
456                         } //neighbours
457                         break; //curses grouped
458                 default:
459                         addstr("[]");
460                         break; //ascii non-grouped
461                 } //draw block
462 #ifdef HAVE_NCURSES
463                 if (Sets.standout) standend();
464 #endif
465         } //display one brick
466 }
467
468 static void block_draw_1(int y, int x, unsigned char type)
469 { //display block small
470         move(y, x);
471         if (type == BT_none) addch(' ');
472         else if (type == BT_shadow) addch(':');
473         else {
474                 if (Sets.standout) {
475 #ifdef HAVE_NCURSES
476                         if (haveColor)
477                                 attrset(COLOR_PAIR(type & 15));
478                         else attrset(A_REVERSE);
479 #endif
480                 }
481                 if ((type & 192) == 64)
482                         addch('[');
483                 else if ((type & 192) == 128)
484                         addch(']');
485                 else
486                         addch('|');
487 #ifdef HAVE_NCURSES
488                 if (Sets.standout) standend();
489 #endif
490         } //display one brick
491 }
492
493 void block_draw_window(int player, int y, int x, unsigned char type)
494 {
495         if (y >= 0 && y < Players[player].boardVisible
496          && x >= 0 && x < Players[player].boardWidth) {
497                 if (window[player].size > 1)
498                         block_draw_2(window[player].posy - y, window[player].posx + 2*x, type);
499                 else
500                         block_draw_1(window[player].posy - y, window[player].posx + x, type);
501         } //on screen
502 }
503 void block_draw_status(int y, int x, unsigned char type)
504 { //Draw block at specified position next to field
505         block_draw_2(20 - y, 2 * x, type);
506 }
507
508 void status_draw(int player, struct score_t score)
509 { //show score stuff
510         float timer;
511
512         mvaddstr(13, statusXPos, MSG_NEXT " ");
513         mvaddstr(14, statusXPos + 5,  "        ");
514         shape_iterate(Players[player].nextShape, player, 8,
515                 statusXPos/2 + (Players[player].nextShape/4 == 5 ? 3 : 4),
516                 block_iter_set_status); //draw; BT_I one more to the left
517         mvprintw(3, statusXPos, MSG_LEVEL, score.level);
518         mvprintw(5, statusXPos, MSG_SCORE, score.score);
519         mvprintw(6, statusXPos, MSG_LINES, score.lines);
520         timer = CurTimeval() / 1e6;
521         if (timer > 4) {
522                 mvprintw(9, statusXPos, MSG_PPM, score.pieces * 60 / timer);
523                 if (score.lines > 0) {
524                         mvprintw(7, statusXPos, MSG_YIELD, 100 * score.adds / score.lines);
525                         mvprintw(10, statusXPos, MSG_APM, score.adds * 60 / timer);
526                 }
527         } //display [ap]pm
528         else {
529                 int i;
530                 for (i = 7; i <= 10; i++)
531                         mvaddstr(i, statusXPos, "             ");
532         } //too early to display stats, remove old..
533 }
534
535 void window_msg(int player, char *message)
536 { //put a message over player's field
537         if (!window[player].shown) return;
538         if (message) {
539                 char s[MAX_BOARD_WIDTH+1];
540                 memset(s, ' ', MAX_BOARD_WIDTH);
541                 memcpy(&s[(window[player].size * Players[player].boardWidth / 2) - (strlen(message) / 2)],
542                         message, strlen(message));
543                 s[window[player].size * Players[player].boardWidth] = 0;
544 #ifdef HAVE_NCURSES
545                 attrset(A_REVERSE);
546 #else
547                 standout();
548 #endif
549                 mvprintw(window[player].posy - Players[player].boardVisible / 2,
550                         window[player].posx, "%s", s);
551                 standend();
552         } //display
553         else {
554                 int x, y;
555                 y = Players[player].boardVisible / 2;
556                 for (x = 0; x <= Players[player].boardWidth; x++)
557                         block_draw_window(player, y, x, block_get(player, y, x));
558         } //restore field
559 }
560
561 void window_msg_status(int player)
562 { //put status (pause, readiness, game over) over player's field
563         if (Players[player].alive > 0)
564                 if (Players[player].flags & SCF_paused)
565                         if (Game.started > 1)
566                                 window_msg(player, window[player].size > 1 ? "P A U S E D" : "PAUSED");
567                         else
568                                 window_msg(player,
569                                         window[player].size > 1 ? "N O T  R E A D Y" : "NOT  READY");
570                 else
571                         if (Game.started > 1)
572                                 window_msg(player, NULL);
573                         else
574                                 window_msg(player, window[player].size > 1 ? "R E A D Y" : "READY");
575         else if (!Players[player].alive)
576                 window_msg(player,
577                         window[player].size > 1 ? "G A M E  O V E R" : "GAME  OVER");
578         else
579                 window_msg(player, window[player].size > 1 ? "E M P T Y" : "EMPTY");
580 }
581
582
583 void status_tick(void)
584 { //display timer
585         mvprintw(statusYPos, statusXPos, "timer %7.0f ", CurTimeval() / 1e6);
586 }
587
588 void ScheduleFullRedraw(void)
589 {
590         touchwin(stdscr);
591 }
592
593 static void CatchWinCh(int sig)
594 { //handle window resize
595         endwin();      //exit curses
596         refresh();     //and reinit display (with different sizes)
597         screen_setup();//manually redraw everything
598         refresh();     //refresh
599 }
600
601 static MyEventType KeyGenFunc(EventGenRec *gen, MyEvent *event)
602 { //read keypresses
603         if (MyRead(gen->fd, &event->u.key, 1))
604                 return E_key;
605         else
606                 return E_none;
607 }
608