1 /*
2  * src/gmp/engine.c, part of Complete Goban (game program)
3  * Copyright (C) 1995-1997 William Shubert.
4  * See "configure.h.in" for more copyright information.
5  */
6 
7 /*
8  * This is based on David Fotland's go modem protocol code.  I've pretty much
9  *   completely rewritten it though.
10  */
11 
12 #include <wms.h>
13 #include <but/but.h>
14 #include <but/timer.h>
15 #include <wms/str.h>
16 #include "../cgoban.h"
17 #include "../goBoard.h"
18 #include "../msg.h"
19 #ifdef  _GMP_ENGINE_H_
20 #error  Levelization Error.
21 #endif
22 #include "engine.h"
23 
24 
25 /**********************************************************************
26  * Constants
27  **********************************************************************/
28 
29 #define  GMP_NUMQUERIESTOASK  4
30 
31 #define  GMP_TIMEOUTSECS  60
32 
33 
34 /**********************************************************************
35  * Data types
36  **********************************************************************/
37 typedef enum  {
38   cmd_ack, cmd_deny, cmd_reset, cmd_query, cmd_respond, cmd_move,
39   cmd_undo
40 } Command;
41 
42 
43 typedef enum  {
44   query_game, query_bufSize, query_protocol, query_stones,
45   query_bTime, query_wTime, query_charSet, query_rules, query_handicap,
46   query_boardSize, query_timeLimit, query_color, query_who
47 } Query;
48 
49 
50 /**********************************************************************
51  * Globals
52  **********************************************************************/
53 #if  DEBUG
54 static const char *cmdNames[] = {
55   "ACK", "DENY", "RESET", "QUERY", "RESPOND", "MOVE", "UNDO"};
56 
57 static bool  showTransfers = TRUE;
58 #endif
59 
60 
61 /**********************************************************************
62  * Forward Declarations
63  **********************************************************************/
64 static ButOut  dataReadyForRecv(void *packet, int fd);
65 static ButOut  getPacket(GmpEngine *ge);
66 static unsigned char  checksum(unsigned char p[4]);
67 static ButOut  parsePacket(GmpEngine *ge);
68 static ButOut  processCommand(GmpEngine *ge, Command command, int val);
69 static ButOut  gotQueryResponse(GmpEngine *ge, int val);
70 static ButOut  putCommand(GmpEngine *ge, Command cmd, int val);
71 static ButOut  heartbeat(ButTimer *timer);
72 static void  respond(GmpEngine *ge, Query query);
73 static void  askQuery(GmpEngine *ge);
74 static void  processQ(GmpEngine *ge);
75 static char  **makeArgv(const char *cmdLine);
76 
77 
78 /**********************************************************************
79  * Functions
80  **********************************************************************/
81 
gmpEngine_init(Cgoban * cg,GmpEngine * ge,int inFile,int outFile,const GmpActions * actions,void * packet)82 GmpEngine  *gmpEngine_init(Cgoban *cg, GmpEngine *ge,
83 			   int inFile, int outFile,
84 			   const GmpActions *actions, void *packet)  {
85   struct timeval  oneSecond;
86 
87   MAGIC_SET(ge);
88   ge->stopped = FALSE;
89   ge->inFile = inFile;
90   ge->outFile = outFile;
91   ge->boardSize = 0;
92   ge->handicap = 0;
93   ge->komi = 0.0;
94   ge->chineseRules = TRUE;
95   ge->iAmWhite = 0;
96   ge->queriesAcked = 0;
97   ge->lastQuerySent = 0;
98 
99   ge->recvSoFar = 0;
100   ge->sendsQueued = 0;
101   ge->sendFailures = 0;
102   ge->waitingHighAck = FALSE;
103   ge->lastSendTime = 0;
104   ge->myLastSeq = 0;
105   ge->hisLastSeq = 0;
106 
107   ge->cg = cg;
108   ge->env = cg->env;
109   ge->packet = packet;
110   ge->actions = actions;
111 
112   butEnv_addFile(cg->env, BUT_READFILE, inFile, ge, dataReadyForRecv);
113   oneSecond.tv_usec = 0;
114   oneSecond.tv_sec = 1;
115   ge->heartbeat = butTimer_create(ge, NULL, oneSecond, oneSecond,
116 				  FALSE, heartbeat);
117   return(ge);
118 }
119 
120 
gmpEngine_deinit(GmpEngine * ge)121 GmpEngine  *gmpEngine_deinit(GmpEngine *ge)  {
122   assert(MAGIC(ge));
123   butEnv_rmFile(ge->env, BUT_READFILE, ge->inFile);
124   butTimer_destroy(ge->heartbeat);
125   MAGIC_UNSET(ge);
126   return(ge);
127 }
128 
129 
dataReadyForRecv(void * packet,int fd)130 static ButOut  dataReadyForRecv(void *packet, int fd)  {
131   GmpEngine  *ge = packet;
132 
133   assert(MAGIC(ge));
134   assert(fd == ge->inFile);
135   return(getPacket(ge));
136 }
137 
138 
getPacket(GmpEngine * ge)139 static ButOut  getPacket(GmpEngine *ge)  {
140   unsigned char  charsIn[4], c;
141   int  count = 0, cNum;
142   Str  errDesc;
143   ButOut  result = 0;
144 
145   count = read(ge->inFile, charsIn, 4 - ge->recvSoFar);
146   if (ge->stopped)
147     return(0);
148   if (count < 0)  {
149     str_init(&errDesc);
150     str_print(&errDesc, msg_gmpDead,
151 	      strerror(errno));
152     ge->actions->errorRecvd(ge, ge->packet, str_chars(&errDesc));
153     str_deinit(&errDesc);
154     return(BUTOUT_ERR);
155   } else if (count == 0)
156     return(0);
157 
158   for (cNum = 0;  cNum < count;  ++cNum)  {
159     c = charsIn[cNum];
160     if (!ge->recvSoFar)  {
161       /* idle, looking for start of packet */
162       if ((c & 0xfc) == 0)  {  /* start of packet */
163 	ge->recvData[0] = c;
164 	ge->recvSoFar = 1;
165       }
166     } else  {
167       /* We're in the packet. */
168       if ((c & 0x80) == 0)  {  /* error */
169 	ge->recvSoFar = 0;
170 	if ((c & 0xfc) == 0)  {
171 	  ge->recvData[ge->recvSoFar++] = c;
172 	}
173       } else  {
174 	/* A valid character for in a packet. */
175 	ge->recvData[ge->recvSoFar++] = c;
176 	if (ge->recvSoFar == 4)  {  /* check for extra bytes */
177 	  assert(cNum + 1 == count);
178 	  if (checksum(ge->recvData) == ge->recvData[1])
179 	    result = parsePacket(ge);
180 	  ge->recvSoFar = 0;
181 	}
182       }
183     }
184   }
185   return(result);
186 }
187 
188 
checksum(unsigned char p[4])189 static unsigned char  checksum(unsigned char p[4])  {
190   unsigned char sum;
191   sum = p[0] + p[2] + p[3];
192   sum |= 0x80;  /* set sign bit */
193   return(sum);
194 }
195 
196 
parsePacket(GmpEngine * ge)197 static ButOut  parsePacket(GmpEngine *ge)  {
198   int  seq, ack, val;
199   Command  command;
200   ButOut  result = 0;
201 
202   seq = ge->recvData[0] & 1;
203   ack = (ge->recvData[0] & 2) >> 1;
204   if (ge->recvData[2] & 0x08)  /* Not-understood command. */
205     return(0);
206   command = (ge->recvData[2] >> 4) & 7;
207 #if DEBUG
208   if (showTransfers) {
209     fprintf(stderr, "GMP: Command %s received from port %d:%d.\n",
210 	    cmdNames[command], ge->inFile, ge->outFile);
211   }
212 #endif
213   if ((command != cmd_deny) && (command != cmd_reset) &&
214       !ge->iAmWhite && !ge->gameStarted) {
215     ge->gameStarted = TRUE;
216     if (ge->actions->newGame != NULL)
217       ge->actions->newGame(ge, ge->packet, ge->boardSize, ge->handicap,
218 			   ge->komi, ge->chineseRules, ge->iAmWhite);
219   }
220   val = ((ge->recvData[2] & 7) << 7) | (ge->recvData[3] & 0x7f);
221   if (!ge->waitingHighAck)  {
222     if ((command == cmd_ack) ||  /* An ack.  We don't need an ack now. */
223 	(ack != ge->myLastSeq))  {  /* He missed my last message. */
224 #if DEBUG
225       if (showTransfers) {
226 	fprintf(stderr, "GMP: ACK does not match myLastSeq.\n");
227       }
228 #endif
229       return(0);
230     } else if (seq == ge->hisLastSeq)  {  /* Seen this one before. */
231 #if DEBUG
232       if (showTransfers) {
233 	fprintf(stderr, "GMP: Repeated packet.\n");
234       }
235 #endif
236       putCommand(ge, cmd_ack, ~0);
237     } else  {
238       ge->hisLastSeq = seq;
239       ge->sendFailures = 0;
240       result = processCommand(ge, command, val);
241     }
242   } else  {
243     /* Waiting for OK. */
244     if (command == cmd_ack)  {
245       if ((ack != ge->myLastSeq) || (seq != ge->hisLastSeq))  {
246 	/* Sequence error. */
247 	return(result);
248       }
249       ge->sendFailures = 0;
250       ge->waitingHighAck = FALSE;
251       processQ(ge);
252     } else if (seq == ge->hisLastSeq)  {
253       /* His command is old. */
254     } else if (ack == ge->myLastSeq)  {
255       ge->sendFailures = 0;
256       ge->waitingHighAck = FALSE;
257       ge->hisLastSeq = seq;
258       result = processCommand(ge, command, val);
259       processQ(ge);
260     } else  {
261       /* Conflict with opponent. */
262 #if DEBUG
263       if (showTransfers) {
264 	fprintf(stderr, "GMP: Received a packet, expected an ACK.\n");
265       }
266 #endif
267       /*
268        * This code seems wrong.
269        *
270        * ge->myLastSeq = 1 - ge->myLastSeq;
271        * ge->waitingHighAck = FALSE;
272        * processQ(ge);
273        */
274     }
275   }
276   return(result);
277 }
278 
279 
processCommand(GmpEngine * ge,Command command,int val)280 static ButOut  processCommand(GmpEngine *ge, Command command, int val)  {
281   int  s, x, y;
282   ButOut  result = 0;
283 
284   switch(command)  {
285   case cmd_deny:
286     return(putCommand(ge, cmd_ack, ~0));
287     break;
288   case cmd_query:
289     respond(ge, val);
290     break;
291   case cmd_reset:  /* New game. */
292     ge->queriesAcked = 0;
293     askQuery(ge);
294     break;
295   case cmd_undo:  /* Take back moves. */
296     result = putCommand(ge, cmd_ack, ~0);
297     assert(ge->actions->undoMoves != NULL);
298     result |= ge->actions->undoMoves(ge, ge->packet, val);
299     return(result);
300     break;
301   case cmd_move:
302     s = val & 0x1ff;
303     if (s == 0)  {
304       x = -1;
305       y = 0;
306     } else  {
307       --s;
308       x = (s % ge->boardSize);
309       y = ge->boardSize - 1 - (s / ge->boardSize);
310     }
311     result = putCommand(ge, cmd_ack, ~0);
312     if (val & 0x200)
313       result |= ge->actions->moveRecvd(ge, ge->packet, goStone_white, x, y);
314     else
315       result |= ge->actions->moveRecvd(ge, ge->packet, goStone_black, x, y);
316     return(result);
317     break;
318   case cmd_respond:
319     return(gotQueryResponse(ge, val));
320     break;
321   default:  /* Don't understand command. */
322     return(putCommand(ge, cmd_deny, 0));
323     break;
324   }
325   return(0);
326 }
327 
328 
putCommand(GmpEngine * ge,Command cmd,int val)329 static ButOut  putCommand(GmpEngine *ge, Command cmd, int val)  {
330   int  writeResult;
331 
332   if (ge->waitingHighAck &&
333       (cmd != cmd_ack) && (cmd != cmd_respond) && (cmd != cmd_deny))  {
334     if (ge->sendsQueued < GMP_SENDBUFSIZE)  {
335       ge->sendsPending[ge->sendsQueued].cmd = cmd;
336       ge->sendsPending[ge->sendsQueued].val = val;
337       ++ge->sendsQueued;
338     } else  {
339       return(ge->actions->errorRecvd(ge, ge->packet,
340 				     msg_gmpSendBufFull));
341     }
342     return(0);
343   }
344   if ((cmd == cmd_ack) && (ge->sendsQueued))  {
345     ge->waitingHighAck = FALSE;
346     processQ(ge);
347     return(0);
348   }
349   if (cmd != cmd_ack)
350     ge->myLastSeq ^= 1;
351   ge->sendData[0] = ge->myLastSeq | (ge->hisLastSeq << 1);
352   ge->sendData[2] = 0x80 | (cmd << 4) | ((val >> 7) & 7);
353   ge->sendData[3] = 0x80 | val;
354   ge->sendData[1] = checksum(ge->sendData);
355   ge->lastSendTime = time(NULL);
356 #if DEBUG
357   if (showTransfers) {
358     fprintf(stderr,
359 	    "GMP: Writing command %s to port %d:%d.\n",
360 	    cmdNames[cmd], ge->inFile, ge->outFile);
361   }
362 #endif
363   writeResult = write(ge->outFile, ge->sendData, 4);
364   assert(writeResult == 4);
365   ge->waitingHighAck = (cmd != cmd_ack);
366   return(0);
367 }
368 
369 
respond(GmpEngine * ge,Query query)370 static void  respond(GmpEngine *ge, Query query)  {
371   int  response;
372 
373   if (query & 0x200)  {
374     /*
375      * Oops!  This really means "do you support this extended command."
376      * I don't support any extended commands, so the response should always
377      *   be zero.
378      */
379     response = 0;
380     /*
381      * * Do you support this query? *
382      *
383      * query &= ~0x200;
384      * if ((query == query_game) || (query == query_rules) ||
385      *     (query == query_handicap) || (query == query_boardSize) ||
386      *     (query == query_color))
387      *   response = 15;  * Yes. *
388      * else
389      *   response = 0;  * No. *
390      */
391   } else  {
392     ge->waitingHighAck = TRUE;
393     switch(query)  {
394     case query_game:
395       response = 1;  /* GO */
396       break;
397     case query_rules:
398       if (ge->chineseRules)
399 	response = 2;
400       else
401 	response = 1;
402       break;
403     case query_handicap:
404       response = ge->handicap;
405       if (response == 0)
406 	response = 1;
407       break;
408     case query_boardSize:
409       response = ge->boardSize;
410       break;
411     case query_color:
412       if (ge->iAmWhite)
413 	response = 1;
414       else
415 	response = 2;
416       break;
417     default:
418       response = 0;
419       break;
420     }
421   }
422   putCommand(ge, cmd_respond, response);
423 }
424 
425 
askQuery(GmpEngine * ge)426 static void  askQuery(GmpEngine *ge)  {
427   static const Query  queryList[GMP_NUMQUERIESTOASK] = {
428     query_rules, query_handicap, query_boardSize, query_color};
429   int  query;
430 
431   assert(ge->queriesAcked < GMP_NUMQUERIESTOASK);
432   query = queryList[ge->queriesAcked];
433   ge->lastQuerySent = query;
434   putCommand(ge, cmd_query, query);
435 }
436 
437 
gotQueryResponse(GmpEngine * ge,int val)438 static ButOut  gotQueryResponse(GmpEngine *ge, int val)  {
439   static const char  *ruleNames[] = {"Japanese", "Chinese"};
440   static const char  *colorNames[] = {"Black", "White"};
441   Str  err;
442   ButOut  result = 0;
443 
444   switch(ge->lastQuerySent)  {
445   case query_handicap:
446     if (val <= 1)
447       --val;
448     if ((val != -1) && (val != ge->handicap))  {
449       str_init(&err);
450       str_print(&err, "Handicaps do not agree; I want %d, he wants %d.",
451 		ge->handicap, val);
452       result |= ge->actions->errorRecvd(ge, ge->packet, str_chars(&err));
453       str_deinit(&err);
454     }
455     break;
456   case query_boardSize:
457     assert(ge->boardSize != 0);
458     if ((val != 0) && (val != ge->boardSize))  {
459       str_init(&err);
460       str_print(&err, "Board sizes do not agree; I want %d, he wants %d.",
461 		ge->boardSize, val);
462       result |= ge->actions->errorRecvd(ge, ge->packet, str_chars(&err));
463       str_deinit(&err);
464     }
465     break;
466   case query_rules:
467     if (val != 0)  {
468       if (ge->chineseRules != (val == 2))  {
469 	str_init(&err);
470 	str_print(&err, "Rule sets do not agree; I want %s, he wants %s.",
471 		  ruleNames[ge->chineseRules], ruleNames[val == 2]);
472 	result |= ge->actions->errorRecvd(ge, ge->packet, str_chars(&err));
473 	str_deinit(&err);
474       }
475     }
476     break;
477   case query_color:
478     if (val != 0)  {
479       if (ge->iAmWhite == (val == 1))  {
480 	str_init(&err);
481 	str_print(&err, "Colors sets do not agree; we both want to be %s.",
482 		  colorNames[ge->iAmWhite]);
483 	result |= ge->actions->errorRecvd(ge, ge->packet, str_chars(&err));
484 	str_deinit(&err);
485       }
486     }
487   }
488   if ((result & BUTOUT_ERR) == 0) {
489     ++ge->queriesAcked;
490     if (ge->queriesAcked < GMP_NUMQUERIESTOASK)
491       askQuery(ge);
492     else {
493       result |= putCommand(ge, cmd_ack, ~0);
494       if (ge->actions->newGame != NULL)
495 	ge->actions->newGame(ge,
496 			     ge->packet,
497 			     ge->boardSize,
498 			     ge->handicap,
499 			     ge->komi,
500 			     ge->chineseRules,
501 			     ge->iAmWhite);
502     }
503   }
504   return(result);
505 }
506 
507 
heartbeat(ButTimer * timer)508 static ButOut heartbeat(ButTimer *timer)  {
509   GmpEngine *ge = butTimer_packet(timer);
510   int  writeResult;
511 
512   assert(MAGIC(ge));
513   if (ge->waitingHighAck && (time(NULL) != ge->lastSendTime))  {
514     if (++ge->sendFailures > GMP_TIMEOUTSECS)  {
515       return(ge->actions->errorRecvd(ge, ge->packet,
516 				     msg_gmpTimeout));
517     } else  {
518       ge->lastSendTime = time(NULL);
519 #if DEBUG
520       if (showTransfers) {
521 	fprintf(stderr,
522 		"GMP: Writing command %s to port %d:%d (retry %d).\n",
523 		cmdNames[(ge->sendData[2] >> 4) & 7], ge->inFile, ge->outFile,
524 		ge->sendFailures);
525       }
526 #endif
527       writeResult = write(ge->outFile, ge->sendData, 4);
528       if (writeResult != 4) {
529 	return(BUTOUT_ERR);
530       }
531     }
532   }
533   return(0);
534 }
535 
536 
gmpEngine_startGame(GmpEngine * ge,int size,int handicap,float komi,bool chineseRules,bool iAmWhite)537 void  gmpEngine_startGame(GmpEngine *ge, int size, int handicap, float komi,
538 			  bool chineseRules, bool iAmWhite)  {
539   ge->boardSize = size;
540   ge->handicap = handicap;
541   ge->komi = komi;
542   ge->chineseRules = chineseRules;
543   ge->iAmWhite = iAmWhite;
544   ge->gameStarted = FALSE;
545   if (!iAmWhite) {
546     putCommand(ge, cmd_reset, 0);
547   }
548 }
549 
550 
gmpEngine_sendPass(GmpEngine * ge)551 void  gmpEngine_sendPass(GmpEngine *ge)  {
552   int  arg;
553 
554   assert(MAGIC(ge));
555   if (ge->iAmWhite)
556     arg = 0x200;
557   else
558     arg = 0;
559   putCommand(ge, cmd_move, arg);
560 }
561 
562 
gmpEngine_sendMove(GmpEngine * ge,int x,int y)563 void  gmpEngine_sendMove(GmpEngine *ge, int x, int y)  {
564   int  val;
565 
566   assert(MAGIC(ge));
567   val = x + ge->boardSize * (ge->boardSize - 1 - y) + 1;
568   if (ge->iAmWhite)
569     val |= 0x200;
570   putCommand(ge, cmd_move, val);
571 }
572 
573 
gmpEngine_sendUndo(GmpEngine * ge,int numUndos)574 void  gmpEngine_sendUndo(GmpEngine *ge, int numUndos)  {
575   assert(MAGIC(ge));
576   putCommand(ge, cmd_undo, numUndos);
577 }
578 
579 
processQ(GmpEngine * ge)580 static void  processQ(GmpEngine *ge)  {
581   int  i;
582 
583   if (!ge->waitingHighAck && ge->sendsQueued)  {
584     putCommand(ge, ge->sendsPending[0].cmd, ge->sendsPending[0].val);
585     --ge->sendsQueued;
586     for (i = 0;  i < ge->sendsQueued;  ++i)  {
587       ge->sendsPending[i] = ge->sendsPending[i + 1];
588     }
589   }
590 }
591 
592 
gmp_forkProgram(Cgoban * cg,int * inFile,int * outFile,const char * progName,int mainTime,int byTime)593 int  gmp_forkProgram(Cgoban *cg, int *inFile, int *outFile,
594 		     const char *progName, int mainTime, int byTime)  {
595   int  pid, result, fd;
596   int  cgToProg[2], progToCg[2];
597   int  i;
598   char  **argv;
599   Str  cmdLine;
600 
601   result = pipe(cgToProg);
602   if (result)
603     return(-1);
604   result = pipe(progToCg);
605   if (result)  {
606     close(cgToProg[0]);
607     close(cgToProg[1]);
608     return(-1);
609   }
610   pid = fork();
611   if (pid < 0)  {
612     close(cgToProg[0]);
613     close(cgToProg[1]);
614     close(progToCg[0]);
615     close(progToCg[1]);
616     return(-1);
617   }
618   if (pid == 0)  {
619     /* The child. */
620     for (fd = 0;  fd < 100;  ++fd)  {
621       if ((fd != 2) &&  /* Don't close stderr! */
622 	  (fd != cgToProg[0]) && (fd != progToCg[1]))
623 	close(fd);
624     }
625     if (dup(cgToProg[0]) != 0)  {
626       fprintf(stderr, "I couldn't fdup into stdin!\n");
627       exit(1);
628     }
629     if (dup(progToCg[1]) != 1)  {
630       fprintf(stderr, "I couldn't fdup into stdout!\n");
631       exit(1);
632     }
633     str_init(&cmdLine);
634     str_print(&cmdLine, progName, mainTime, byTime);
635     argv = makeArgv(str_chars(&cmdLine));
636     i = execve(argv[0], argv, cg->envp);
637     /*
638      * If we got here, then the execve() must have failed.
639      * No need to clean up, free cmdLine, etc...we're dead!
640      */
641     exit(GMP_EXECVEFAILED);
642   } else  {
643     close(cgToProg[0]);
644     close(progToCg[1]);
645     *inFile = progToCg[0];
646     *outFile = cgToProg[1];
647     return(pid);
648   }
649 }
650 
651 
makeArgv(const char * cmdLine)652 static char  **makeArgv(const char *cmdLine)  {
653   char  *buf;
654   char  **args;
655   int  i, numWords, len;
656 
657   len = strlen(cmdLine);
658   buf = wms_malloc(len + 1);
659   strcpy(buf, cmdLine);
660   numWords = 1;
661   for (i = 1;  i < len;  ++i)  {
662     if ((buf[i - 1] == ' ') && (buf[i] != ' '))
663       ++numWords;
664   }
665   args = wms_malloc((numWords + 1) * sizeof(char *));
666   args[0] = buf;
667   numWords = 1;
668   for (i = 1;  i < len;  ++i)  {
669     if (buf[i] == ' ')
670       buf[i] = '\0';
671     if ((buf[i - 1] == '\0') && (buf[i] != '\0'))  {
672       args[numWords++] = buf + i;
673     }
674   }
675   args[numWords++] = NULL;
676   return(args);
677 }
678