]> icculus.org git repositories - taylor/freespace2.git/blob - src/network/stand_server.cpp
fix issue with looping audio streams
[taylor/freespace2.git] / src / network / stand_server.cpp
1 /*
2  * Copyright (C) Volition, Inc. 1999.  All rights reserved.
3  *
4  * All source code herein is the property of Volition, Inc. You may not sell
5  * or otherwise commercially exploit the source or things you created based on the
6  * source.
7  *
8 */
9
10
11 #include "pstypes.h"
12 #include "multi.h"
13 #include "stand_server.h"
14
15 #ifndef __EMSCRIPTEN__
16
17 #include "osregistry.h"
18 #include "multi_options.h"
19 #include "gamesequence.h"
20 #include "timer.h"
21 #include "version.h"
22 #include "multi_pmsg.h"
23 #include "multi_endgame.h"
24 #include "multimsgs.h"
25 #include "multiutil.h"
26 #include "freespace.h"
27 #include "missiongoals.h"
28 #include "cmdline.h"
29 #include "multi_kick.h"
30 #include "multi_fstracker.h"
31 #include "osregistry.h"
32 #include "standalone_html.h"
33
34 #include <libwebsockets.h>
35 #include <string>
36 #include <vector>
37 #include <list>
38
39
40 struct std_state {
41         std::string title;
42         std::string debug_txt;
43         std::string popup_title;
44         std::string popup_field1;
45         std::string popup_field2;
46
47         std::string active_player;
48 };
49
50 static std_state Standalone_state;
51
52 static std::list<std::string> Standalone_send_buf;
53
54
55 #define STANDALONE_MAX_BAN              50
56 static std::vector<std::string> Standalone_ban_list;
57
58 #define STD_STATS_UPDATE_TIME           500             // ms between updating player stats
59 #define STD_NG_UPDATE_TIME                      1500    // ms between updating netgame information
60 #define STD_PING_UPDATE_TIME            1000    // ms between updating pings
61 #define STD_FPS_UPDATE_TIME                     250             // ms between fps updates
62
63 static int Standalone_stats_stamp = -1;
64 static int Standalone_ng_stamp = -1;
65 static int Standalone_ping_stamp = -1;
66 static int Standalone_fps_stamp = -1;
67
68 static lws_context *stand_context = NULL;
69
70 static int startup_reset_stamp;
71 static struct lws *active_wsi = NULL;
72
73
74 static int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
75 {
76         unsigned char buf[LWS_SEND_BUFFER_PRE_PADDING + standalone_html_len + LWS_SEND_BUFFER_POST_PADDING];
77         unsigned char *p = &buf[LWS_SEND_BUFFER_PRE_PADDING];
78         unsigned char *start = p;
79         unsigned char *end = p + standalone_html_len;
80         bool try_reuse = false;
81         int rval;
82         int size;
83         static unsigned int sent = 0;
84
85         switch (reason) {
86                 case LWS_CALLBACK_HTTP: {
87                         if (len < 1) {
88                                 lws_return_http_status(wsi, HTTP_STATUS_BAD_REQUEST, NULL);
89                                 try_reuse = true;
90
91                                 break;
92                         }
93
94                         // no favicon so return 404
95                         if ( in && !SDL_strcmp((const char *)in, "/favicon.ico") ) {
96                                 lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
97                                 try_reuse = true;
98
99                                 break;
100                         }
101
102                         // any other request will get our basic html ...
103
104 #ifndef NDEBUG
105                         FILE *html = fopen("./standalone.html", "rb");
106
107                         if (html) {
108                                 fclose(html);
109
110                                 rval = lws_serve_http_file(wsi, "./standalone.html", "text/html", NULL, 0);
111
112                                 if ( (rval < 0) || ((rval > 0) && lws_http_transaction_completed(wsi)) ) {
113                                         // error or can't reuse connection, close the socket
114                                         return -1;
115                                 }
116                         } else
117 #endif
118                         {
119                                 if ( lws_add_http_header_status(wsi, 200, &p, end) ) {
120                                         return 1;
121                                 }
122
123                                 if ( lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_SERVER, (unsigned char *)"libwebsockets", 13, &p, end) ) {
124                                         return 1;
125                                 }
126
127                                 if ( lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char *)"text/html", 9, &p, end) ) {
128                                         return 1;
129                                 }
130
131                                 if ( lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_ENCODING, (unsigned char *)"gzip", 4, &p, end) ) {
132                                         return 1;
133                                 }
134
135                                 if ( lws_add_http_header_content_length(wsi, standalone_html_len, &p, end) ) {
136                                         return 1;
137                                 }
138
139                                 if ( lws_finalize_http_header(wsi, &p, end) ) {
140                                         return 1;
141                                 }
142
143                                 rval = lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
144
145                                 if (rval != (p - start)) {
146                                         return -1;
147                                 }
148
149                                 sent = 0;
150
151                                 lws_callback_on_writable(wsi);
152                         }
153
154                         break;
155                 }
156
157                 case LWS_CALLBACK_HTTP_BODY_COMPLETION: {
158                         lws_return_http_status(wsi, HTTP_STATUS_OK, NULL);
159                         try_reuse = true;
160
161                         break;
162                 }
163
164                 case LWS_CALLBACK_HTTP_FILE_COMPLETION: {
165                         try_reuse = true;
166
167                         break;
168                 }
169
170                 case LWS_CALLBACK_HTTP_WRITEABLE: {
171                         while ( !lws_send_pipe_choked(wsi) && (sent < standalone_html_len) ) {
172                                 size = standalone_html_len - sent;
173
174                                 int pwa = lws_get_peer_write_allowance(wsi);
175
176                                 if (pwa == 0) {
177                                         lws_callback_on_writable(wsi);
178
179                                         break;
180                                 }
181
182                                 if ( (pwa != -1) && (pwa < size) ) {
183                                         size = pwa;
184                                 }
185
186                                 memcpy(p, standalone_html + sent, size);
187
188                                 rval = lws_write(wsi, p, size, LWS_WRITE_HTTP);
189
190                                 if (rval < 0) {
191                                         return -1;
192                                 }
193
194                                 if (rval) {
195                                         // while still active, extent timeout
196                                         lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_CONTENT, 5);
197                                 }
198
199                                 sent += rval;
200                         }
201
202                         try_reuse = true;
203
204                         break;
205                 }
206
207                 default:
208                         break;
209         }
210
211         if (try_reuse) {
212                 if (lws_http_transaction_completed(wsi)) {
213                         return -1;
214                 }
215         }
216
217         return 0;
218 }
219
220 static int callback_standalone(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
221 {
222         #define MAX_BUF_SIZE    1024
223         unsigned char buf[LWS_SEND_BUFFER_PRE_PADDING + MAX_BUF_SIZE + LWS_SEND_BUFFER_POST_PADDING];
224         unsigned char *p = &buf[LWS_SEND_BUFFER_PRE_PADDING];
225         int rval;
226         int size;
227
228         switch (reason) {
229                 case LWS_CALLBACK_ESTABLISHED: {
230                         active_wsi = wsi;
231
232                         if ( timestamp_elapsed(startup_reset_stamp) ) {
233                                 std_reset_standalone_gui();
234                         }
235
236                         break;
237                 }
238
239                 case LWS_CALLBACK_CLOSED: {
240                         active_wsi = NULL;
241
242                         break;
243                 }
244
245                 case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION: {
246                         if (active_wsi) {
247                                 return -1;
248                         }
249
250                         break;
251                 }
252
253                 case LWS_CALLBACK_SERVER_WRITEABLE: {
254                         while ( !Standalone_send_buf.empty() ) {
255                                 size = SDL_strlcpy((char *)p, Standalone_send_buf.front().c_str(), MAX_BUF_SIZE);
256
257                                 rval = lws_write(wsi, p, size, LWS_WRITE_TEXT);
258
259                                 if (rval < size) {
260                                         lwsl_err("ERROR sending buffer!\n");
261                                         lws_close_reason(wsi, LWS_CLOSE_STATUS_UNEXPECTED_CONDITION, (unsigned char *)"write error", 11);
262
263                                         return -1;
264                                 }
265
266                                 Standalone_send_buf.pop_front();
267
268                                 if ( lws_send_pipe_choked(wsi) ) {
269                                         lws_callback_on_writable(wsi);
270
271                                         break;
272                                 }
273                         }
274
275                         break;
276                 }
277
278                 case LWS_CALLBACK_RECEIVE: {
279                         if (in != NULL && len > 0) {
280                                 const char *msg = (const char *)in;
281                                 char mtype = msg[0];
282
283                                 if ( !SDL_strcmp(msg, "shutdown") ) {
284                                         gameseq_post_event(GS_EVENT_QUIT_GAME);
285                                         lws_close_reason(wsi, LWS_CLOSE_STATUS_GOINGAWAY, (unsigned char *)"shutdown", 8);
286
287                                         return -1;
288                                 }
289
290                                 if ( !SDL_strcmp(msg, "reset") ) {
291                                         multi_quit_game(PROMPT_NONE);
292                                         std_reset_standalone_gui();
293
294                                         break;
295                                 }
296
297                                 // server tab
298                                 if (mtype == 'S') {
299                                         if (len >= 7) {
300                                                 if ( !SDL_strncmp(msg+2, "name ", 5) ) {
301                                                         SDL_strlcpy(Netgame.name, msg+7, SDL_arraysize(Netgame.name));
302                                                         SDL_strlcpy(Multi_options_g.std_pname, Netgame.name, SDL_arraysize(Multi_options_g.std_pname));
303                                                 } else if ( !SDL_strncmp(msg+2, "pass ", 5) ) {
304                                                         SDL_strlcpy(Multi_options_g.std_passwd, msg+7, SDL_arraysize(Multi_options_g.std_passwd));
305                                                 } else if ( !SDL_strncmp(msg+2, "kick ", 5) ) {
306                                                         char ip_string[60];
307
308                                                         for (int i = 0; i < MAX_PLAYERS; i++) {
309                                                                 if ( MULTI_CONNECTED(Net_players[i]) ) {
310                                                                         psnet_addr_to_string(ip_string, SDL_arraysize(ip_string), &Net_players[i].p_info.addr);
311
312                                                                         if ( !SDL_strcmp(msg+7, ip_string) ) {
313                                                                                 multi_kick_player(i, 0);
314
315                                                                                 break;
316                                                                         }
317                                                                 }
318                                                         }
319                                                 }
320                                         }
321                                 }
322                                 // multi-player tab
323                                 else if (mtype == 'M') {
324                                         if (len >= 6) {
325                                                 if ( !SDL_strncmp(msg+2, "fps ", 4) ) {
326                                                         int fps = SDL_atoi(msg+6);
327                                                         CAP(fps, 10, 120);
328
329                                                         Multi_options_g.std_framecap = fps;
330                                                 }
331                                         }
332                                 }
333                                 // player info tab
334                                 else if (mtype == 'P') {
335                                         if (len >= 7) {
336                                                 if ( !SDL_strncmp(msg+2, "info ", 5) ) {
337                                                         int i;
338
339                                                         for (i = 0; i < MAX_PLAYERS; i++) {
340                                                                 net_player *np = &Net_players[i];
341
342                                                                 if ( MULTI_CONNECTED((*np)) && (Net_player != np) ) {
343                                                                         if ( !SDL_strcmp(msg+7, np->player->callsign) ) {
344                                                                                 Standalone_state.active_player = msg+7;
345                                                                                 std_pinfo_display_player_info(np);
346
347                                                                                 break;
348                                                                         }
349                                                                 }
350                                                         }
351
352                                                         if (i == MAX_PLAYERS) {
353                                                                 Standalone_state.active_player.clear();
354                                                         }
355                                                 }
356                                         }
357                                 }
358                                 // god stuff tab
359                                 else if (mtype == 'G') {
360                                         if (len >= 7) {
361                                                 if ( !SDL_strncmp(msg+2, "smsg ", 5) ) {
362                                                         char txt[256];
363
364                                                         SDL_strlcpy(txt, msg+7, SDL_arraysize(txt));
365
366                                                         if (SDL_strlen(txt) > 0) {
367                                                                 send_game_chat_packet(Net_player, txt, MULTI_MSG_ALL, NULL);
368
369                                                                 std_add_chat_text(txt, MY_NET_PLAYER_NUM, 1);
370                                                         }
371                                                 } else if ( !SDL_strcmp(msg+2, "mrefresh") ) {
372                                                         if (MULTI_IS_TRACKER_GAME) {
373                                                                 cf_delete(MULTI_VALID_MISSION_FILE, CF_TYPE_DATA);
374
375                                                                 multi_update_valid_missions();
376                                                         }
377                                                 }
378                                         }
379                                 }
380                         }
381
382                         break;
383                 }
384
385                 default:
386                         break;
387         }
388
389         return 0;
390 }
391
392 static struct lws_protocols stand_protocols[] = {
393         {
394                 "http-only",
395                 callback_http,
396                 0,
397                 0
398         },
399         {
400                 "standalone",
401                 callback_standalone,
402                 0,
403                 0
404         },
405         // terminator
406         {
407                 NULL,
408                 NULL,
409                 0,
410                 0
411         }
412 };
413
414 static void std_lws_logger(int level, const char *line)
415 {
416         if (level & (LLL_WARN|LLL_ERR)) {
417                 mprintf(("STD: %s", line));
418         } else if (level & LLL_NOTICE) {
419                 nprintf(("lws", "STD: %s", line));
420         }
421 }
422
423
424
425 static void std_add_ws_message(const char *id, const char *val)
426 {
427         std::string msg;
428
429         // if no client, and startup stamp elapsed, then don't add more messages
430         if ( (active_wsi == NULL) && timestamp_elapsed(startup_reset_stamp) ) {
431                 return;
432         }
433
434         msg.assign(id);
435
436         if (val) {
437                 msg.append(val);
438         }
439
440         Standalone_send_buf.push_back(msg);
441 }
442
443 void std_deinit_standalone()
444 {
445         if (stand_context) {
446                 lws_cancel_service(stand_context);
447                 lws_context_destroy(stand_context);
448                 stand_context = NULL;
449         }
450 }
451
452 void std_init_standalone()
453 {
454         struct lws_context_creation_info info;
455
456         if (stand_context) {
457                 return;
458         }
459
460         SDL_zero(info);
461
462         // basic security measure for admin interface
463         //   "1" bind to loopback iface only *default*
464         //   "0" bind to any/all
465         //   any other value should be ip or iface name to bind
466         //   (invalid values will trigger error)
467
468         const char *stand_iface = os_config_read_string("Network", "RestrictStandAdmin", "1");
469
470         if ( !SDL_strcmp(stand_iface, "1") ) {
471                 info.iface = "127.0.0.1";
472         } else if ( !SDL_strcmp(stand_iface, "0") ) {
473                 info.iface = NULL;
474         } else {
475                 info.iface = stand_iface;
476         }
477
478         info.port = Multi_options_g.port;
479
480         info.protocols = stand_protocols;
481
482         info.gid = -1;
483         info.uid = -1;
484
485         lws_set_log_level(LLL_ERR|LLL_WARN|LLL_NOTICE, std_lws_logger);
486
487         stand_context = lws_create_context(&info);
488
489         if (stand_context == NULL) {
490                 Error(LOCATION, "Unable to initialize standalone server!");
491         }
492
493         atexit(std_deinit_standalone);
494
495         // turn off all sound and music
496         Cmdline_freespace_no_sound = 1;
497         Cmdline_freespace_no_music = 1;
498
499         char title[64];
500         SDL_snprintf(title, SDL_arraysize(title), "%s %d.%02d.%02d", XSTR("FreeSpace Standalone", 935), FS_VERSION_MAJOR, FS_VERSION_MINOR, FS_VERSION_BUILD);
501         Standalone_state.title = title;
502
503         // connections > 5 sec after startup should get gui reset
504         startup_reset_stamp = timestamp(5000);
505
506         std_reset_standalone_gui();
507
508         std_multi_update_netgame_info_controls();
509 }
510
511 static void std_update_ping_all()
512 {
513         std::string ping_upd;
514         char ping_str[10];
515
516         for (int i = 0, idx = 0; i < MAX_PLAYERS; i++) {
517                 net_player *np = &Net_players[i];
518
519                 if ( MULTI_CONNECTED((*np)) && (Net_player != np) ) {
520                         if (np->s_info.ping.ping_avg > -1) {
521                                 if (np->s_info.ping.ping_avg >= 1000) {
522                                         SDL_snprintf(ping_str, SDL_arraysize(ping_str), "%s", XSTR("> 1 sec", 914));
523                                 } else {
524                                         SDL_snprintf(ping_str, SDL_arraysize(ping_str), "%d%s", np->s_info.ping.ping_avg, XSTR(" ms", 915));
525                                 }
526                         } else {
527                                 SDL_zero(ping_str);
528                         }
529
530                         // append separator if not first
531                         if (idx++) {
532                                 ping_upd.append(",");
533                         }
534
535                         ping_upd.append(ping_str);
536                 }
537         }
538
539         if ( !ping_upd.empty() ) {
540                 std_add_ws_message("S:ping ", ping_upd.c_str());
541         }
542 }
543
544 static void std_update_connections()
545 {
546         std::string conn_str;
547         char ip_address[60];
548
549         conn_str.reserve(1024);
550
551         for (int i = 0, idx = 0; i < MAX_PLAYERS; i++) {
552                 net_player *np = &Net_players[i];
553
554                 if ( MULTI_CONNECTED((*np)) && (Net_player != np) ) {
555                         // append seperator if not first
556                         if (idx++) {
557                                 conn_str.append(";");
558                         }
559
560                         conn_str.append(np->player->callsign);
561                         conn_str.append(",");
562
563                         psnet_addr_to_string(ip_address, SDL_arraysize(ip_address), &np->p_info.addr);
564                         conn_str.append(ip_address);
565                 }
566         }
567
568         SDL_assert(conn_str.length() < 1024);
569
570         if ( !conn_str.empty() ) {
571                 std_add_ws_message("S:conn ", conn_str.c_str());
572         }
573 }
574
575 void std_do_gui_frame()
576 {
577         // maybe update selected player stats
578         if ( ((Standalone_stats_stamp == -1) || timestamp_elapsed(Standalone_stats_stamp)) && !Standalone_state.active_player.empty() ) {
579                 Standalone_stats_stamp = timestamp(STD_STATS_UPDATE_TIME);
580
581                 for (int i = 0; i < MAX_PLAYERS; i++) {
582                         net_player *np = &Net_players[i];
583
584                         if ( MULTI_CONNECTED((*np)) && (Net_player != np) ) {
585                                 if ( !SDL_strcmp(Standalone_state.active_player.c_str(), np->player->callsign) ) {
586                                         std_pinfo_display_player_info(np);
587
588                                         break;
589                                 }
590                         }
591                 }
592         }
593
594         // maybe update netgame info
595         if ( (Standalone_ng_stamp == -1) || timestamp_elapsed(Standalone_ng_stamp) ) {
596                 Standalone_ng_stamp = timestamp(STD_NG_UPDATE_TIME);
597
598                 std_multi_update_netgame_info_controls();
599         }
600
601         // update connection ping times
602         if ( ((Standalone_ping_stamp == -1) || timestamp_elapsed(Standalone_ping_stamp)) ) {
603                 Standalone_ping_stamp = timestamp(STD_PING_UPDATE_TIME);
604
605                 std_update_ping_all();
606         }
607
608         if ( !Standalone_send_buf.empty() ) {
609                 lws_callback_on_writable_all_protocol(stand_context, &stand_protocols[1]);
610         }
611
612         lws_service(stand_context, 0);
613 }
614
615 void std_debug_set_standalone_state_string(const char *str)
616 {
617         Standalone_state.debug_txt = str;
618
619         std_add_ws_message("D:", str);
620 }
621
622 void std_connect_set_gamename(const char *name)
623 {
624         if (name == NULL) {
625                 // if a permanent name exists, use that instead of the default
626                 if ( SDL_strlen(Multi_options_g.std_pname) ) {
627                         SDL_strlcpy(Netgame.name, Multi_options_g.std_pname, SDL_arraysize(Netgame.name));
628                 } else {
629                         SDL_strlcpy(Netgame.name, XSTR("Standalone Server", 916), SDL_arraysize(Netgame.name));
630                 }
631         } else {
632                 SDL_strlcpy(Netgame.name, name, SDL_arraysize(Netgame.name));
633         }
634
635         std_add_ws_message("S:name ", Netgame.name);
636 }
637
638 int std_connect_set_connect_count()
639 {
640         int count = 0;
641
642         for (int i = 0; i < MAX_PLAYERS; i++) {
643                 if (MULTI_CONNECTED(Net_players[i]) && (Net_player != &Net_players[i]) ) {
644                         count++;
645                 }
646         }
647
648         return count;
649 }
650
651 void std_add_player(net_player *p)
652 {
653         std_update_connections();
654
655         // clear active player, client should reset if needed
656         Standalone_state.active_player.clear();
657
658         // check to see if this guy is the host
659         std_connect_set_host_connect_status();
660 }
661
662 int std_remove_player(net_player *p)
663 {
664         int count;
665
666         std_update_connections();
667
668         // clear active player, client should reset if needed
669         Standalone_state.active_player.clear();
670
671         // update the host connect count
672         std_connect_set_host_connect_status();
673
674         // update the currently connected players
675         count = std_connect_set_connect_count();
676
677         if (count == 0) {
678                 multi_quit_game(PROMPT_NONE);
679                 return 1;
680         }
681
682         return 0;
683 }
684
685 void std_update_player_ping(net_player *p)
686 {
687 }
688
689 void std_pinfo_display_player_info(net_player *p)
690 {
691         char sml_ping[30];
692         std::string pinfo;
693
694         pinfo.reserve(256);
695
696         // ship type
697         pinfo.append(Ship_info[p->p_info.ship_class].name);
698         pinfo.append(";");
699
700         // avg ping time
701         if (p->s_info.ping.ping_avg > 1000) {
702                 SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%s", XSTR("> 1 sec", 914));
703         } else {
704                 SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d%s", p->s_info.ping.ping_avg, XSTR(" ms", 915));
705         }
706
707         pinfo.append(sml_ping);
708         pinfo.append(";");
709
710         scoring_struct *ptr = &p->player->stats;
711
712         // all-time stats
713         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->p_shots_fired);
714         pinfo.append(sml_ping);
715         pinfo.append(",");
716         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->p_shots_hit);
717         pinfo.append(sml_ping);
718         pinfo.append(",");
719         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->p_bonehead_hits);
720         pinfo.append(sml_ping);
721         pinfo.append(",");
722         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->p_shots_fired ? (int)(100.0f * ((float)ptr->p_shots_hit / (float)ptr->p_shots_fired)) : 0);
723         pinfo.append(sml_ping);
724         pinfo.append(",");
725         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->p_shots_fired ? (int)(100.0f * ((float)ptr->p_bonehead_hits / (float)ptr->p_shots_fired)) : 0);
726         pinfo.append(sml_ping);
727         pinfo.append(",");
728         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->s_shots_fired);
729         pinfo.append(sml_ping);
730         pinfo.append(",");
731         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->s_shots_hit);
732         pinfo.append(sml_ping);
733         pinfo.append(",");
734         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->s_bonehead_hits);
735         pinfo.append(sml_ping);
736         pinfo.append(",");
737         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->s_shots_fired ? (int)(100.0f * ((float)ptr->s_shots_hit / (float)ptr->s_shots_fired)) : 0);
738         pinfo.append(sml_ping);
739         pinfo.append(",");
740         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->s_shots_fired ? (int)(100.0f * ((float)ptr->s_bonehead_hits / (float)ptr->s_shots_fired)) : 0);
741         pinfo.append(sml_ping);
742         pinfo.append(",");
743         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->assists);
744         pinfo.append(sml_ping);
745         pinfo.append(";");      // <- end of block
746
747         // mission stats
748         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->mp_shots_fired);
749         pinfo.append(sml_ping);
750         pinfo.append(",");
751         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->mp_shots_hit);
752         pinfo.append(sml_ping);
753         pinfo.append(",");
754         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->mp_bonehead_hits);
755         pinfo.append(sml_ping);
756         pinfo.append(",");
757         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->mp_shots_fired ? (int)(100.0f * ((float)ptr->mp_shots_hit / (float)ptr->mp_shots_fired)) : 0);
758         pinfo.append(sml_ping);
759         pinfo.append(",");
760         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->mp_shots_fired ? (int)(100.0f * ((float)ptr->mp_bonehead_hits / (float)ptr->mp_shots_fired)) : 0);
761         pinfo.append(sml_ping);
762         pinfo.append(",");
763         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->ms_shots_fired);
764         pinfo.append(sml_ping);
765         pinfo.append(",");
766         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->ms_shots_hit);
767         pinfo.append(sml_ping);
768         pinfo.append(",");
769         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->ms_bonehead_hits);
770         pinfo.append(sml_ping);
771         pinfo.append(",");
772         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->ms_shots_fired ? (int)(100.0f * ((float)ptr->ms_shots_hit / (float)ptr->ms_shots_fired)) : 0);
773         pinfo.append(sml_ping);
774         pinfo.append(",");
775         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->ms_shots_fired ? (int)(100.0f * ((float)ptr->ms_bonehead_hits / (float)ptr->ms_shots_fired)) : 0);
776         pinfo.append(sml_ping);
777         pinfo.append(",");
778         SDL_snprintf(sml_ping, SDL_arraysize(sml_ping), "%d", ptr->m_assists);
779         pinfo.append(sml_ping);
780
781         std_add_ws_message("P:info ", pinfo.c_str());
782 }
783
784 void std_add_chat_text(const char *text, int player_index, int add_id)
785 {
786         char id[32];
787         std::string msg;
788
789         if ( (player_index < 0) || (player_index >= MAX_PLAYERS) ) {
790                 return;
791         }
792
793         // format the chat text nicely
794         if (add_id) {
795                 if ( MULTI_STANDALONE(Net_players[player_index]) ) {
796                         SDL_snprintf(id, SDL_arraysize(id), XSTR("<SERVER> %s", 924), "");
797                 } else {
798                         SDL_snprintf(id, SDL_arraysize(id), "%s: ", Net_players[player_index].player->callsign);
799                 }
800
801                 msg.append(id);
802         }
803
804         msg.append(text);
805         msg.append("\n");
806
807         std_add_ws_message("G:mesg ", msg.c_str());
808 }
809
810 void std_reset_timestamps()
811 {
812         // reset the stats update stamp
813         Standalone_stats_stamp = timestamp(STD_STATS_UPDATE_TIME);
814
815         // reset the netgame controls update timestamp
816         Standalone_ng_stamp = timestamp(STD_NG_UPDATE_TIME);
817
818         // reset the ping update stamp
819         Standalone_ping_stamp = timestamp(STD_PING_UPDATE_TIME);
820
821         // reset fps update stamp
822         Standalone_fps_stamp = timestamp(STD_FPS_UPDATE_TIME);
823 }
824
825 void std_add_ban(const char *name)
826 {
827         if ( (name == NULL) || !SDL_strlen(name) ) {
828                 return;
829         }
830
831         if (Standalone_ban_list.size() >= STANDALONE_MAX_BAN) {
832                 return;
833         }
834
835         Standalone_ban_list.push_back(name);
836 }
837
838 int std_player_is_banned(const char *name)
839 {
840         if ( Standalone_ban_list.empty() ) {
841                 return 0;
842         }
843
844         for (size_t i = 0; i < Standalone_ban_list.size(); i++) {
845                 if ( !SDL_strcasecmp(name, Standalone_ban_list[i].c_str()) ) {
846                         return 1;
847                 }
848         }
849
850         return 0;
851 }
852
853 int std_is_host_passwd()
854 {
855         return (SDL_strlen(Multi_options_g.std_passwd) > 0) ? 1 : 0;
856 }
857
858 void std_multi_set_standalone_mission_name(const char *mission_name)
859 {
860         std_add_ws_message("M:name ", mission_name);
861 }
862
863 void std_multi_set_standalone_missiontime(float mission_time)
864 {
865         char txt[80];
866         char timestr[50];
867         fix m_time = fl2f(mission_time);
868
869         // format the time string and set the text
870         game_format_time(m_time, timestr, SDL_arraysize(timestr));
871         SDL_snprintf(txt, SDL_arraysize(txt), "%s  :  %.1f", timestr, mission_time);
872
873         std_add_ws_message("M:time ", txt);
874 }
875
876 void std_multi_update_netgame_info_controls()
877 {
878         char nginfo[50];
879
880         SDL_snprintf(nginfo, SDL_arraysize(nginfo), "%d,%d,%d,%d", Netgame.max_players, Netgame.options.max_observers, Netgame.security, Netgame.respawn);
881
882         std_add_ws_message("M:info ", nginfo);
883 }
884
885 void std_set_standalone_fps(float fps)
886 {
887         if ( (Standalone_fps_stamp == -1) || timestamp_elapsed(Standalone_fps_stamp) ) {
888                 Standalone_fps_stamp = timestamp(STD_FPS_UPDATE_TIME);
889
890                 char rfps[10];
891
892                 SDL_snprintf(rfps, SDL_arraysize(rfps), "%.1f", fps);
893
894                 std_add_ws_message("M:rfps ", rfps);
895         }
896 }
897
898 void std_multi_setup_goal_tree()
899 {
900         std::string mission_goals;
901         std::string primary;
902         std::string secondary;
903         std::string bonus;
904         std::string status;
905
906         for (int i = 0; i < Num_goals; i++) {
907                 switch (Mission_goals[i].satisfied) {
908                         case GOAL_FAILED: {
909                                 status = "f ";
910                                 break;
911                         }
912
913                         case GOAL_COMPLETE: {
914                                 status = "c ";
915                                 break;
916                         }
917
918                         case GOAL_INCOMPLETE:
919                         default: {
920                                 status = "i ";
921                                 break;
922                         }
923                 }
924
925                 switch (Mission_goals[i].type & GOAL_TYPE_MASK) {
926                         case PRIMARY_GOAL: {
927                                 primary.append(status);
928                                 primary.append(Mission_goals[i].name);
929                                 primary.append(",");
930
931                                 break;
932                         }
933
934                         case SECONDARY_GOAL: {
935                                 secondary.append(status);
936                                 secondary.append(Mission_goals[i].name);
937                                 secondary.append(",");
938
939                                 break;
940                         }
941
942                         case BONUS_GOAL: {
943                                 bonus.append(status);
944                                 bonus.append(Mission_goals[i].name);
945                                 bonus.append(",");
946
947                                 break;
948                         }
949
950                         default:
951                                 break;
952                 }
953         }
954
955         if ( primary.empty() ) {
956                 mission_goals.append("i none");
957         } else {
958                 mission_goals.append(primary.substr(0, primary.size()-1));
959         }
960
961         mission_goals.append(";");
962
963         if ( secondary.empty() ) {
964                 mission_goals.append("i none");
965         } else {
966                 mission_goals.append(secondary.substr(0, secondary.size()-1));
967         }
968
969         mission_goals.append(";");
970
971         if ( bonus.empty() ) {
972                 mission_goals.append("i none");
973         } else {
974                 mission_goals.append(bonus.substr(0, bonus.size()-1));
975         }
976
977         std_add_ws_message("M:goal ", mission_goals.c_str());
978 }
979
980 void std_multi_add_goals()
981 {
982         std_multi_setup_goal_tree();
983 }
984
985 void std_multi_update_goals()
986 {
987         std_multi_setup_goal_tree();
988 }
989
990 void std_reset_standalone_gui()
991 {
992         Standalone_send_buf.clear();
993
994         std_add_ws_message("reset", NULL);
995
996         std_add_ws_message("T:", Standalone_state.title.c_str());
997         std_add_ws_message("D:", Standalone_state.debug_txt.c_str());
998
999         std_add_ws_message("S:name ", Netgame.name);
1000         std_add_ws_message("S:pass ", Multi_options_g.std_passwd);
1001
1002         std_update_connections();
1003         std_set_standalone_fps(0.0f);
1004         std_multi_set_standalone_missiontime(0.0f);
1005         std_multi_update_netgame_info_controls();
1006
1007         Standalone_fps_stamp = -1;
1008         Standalone_ng_stamp = -1;
1009         Standalone_ping_stamp = -1;
1010         Standalone_stats_stamp = -1;
1011
1012         Standalone_state.active_player.clear();
1013 }
1014
1015
1016 void std_create_gen_dialog(const char *title)
1017 {
1018         Standalone_state.popup_title = title;
1019
1020         Standalone_state.popup_field1 = "";
1021         Standalone_state.popup_field2 = "";
1022 }
1023
1024 void std_destroy_gen_dialog()
1025 {
1026         std_add_ws_message("popup ", NULL);
1027 }
1028
1029 void std_gen_set_text(const char *str, int field_num)
1030 {
1031         std::string popup_str;
1032
1033         switch (field_num) {
1034                 case 0:
1035                         Standalone_state.popup_title = str;
1036                         break;
1037
1038                 case 1:
1039                         Standalone_state.popup_field1 = str;
1040                         break;
1041
1042                 case 2:
1043                         Standalone_state.popup_field2 = str;
1044                         break;
1045
1046                 default:
1047                         break;
1048         }
1049
1050         popup_str.append(Standalone_state.popup_title);
1051         popup_str.append(";");
1052         popup_str.append(Standalone_state.popup_field1);
1053         popup_str.append(";");
1054         popup_str.append(Standalone_state.popup_field2);
1055
1056         std_add_ws_message("popup ", popup_str.c_str());
1057
1058         // force ws write since do_frame() may not happen until popup is done
1059         lws_callback_on_writable_all_protocol(stand_context, &stand_protocols[1]);
1060         lws_service(stand_context, 0);
1061 }
1062
1063 void std_tracker_notify_login_fail()
1064 {
1065
1066 }
1067
1068 void std_tracker_login()
1069 {
1070         if ( !Multi_options_g.pxo ) {
1071                 return;
1072         }
1073
1074         multi_fs_tracker_init();
1075
1076         if ( !multi_fs_tracker_inited() ) {
1077                 std_tracker_notify_login_fail();
1078                 return;
1079         }
1080
1081         multi_fs_tracker_login_freespace();
1082 }
1083
1084 void std_connect_set_host_connect_status()
1085 {
1086 }
1087
1088 #else
1089
1090 void std_init_standalone(){}
1091 void std_do_gui_frame(){}
1092 void std_debug_set_standalone_state_string(const char *){}
1093 void std_connect_set_gamename(const char *){}
1094 int std_connect_set_connect_count(){return 0;}
1095 void std_add_player(net_player *){}
1096 int std_remove_player(net_player *){return 0;}
1097 void std_update_player_ping(net_player *){}
1098 void std_pinfo_display_player_info(net_player *){}
1099 void std_add_chat_text(const char *, int , int ){}
1100 void std_reset_timestamps(){}
1101 void std_add_ban(const char *){}
1102 int std_player_is_banned(const char *){return 0;}
1103 int std_is_host_passwd(){return 0;}
1104 void std_multi_set_standalone_mission_name(const char *){}
1105 void std_multi_set_standalone_missiontime(float mission_time){}
1106 void std_multi_update_netgame_info_controls(){}
1107 void std_set_standalone_fps(float ){}
1108 void std_multi_setup_goal_tree(){}
1109 void std_multi_add_goals(){}
1110 void std_multi_update_goals(){}
1111 void std_reset_standalone_gui(){}
1112 void std_create_gen_dialog(const char *){}
1113 void std_destroy_gen_dialog(){}
1114 void std_gen_set_text(const char *, int ){}
1115 void std_tracker_notify_login_fail(){}
1116 void std_tracker_login(){}
1117 void std_connect_set_host_connect_status(){}
1118
1119 #endif