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