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