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