added setting network connection flag for network games
[rocksndiamonds.git] / src / network.c
1 // ============================================================================
2 // Rocks'n'Diamonds - McDuffin Strikes Back!
3 // ----------------------------------------------------------------------------
4 // (c) 1995-2014 by Artsoft Entertainment
5 //                  Holger Schemel
6 //                  info@artsoft.org
7 //                  http://www.artsoft.org/
8 // ----------------------------------------------------------------------------
9 // network.c
10 // ============================================================================
11
12 #include <signal.h>
13 #include <sys/time.h>
14
15 #include "libgame/libgame.h"
16
17 #include "network.h"
18 #include "netserv.h"
19 #include "main.h"
20 #include "game.h"
21 #include "tape.h"
22 #include "files.h"
23 #include "tools.h"
24 #include "screens.h"
25
26 struct NetworkClientPlayerInfo
27 {
28   byte nr;
29   char name[MAX_PLAYER_NAME_LEN + 1];
30   struct NetworkClientPlayerInfo *next;
31 };
32
33 static struct NetworkClientPlayerInfo first_player =
34 {
35   0,
36   EMPTY_PLAYER_NAME,
37   NULL
38 };
39
40 /* server stuff */
41
42 static TCPsocket sfd;           /* TCP server socket */
43 static UDPsocket udp;           /* UDP server socket */
44 static SDLNet_SocketSet rfds;   /* socket set */
45
46 static byte realbuffer[512];
47 static byte readbuffer[MAX_BUFFER_SIZE], writbuffer[MAX_BUFFER_SIZE];
48 static byte *buffer = realbuffer + 4;
49 static int nread = 0, nwrite = 0;
50 static boolean stop_network_game = FALSE;
51
52 static void SendBufferToServer(int size)
53 {
54   if (!network.enabled)
55     return;
56
57   realbuffer[0] = realbuffer[1] = realbuffer[2] = 0;
58   realbuffer[3] = (byte)size;
59   buffer[0] = 0;
60
61   if (nwrite + 4 + size >= MAX_BUFFER_SIZE)
62     Error(ERR_EXIT, "internal error: network send buffer overflow");
63
64   memcpy(writbuffer + nwrite, realbuffer, 4 + size);
65   nwrite += 4 + size;
66
67   /* directly send the buffer to the network server */
68   SDLNet_TCP_Send(sfd, writbuffer, nwrite);
69   nwrite = 0;
70 }
71
72 struct NetworkClientPlayerInfo *getNetworkPlayer(int player_nr)
73 {
74   struct NetworkClientPlayerInfo *player = NULL;
75
76   for (player = &first_player; player; player = player->next)
77     if (player->nr == player_nr)
78       break;
79
80   if (player == NULL)   /* should not happen */
81     Error(ERR_EXIT, "protocol error: reference to non-existing player %d",
82           player_nr);
83
84   return player;
85 }
86
87 char *getNetworkPlayerName(int player_nr)
88 {
89   struct NetworkClientPlayerInfo *player;
90
91   if (player_nr == 0)
92     return("the network game server");
93   else if (player_nr == first_player.nr)
94     return("you");
95   else
96     for (player = &first_player; player; player = player->next)
97       if (player->nr == player_nr && strlen(player->name) > 0)
98         return(player->name);
99
100   return(EMPTY_PLAYER_NAME);
101 }
102
103 static void StartNetworkServer(int port)
104 {
105   static int p;
106
107   p = port;
108 #if defined(TARGET_SDL2)
109   server_thread = SDL_CreateThread(NetworkServerThread,
110                                    "NetworkServerThread", &p);
111 #else
112   server_thread = SDL_CreateThread(NetworkServerThread, &p);
113 #endif
114   network_server = TRUE;
115 }
116
117 boolean ConnectToServer(char *hostname, int port)
118 {
119   IPaddress ip;
120   int server_host = 0;
121   int i;
122
123   if (port == 0)
124     port = DEFAULT_SERVER_PORT;
125
126   if (hostname == NULL)
127   {
128     // if no hostname given, try to auto-detect network server in local network
129     // by doing a UDP broadcast on the network server port and wait for answer
130
131     SDLNet_SocketSet udp_socket_set = SDLNet_AllocSocketSet(1);
132     if (!udp_socket_set)
133       Error(ERR_EXIT, "SDLNet_AllocSocketSet() failed: %s"), SDLNet_GetError();
134
135     udp = SDLNet_UDP_Open(0);
136     if(!udp)
137       Error(ERR_EXIT, "SDLNet_UDP_Open() failed: %s", SDLNet_GetError());
138
139     if (SDLNet_UDP_AddSocket(udp_socket_set, udp) == -1)
140       Error(ERR_EXIT_NETWORK_SERVER, "SDLNet_TCP_AddSocket() failed: %s"),
141         SDLNet_GetError();
142
143     char *data_ptr = "network server UDB broadcast";
144     int data_len = strlen(data_ptr) + 1;
145     IPaddress ip_address;
146
147     SDLNet_Write32(0xffffffff, &ip_address.host);       /* 255.255.255.255 */
148     SDLNet_Write16(port,       &ip_address.port);
149
150     UDPpacket packet =
151     {
152       -1,
153       (Uint8 *)data_ptr,
154       data_len,
155       data_len,
156       0,
157       ip_address
158     };
159
160     SDLNet_UDP_Send(udp, -1, &packet);
161
162     Error(ERR_DEBUG, "doing UDP broadcast for local network server ...");
163
164     if (SDLNet_CheckSockets(udp_socket_set, 1000) == 1)
165     {
166       int num_packets = SDLNet_UDP_Recv(udp, &packet);
167
168       if (num_packets == 1)
169       {
170         Error(ERR_DEBUG, "network server found");
171
172         server_host = SDLNet_Read32(&packet.address.host);
173       }
174       else
175       {
176         Error(ERR_DEBUG, "no answer from network server");
177       }
178     }
179     else
180     {
181       Error(ERR_DEBUG, "no network server found");
182     }
183   }
184
185   rfds = SDLNet_AllocSocketSet(1);
186
187   if (hostname)
188   {
189     SDLNet_ResolveHost(&ip, hostname, port);
190
191     if (ip.host == INADDR_NONE)
192       Error(ERR_EXIT, "cannot locate host '%s'", hostname);
193     else
194       server_host = SDLNet_Read32(&ip.host);
195   }
196   else
197   {
198     // if no hostname was given and no network server was auto-detected in the
199     // local network, try to connect to a network server at the local host
200     if (server_host == 0)
201       server_host = 0x7f000001;                 /* 127.0.0.1 */
202
203     SDLNet_Write32(server_host, &ip.host);
204     SDLNet_Write16(port,        &ip.port);
205   }
206
207   Error(ERR_DEBUG, "trying to connect to network server at %d.%d.%d.%d ...",
208         (server_host >> 24) & 0xff,
209         (server_host >> 16) & 0xff,
210         (server_host >>  8) & 0xff,
211         (server_host >>  0) & 0xff);
212
213   sfd = SDLNet_TCP_Open(&ip);
214
215   if (sfd)
216   {
217     SDLNet_TCP_AddSocket(rfds, sfd);
218     return TRUE;
219   }
220   else
221   {
222     printf("SDLNet_TCP_Open(): %s\n", SDLNet_GetError());
223   }
224
225   if (hostname)                 /* connect to specified server failed */
226     return FALSE;
227
228   printf("No rocksndiamonds server on localhost -- starting up one ...\n");
229   StartNetworkServer(port);
230
231   /* wait for server to start up and try connecting several times */
232   for (i = 0; i < 6; i++)
233   {
234     Delay(500);                 /* wait 500 ms == 0.5 seconds */
235
236     if ((sfd = SDLNet_TCP_Open(&ip)))           /* connected */
237     {
238       SDLNet_TCP_AddSocket(rfds, sfd);
239       return TRUE;
240     }
241   }
242
243   /* when reaching this point, connect to newly started server has failed */
244   return FALSE;
245 }
246
247 void SendToServer_PlayerName(char *player_name)
248 {
249   int len_player_name = strlen(player_name);
250
251   buffer[1] = OP_PLAYER_NAME;
252   memcpy(&buffer[2], player_name, len_player_name);
253   SendBufferToServer(2 + len_player_name);
254   Error(ERR_NETWORK_CLIENT, "you set your player name to \"%s\"", player_name);
255 }
256
257 void SendToServer_ProtocolVersion()
258 {
259   buffer[1] = OP_PROTOCOL_VERSION;
260   buffer[2] = PROTOCOL_VERSION_1;
261   buffer[3] = PROTOCOL_VERSION_2;
262   buffer[4] = PROTOCOL_VERSION_3;
263
264   SendBufferToServer(5);
265 }
266
267 void SendToServer_NrWanted(int nr_wanted)
268 {
269   buffer[1] = OP_NUMBER_WANTED;
270   buffer[2] = nr_wanted;
271
272   SendBufferToServer(3);
273 }
274
275 void SendToServer_StartPlaying()
276 {
277   unsigned int new_random_seed = InitRND(level.random_seed);
278
279   int dummy = 0;                /* !!! HAS NO MEANING ANYMORE !!! */
280                                 /* the name of the level must be enough */
281
282   buffer[1] = OP_START_PLAYING;
283   buffer[2] = (byte)(level_nr >> 8);
284   buffer[3] = (byte)(level_nr & 0xff);
285   buffer[4] = (byte)(dummy >> 8);
286   buffer[5] = (byte)(dummy & 0xff);
287
288   buffer[6] = (unsigned char)((new_random_seed >> 24) & 0xff);
289   buffer[7] = (unsigned char)((new_random_seed >> 16) & 0xff);
290   buffer[8] = (unsigned char)((new_random_seed >>  8) & 0xff);
291   buffer[9] = (unsigned char)((new_random_seed >>  0) & 0xff);
292
293   strcpy((char *)&buffer[10], leveldir_current->identifier);
294
295   SendBufferToServer(10 + strlen(leveldir_current->identifier) + 1);
296 }
297
298 void SendToServer_PausePlaying()
299 {
300   buffer[1] = OP_PAUSE_PLAYING;
301
302   SendBufferToServer(2);
303 }
304
305 void SendToServer_ContinuePlaying()
306 {
307   buffer[1] = OP_CONTINUE_PLAYING;
308
309   SendBufferToServer(2);
310 }
311
312 void SendToServer_StopPlaying(int cause_for_stopping)
313 {
314   buffer[1] = OP_STOP_PLAYING;
315   buffer[2] = cause_for_stopping;
316
317   SendBufferToServer(3);
318 }
319
320 void SendToServer_MovePlayer(byte player_action)
321 {
322   buffer[1] = OP_MOVE_PLAYER;
323   buffer[2] = player_action;
324
325   SendBufferToServer(3);
326 }
327
328 static void Handle_OP_BAD_PROTOCOL_VERSION()
329 {
330   Error(ERR_WARN, "protocol version mismatch");
331   Error(ERR_EXIT, "server expects %d.%d.x instead of %d.%d.%d",
332         buffer[2], buffer[3],
333         PROTOCOL_VERSION_1, PROTOCOL_VERSION_2, PROTOCOL_VERSION_3);
334 }
335
336 static void Handle_OP_YOUR_NUMBER()
337 {
338   int new_client_nr = buffer[2];
339   int new_index_nr = new_client_nr - 1;
340   struct PlayerInfo *old_local_player = local_player;
341   struct PlayerInfo *new_local_player = &stored_player[new_index_nr];
342
343   printf("OP_YOUR_NUMBER: %d\n", buffer[0]);
344   first_player.nr = new_client_nr;
345
346   if (old_local_player != new_local_player)
347   {
348     /* set relevant player settings and change to new player */
349
350     local_player = new_local_player;
351
352     old_local_player->connected_locally = FALSE;
353     new_local_player->connected_locally = TRUE;
354
355     old_local_player->connected_network = FALSE;
356     new_local_player->connected_network = TRUE;
357   }
358
359   if (first_player.nr > MAX_PLAYERS)
360     Error(ERR_EXIT, "sorry, more than %d players not allowed", MAX_PLAYERS);
361
362   Error(ERR_NETWORK_CLIENT, "you get client # %d", new_client_nr);
363 }
364
365 static void Handle_OP_NUMBER_WANTED()
366 {
367   int client_nr_wanted = buffer[2];
368   int old_client_nr = buffer[0];
369   int new_client_nr = buffer[3];
370   int old_index_nr = old_client_nr - 1;
371   int new_index_nr = new_client_nr - 1;
372   int index_nr_wanted = client_nr_wanted - 1;
373   struct PlayerInfo *old_player = &stored_player[old_index_nr];
374   struct PlayerInfo *new_player = &stored_player[new_index_nr];
375
376   printf("OP_NUMBER_WANTED: %d\n", buffer[0]);
377
378   if (new_client_nr == client_nr_wanted)        /* switching succeeded */
379   {
380     struct NetworkClientPlayerInfo *player;
381
382     if (old_client_nr != client_nr_wanted)      /* client's nr has changed */
383       Error(ERR_NETWORK_CLIENT, "client %d switches to # %d",
384             old_client_nr, new_client_nr);
385     else if (old_client_nr == first_player.nr)  /* local player keeps his nr */
386       Error(ERR_NETWORK_CLIENT, "keeping client # %d", new_client_nr);
387
388     if (old_client_nr != new_client_nr)
389     {
390       /* set relevant player settings and change to new player */
391
392       old_player->connected_network = FALSE;
393       new_player->connected_network = TRUE;
394     }
395
396     player = getNetworkPlayer(old_client_nr);
397     player->nr = new_client_nr;
398
399     if (old_player == local_player)             /* local player switched */
400     {
401       local_player = new_player;
402
403       old_player->connected_locally = FALSE;
404       new_player->connected_locally = TRUE;
405     }
406   }
407   else if (old_client_nr == first_player.nr)    /* failed -- local player? */
408   {
409     char request[100];
410
411     sprintf(request, "Sorry! Player %d already exists! You are player %d!",
412             index_nr_wanted + 1, new_index_nr + 1);
413
414     Request(request, REQ_CONFIRM);
415
416     Error(ERR_NETWORK_CLIENT, "cannot switch -- you keep client # %d",
417           new_client_nr);
418   }
419 }
420
421 static void Handle_OP_PLAYER_NAME(unsigned int len)
422 {
423   struct NetworkClientPlayerInfo *player;
424   int player_nr = (int)buffer[0];
425
426   printf("OP_PLAYER_NAME: %d\n", player_nr);
427   player = getNetworkPlayer(player_nr);
428   buffer[len] = 0;
429   Error(ERR_NETWORK_CLIENT, "client %d calls itself \"%s\"",
430         buffer[0], &buffer[2]);
431   strncpy(player->name, (char *)&buffer[2], MAX_PLAYER_NAME_LEN);
432 }
433
434 static void Handle_OP_PLAYER_CONNECTED()
435 {
436   struct NetworkClientPlayerInfo *player, *last_player = NULL;
437   int new_client_nr = (int)buffer[0];
438   int new_index_nr = new_client_nr - 1;
439
440   printf("OP_PLAYER_CONNECTED: %d\n", new_client_nr);
441   Error(ERR_NETWORK_CLIENT, "new client %d connected", new_client_nr);
442
443   for (player = &first_player; player; player = player->next)
444   {
445     if (player->nr == new_client_nr)
446       Error(ERR_EXIT, "multiplayer server sent duplicate player id");
447
448     last_player = player;
449   }
450
451   last_player->next = player =
452     checked_malloc(sizeof(struct NetworkClientPlayerInfo));
453   player->nr = new_client_nr;
454   player->name[0] = '\0';
455   player->next = NULL;
456
457   stored_player[new_index_nr].connected_network = TRUE;
458 }
459
460 static void Handle_OP_PLAYER_DISCONNECTED()
461 {
462   struct NetworkClientPlayerInfo *player, *player_disconnected;
463   int player_nr = (int)buffer[0];
464   int index_nr = player_nr - 1;
465
466   printf("OP_PLAYER_DISCONNECTED: %d\n", player_nr);
467   player_disconnected = getNetworkPlayer(player_nr);
468   Error(ERR_NETWORK_CLIENT, "client %d (%s) disconnected",
469         player_nr, getNetworkPlayerName(buffer[0]));
470
471   for (player = &first_player; player; player = player->next)
472     if (player->next == player_disconnected)
473       player->next = player_disconnected->next;
474   free(player_disconnected);
475
476   stored_player[index_nr].connected_locally = FALSE;
477   stored_player[index_nr].connected_network = FALSE;
478 }
479
480 static void Handle_OP_START_PLAYING()
481 {
482   LevelDirTree *new_leveldir;
483   int new_level_nr;
484   unsigned int new_random_seed;
485   char *new_leveldir_identifier;
486
487   new_level_nr = (buffer[2] << 8) + buffer[3];
488   new_random_seed =
489     (buffer[6] << 24) | (buffer[7] << 16) | (buffer[8] << 8) | (buffer[9]);
490   new_leveldir_identifier = (char *)&buffer[10];
491
492   new_leveldir = getTreeInfoFromIdentifier(leveldir_first,
493                                            new_leveldir_identifier);
494   if (new_leveldir == NULL)
495   {
496     Error(ERR_WARN, "no such level identifier: '%s'", new_leveldir_identifier);
497
498     new_leveldir = leveldir_first;
499     Error(ERR_WARN, "using default level set: '%s'", new_leveldir->identifier);
500   }
501
502   printf("OP_START_PLAYING: %d\n", buffer[0]);
503   Error(ERR_NETWORK_CLIENT,
504         "client %d starts game [level %d from level identifier '%s']\n",
505         buffer[0], new_level_nr, new_leveldir->identifier);
506
507   leveldir_current = new_leveldir;
508   level_nr = new_level_nr;
509
510   TapeErase();
511   LoadTape(level_nr);
512   LoadLevel(level_nr);
513
514   StartGameActions(FALSE, setup.autorecord, new_random_seed);
515 }
516
517 static void Handle_OP_PAUSE_PLAYING()
518 {
519   printf("OP_PAUSE_PLAYING: %d\n", buffer[0]);
520   Error(ERR_NETWORK_CLIENT, "client %d pauses game", buffer[0]);
521
522   tape.pausing = TRUE;
523   DrawVideoDisplay(VIDEO_STATE_PAUSE_ON,0);
524 }
525
526 static void Handle_OP_CONTINUE_PLAYING()
527 {
528   printf("OP_CONTINUE_PLAYING: %d\n", buffer[0]);
529   Error(ERR_NETWORK_CLIENT, "client %d continues game", buffer[0]);
530
531   tape.pausing = FALSE;
532   DrawVideoDisplay(VIDEO_STATE_PAUSE_OFF,0);
533 }
534
535 static void Handle_OP_STOP_PLAYING()
536 {
537   printf("OP_STOP_PLAYING: %d [%d]\n", buffer[0], buffer[2]);
538   Error(ERR_NETWORK_CLIENT, "client %d stops game [%d]", buffer[0], buffer[2]);
539
540   if (game_status == GAME_MODE_PLAYING)
541   {
542     int client_nr = buffer[0];
543     int index_nr = client_nr - 1;
544     struct PlayerInfo *client_player = &stored_player[index_nr];
545     boolean stopped_by_remote_player = (!client_player->connected_locally);
546     char *message = (buffer[2] == NETWORK_STOP_BY_PLAYER ?
547                      "Network game stopped by player!" :
548                      buffer[2] == NETWORK_STOP_BY_ERROR ?
549                      "Network game stopped due to internal error!" :
550                      "Network game stopped!");
551
552     if (buffer[2] != NETWORK_STOP_BY_PLAYER || stopped_by_remote_player)
553       Request(message, REQ_CONFIRM | REQ_STAY_CLOSED);
554   }
555
556   SetGameStatus(GAME_MODE_MAIN);
557
558   DrawMainMenu();
559 }
560
561 static void Handle_OP_MOVE_PLAYER(unsigned int len)
562 {
563   int server_frame_counter;
564   int i;
565
566   if (!network_playing)
567     return;
568
569   server_frame_counter =
570     (buffer[2] << 24) | (buffer[3] << 16) | (buffer[4] << 8) | (buffer[5]);
571
572   if (server_frame_counter != FrameCounter)
573   {
574     Error(ERR_INFO, "client and servers frame counters out of sync");
575     Error(ERR_INFO, "frame counter of client is %d", FrameCounter);
576     Error(ERR_INFO, "frame counter of server is %d", server_frame_counter);
577     Error(ERR_INFO, "this should not happen -- please debug");
578
579     stop_network_game = TRUE;
580
581     return;
582   }
583
584   /* copy valid player actions */
585   for (i = 0; i < MAX_PLAYERS; i++)
586     stored_player[i].effective_action =
587       (i < len - 6 ? buffer[6 + i] : 0);
588
589   network_player_action_received = TRUE;
590 }
591
592 static void HandleNetworkingMessages()
593 {
594   unsigned int message_length;
595
596   stop_network_game = FALSE;
597
598   while (nread >= 4 && nread >= 4 + readbuffer[3])
599   {
600     message_length = readbuffer[3];
601     if (readbuffer[0] || readbuffer[1] || readbuffer[2])
602       Error(ERR_EXIT, "wrong network server line length");
603
604     memcpy(buffer, &readbuffer[4], message_length);
605     nread -= 4 + message_length;
606     memmove(readbuffer, readbuffer + 4 + message_length, nread);
607
608     switch (buffer[1])
609     {
610       case OP_BAD_PROTOCOL_VERSION:
611         Handle_OP_BAD_PROTOCOL_VERSION();
612         break;
613
614       case OP_YOUR_NUMBER:
615         Handle_OP_YOUR_NUMBER();
616         break;
617
618       case OP_NUMBER_WANTED:
619         Handle_OP_NUMBER_WANTED();
620         break;
621
622       case OP_PLAYER_NAME:
623         Handle_OP_PLAYER_NAME(message_length);
624         break;
625
626       case OP_PLAYER_CONNECTED:
627         Handle_OP_PLAYER_CONNECTED();
628         break;
629       
630       case OP_PLAYER_DISCONNECTED:
631         Handle_OP_PLAYER_DISCONNECTED();
632         break;
633
634       case OP_START_PLAYING:
635         Handle_OP_START_PLAYING();
636         break;
637
638       case OP_PAUSE_PLAYING:
639         Handle_OP_PAUSE_PLAYING();
640         break;
641
642       case OP_CONTINUE_PLAYING:
643         Handle_OP_CONTINUE_PLAYING();
644         break;
645
646       case OP_STOP_PLAYING:
647         Handle_OP_STOP_PLAYING();
648         break;
649
650       case OP_MOVE_PLAYER:
651         Handle_OP_MOVE_PLAYER(message_length);
652         break;
653
654       case OP_BROADCAST_MESSAGE:
655         printf("OP_BROADCAST_MESSAGE: %d\n", buffer[0]);
656         Error(ERR_NETWORK_CLIENT, "client %d sends message", buffer[0]);
657         break;
658     }
659   }
660
661   fflush(stdout);
662
663   /* in case of internal error, stop network game */
664   if (stop_network_game)
665     SendToServer_StopPlaying(NETWORK_STOP_BY_ERROR);
666 }
667
668 static char *HandleNetworkingPackets()
669 {
670   while (1)
671   {
672     /* ---------- check network server for activity ---------- */
673
674     int num_active_sockets = SDLNet_CheckSockets(rfds, 1);
675
676     if (num_active_sockets < 0)
677       return "Error checking network sockets!";
678
679     if (num_active_sockets == 0)
680       break;    // no active sockets, stop here
681
682     /* ---------- read packets from network server ---------- */
683
684     int num_bytes = SDLNet_TCP_Recv(sfd, readbuffer + nread, 1);
685
686     if (num_bytes < 0)
687       return "Error reading from network server!";
688
689     if (num_bytes == 0)
690       return "Connection to network server lost!";
691
692     nread += num_bytes;
693
694     HandleNetworkingMessages();
695   }
696
697   return NULL;
698 }
699
700 static void HandleNetworkingDisconnect()
701 {
702   int i;
703
704   SDLNet_TCP_DelSocket(rfds, sfd);
705   SDLNet_TCP_Close(sfd);
706
707   network_playing = FALSE;
708
709   network.enabled = FALSE;
710   network.connected = FALSE;
711
712   setup.network_mode = FALSE;
713
714   for (i = 0; i < MAX_PLAYERS; i++)
715     stored_player[i].connected_network = FALSE;
716 }
717
718 void HandleNetworking()
719 {
720   char *error_message = HandleNetworkingPackets();
721
722   if (error_message != NULL)
723   {
724     HandleNetworkingDisconnect();
725
726     if (game_status == GAME_MODE_PLAYING)
727     {
728       Request(error_message, REQ_CONFIRM | REQ_STAY_CLOSED);
729
730       SetGameStatus(GAME_MODE_MAIN);
731
732       DrawMainMenu();
733     }
734     else
735     {
736       Request(error_message, REQ_CONFIRM);
737     }
738   }
739 }