1 //
2 // Copyright(C) 2005-2014 Simon Howard
3 //
4 // This program is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU General Public License
6 // as published by the Free Software Foundation; either version 2
7 // of the License, or (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 
19 #include "doomtype.h"
20 
21 #include "textscreen.h"
22 
23 #include "d_iwad.h"
24 #include "m_config.h"
25 #include "m_misc.h"
26 #include "doom/d_englsh.h"
27 #include "m_controls.h"
28 
29 #include "multiplayer.h"
30 #include "mode.h"
31 #include "execute.h"
32 
33 #include "net_io.h"
34 #include "net_query.h"
35 
36 #include "net_petname.h"
37 
38 #define MULTI_START_HELP_URL "https://www.chocolate-doom.org/setup-multi-start"
39 #define MULTI_JOIN_HELP_URL "https://www.chocolate-doom.org/setup-multi-join"
40 #define MULTI_CONFIG_HELP_URL "https://www.chocolate-doom.org/setup-multi-config"
41 #define LEVEL_WARP_HELP_URL "https://www.chocolate-doom.org/setup-level-warp"
42 
43 #define NUM_WADS 10
44 #define NUM_EXTRA_PARAMS 10
45 
46 typedef enum
47 {
48     WARP_ExMy,
49     WARP_MAPxy,
50 } warptype_t;
51 
52 // Fallback IWADs to use if no IWADs are detected.
53 
54 static const iwad_t fallback_iwads[] = {
55     { "doom.wad",     doom,     registered,  "Doom" },
56     { "heretic.wad",  heretic,  retail,      "Heretic" },
57     { "hexen.wad",    hexen,    commercial,  "Hexen" },
58     { "strife1.wad",  strife,   commercial,  "Strife" },
59 };
60 
61 // Array of IWADs found to be installed
62 
63 static const iwad_t **found_iwads;
64 static const char **iwad_labels;
65 
66 // Index of the currently selected IWAD
67 
68 static int found_iwad_selected = -1;
69 
70 // Filename to pass to '-iwad'.
71 
72 static const char *iwadfile;
73 
74 static const char *wad_extensions[] = { "wad", "lmp", "deh", NULL };
75 
76 static const char *doom_skills[] =
77 {
78     "I'm too young to die.", "Hey, not too rough.", "Hurt me plenty.",
79     "Ultra-Violence.", "NIGHTMARE!",
80 };
81 
82 static const char *chex_skills[] =
83 {
84     "Easy does it", "Not so sticky", "Gobs of goo", "Extreme ooze",
85     "SUPER SLIMEY!"
86 };
87 
88 static const char *heretic_skills[] =
89 {
90     "Thou needeth a wet-nurse", "Yellowbellies-R-us", "Bringest them oneth",
91     "Thou art a smite-meister", "Black plague possesses thee"
92 };
93 
94 static const char *hexen_fighter_skills[] =
95 {
96     "Squire", "Knight", "Warrior", "Berserker", "Titan"
97 };
98 
99 static const char *hexen_cleric_skills[] =
100 {
101     "Altar boy", "Acolyte", "Priest", "Cardinal", "Pope"
102 };
103 
104 static const char *hexen_mage_skills[] =
105 {
106     "Apprentice", "Enchanter", "Sorceror", "Warlock", "Archimage"
107 };
108 
109 static const char *strife_skills[] =
110 {
111     "Training", "Rookie", "Veteran", "Elite", "Bloodbath"
112 };
113 
114 static const char *character_classes[] = { "Fighter", "Cleric", "Mage" };
115 
116 static const char *gamemodes[] = { "Co-operative", "Deathmatch", "Deathmatch 2.0", "Deathmatch 3.0" };
117 
118 static const char *strife_gamemodes[] =
119 {
120     "Normal deathmatch",
121     "Items respawn", // (altdeath)
122 };
123 
124 static char *net_player_name;
125 static char *chat_macros[10];
126 
127 static char *wads[NUM_WADS];
128 static char *extra_params[NUM_EXTRA_PARAMS];
129 static int character_class = 0;
130 static int skill = 2;
131 static int nomonsters = 0;
132 static int deathmatch = 0;
133 static int strife_altdeath = 0;
134 static int fast = 0;
135 static int respawn = 0;
136 static int udpport = 2342;
137 static int timer = 0;
138 static int privateserver = 0;
139 
140 static txt_dropdown_list_t *skillbutton;
141 static txt_button_t *warpbutton;
142 static warptype_t warptype = WARP_MAPxy;
143 static int warpepisode = 1;
144 static int warpmap = 1;
145 
146 // Address to connect to when joining a game
147 
148 static char *connect_address = NULL;
149 
150 static txt_window_t *query_window;
151 static int query_servers_found;
152 
153 // Find an IWAD from its description
154 
GetCurrentIWAD(void)155 static const iwad_t *GetCurrentIWAD(void)
156 {
157     return found_iwads[found_iwad_selected];
158 }
159 
160 // Is the currently selected IWAD the Chex Quest chex.wad?
161 
IsChexQuest(const iwad_t * iwad)162 static boolean IsChexQuest(const iwad_t *iwad)
163 {
164     return !strcmp(iwad->name, "chex.wad");
165 }
166 
AddWADs(execute_context_t * exec)167 static void AddWADs(execute_context_t *exec)
168 {
169     int have_wads = 0;
170     int i;
171 
172     for (i=0; i<NUM_WADS; ++i)
173     {
174         if (wads[i] != NULL && strlen(wads[i]) > 0)
175         {
176             if (!have_wads)
177             {
178                 AddCmdLineParameter(exec, "-file");
179             }
180 
181             AddCmdLineParameter(exec, "\"%s\"", wads[i]);
182         }
183     }
184 }
185 
AddExtraParameters(execute_context_t * exec)186 static void AddExtraParameters(execute_context_t *exec)
187 {
188     int i;
189 
190     for (i=0; i<NUM_EXTRA_PARAMS; ++i)
191     {
192         if (extra_params[i] != NULL && strlen(extra_params[i]) > 0)
193         {
194             AddCmdLineParameter(exec, "%s", extra_params[i]);
195         }
196     }
197 }
198 
AddIWADParameter(execute_context_t * exec)199 static void AddIWADParameter(execute_context_t *exec)
200 {
201     if (iwadfile != NULL)
202     {
203         AddCmdLineParameter(exec, "-iwad %s", iwadfile);
204     }
205 }
206 
207 // Callback function invoked to launch the game.
208 // This is used when starting a server and also when starting a
209 // single player game via the "warp" menu.
210 
StartGame(int multiplayer)211 static void StartGame(int multiplayer)
212 {
213     execute_context_t *exec;
214 
215     exec = NewExecuteContext();
216 
217     // Extra parameters come first, before all others; this way,
218     // they can override any of the options set in the dialog.
219 
220     AddExtraParameters(exec);
221 
222     AddIWADParameter(exec);
223     AddCmdLineParameter(exec, "-skill %i", skill + 1);
224 
225     if (gamemission == hexen)
226     {
227         AddCmdLineParameter(exec, "-class %i", character_class);
228     }
229 
230     if (nomonsters)
231     {
232         AddCmdLineParameter(exec, "-nomonsters");
233     }
234 
235     if (fast)
236     {
237         AddCmdLineParameter(exec, "-fast");
238     }
239 
240     if (respawn)
241     {
242         AddCmdLineParameter(exec, "-respawn");
243     }
244 
245     if (warptype == WARP_ExMy)
246     {
247         // TODO: select IWAD based on warp type
248         AddCmdLineParameter(exec, "-warp %i %i", warpepisode, warpmap);
249     }
250     else if (warptype == WARP_MAPxy)
251     {
252         AddCmdLineParameter(exec, "-warp %i", warpmap);
253     }
254 
255     // Multiplayer-specific options:
256 
257     if (multiplayer)
258     {
259         AddCmdLineParameter(exec, "-server");
260         AddCmdLineParameter(exec, "-port %i", udpport);
261 
262         if (deathmatch == 1)
263         {
264             AddCmdLineParameter(exec, "-deathmatch");
265         }
266         else if (deathmatch == 2 || strife_altdeath != 0)
267         {
268             AddCmdLineParameter(exec, "-altdeath");
269         }
270         else if (deathmatch == 3) // AX: this is a Crispy-specific change
271         {
272             AddCmdLineParameter(exec, "-dm3");
273         }
274 
275         if (timer > 0)
276         {
277             AddCmdLineParameter(exec, "-timer %i", timer);
278         }
279 
280         if (privateserver)
281         {
282             AddCmdLineParameter(exec, "-privateserver");
283         }
284     }
285 
286     AddWADs(exec);
287 
288     TXT_Shutdown();
289 
290     M_SaveDefaults();
291     PassThroughArguments(exec);
292 
293     ExecuteDoom(exec);
294 
295     exit(0);
296 }
297 
StartServerGame(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (unused))298 static void StartServerGame(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused))
299 {
300     StartGame(1);
301 }
302 
StartSinglePlayerGame(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (unused))303 static void StartSinglePlayerGame(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused))
304 {
305     StartGame(0);
306 }
307 
UpdateWarpButton(void)308 static void UpdateWarpButton(void)
309 {
310     char buf[10];
311 
312     if (warptype == WARP_ExMy)
313     {
314         M_snprintf(buf, sizeof(buf), "E%iM%i", warpepisode, warpmap);
315     }
316     else if (warptype == WARP_MAPxy)
317     {
318         M_snprintf(buf, sizeof(buf), "MAP%02i", warpmap);
319     }
320 
321     TXT_SetButtonLabel(warpbutton, buf);
322 }
323 
UpdateSkillButton(void)324 static void UpdateSkillButton(void)
325 {
326     const iwad_t *iwad = GetCurrentIWAD();
327 
328     if (IsChexQuest(iwad))
329     {
330         skillbutton->values = chex_skills;
331     }
332     else switch(gamemission)
333     {
334         default:
335         case doom:
336             skillbutton->values = doom_skills;
337             break;
338 
339         case heretic:
340             skillbutton->values = heretic_skills;
341             break;
342 
343         case hexen:
344             if (character_class == 0)
345             {
346                 skillbutton->values = hexen_fighter_skills;
347             }
348             else if (character_class == 1)
349             {
350                 skillbutton->values = hexen_cleric_skills;
351             }
352             else
353             {
354                 skillbutton->values = hexen_mage_skills;
355             }
356             break;
357 
358         case strife:
359             skillbutton->values = strife_skills;
360             break;
361     }
362 }
363 
SetExMyWarp(TXT_UNCAST_ARG (widget),void * val)364 static void SetExMyWarp(TXT_UNCAST_ARG(widget), void *val)
365 {
366     int l;
367 
368     l = (intptr_t) val;
369 
370     warpepisode = l / 10;
371     warpmap = l % 10;
372 
373     UpdateWarpButton();
374 }
375 
SetMAPxyWarp(TXT_UNCAST_ARG (widget),void * val)376 static void SetMAPxyWarp(TXT_UNCAST_ARG(widget), void *val)
377 {
378     int l;
379 
380     l = (intptr_t) val;
381 
382     warpmap = l;
383 
384     UpdateWarpButton();
385 }
386 
CloseLevelSelectDialog(TXT_UNCAST_ARG (button),TXT_UNCAST_ARG (window))387 static void CloseLevelSelectDialog(TXT_UNCAST_ARG(button), TXT_UNCAST_ARG(window))
388 {
389     TXT_CAST_ARG(txt_window_t, window);
390 
391     TXT_CloseWindow(window);
392 }
393 
LevelSelectDialog(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (user_data))394 static void LevelSelectDialog(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(user_data))
395 {
396     txt_window_t *window;
397     txt_button_t *button;
398     const iwad_t *iwad;
399     char buf[10];
400     int episodes;
401     intptr_t x, y;
402     intptr_t l;
403     int i;
404 
405     window = TXT_NewWindow("Select level");
406     iwad = GetCurrentIWAD();
407 
408     if (warptype == WARP_ExMy)
409     {
410         episodes = D_GetNumEpisodes(iwad->mission, iwad->mode);
411         TXT_SetTableColumns(window, episodes);
412 
413         // ExMy levels
414 
415         for (y=1; y<10; ++y)
416         {
417             for (x=1; x<=episodes; ++x)
418             {
419                 if (IsChexQuest(iwad) && (x > 1 || y > 5))
420                 {
421                     continue;
422                 }
423 
424                 if (!D_ValidEpisodeMap(iwad->mission, iwad->mode, x, y))
425                 {
426                     TXT_AddWidget(window, NULL);
427                     continue;
428                 }
429 
430                 M_snprintf(buf, sizeof(buf),
431                            " E%" PRIiPTR "M%" PRIiPTR " ", x, y);
432                 button = TXT_NewButton(buf);
433                 TXT_SignalConnect(button, "pressed",
434                                   SetExMyWarp, (void *) (x * 10 + y));
435                 TXT_SignalConnect(button, "pressed",
436                                   CloseLevelSelectDialog, window);
437                 TXT_AddWidget(window, button);
438 
439                 if (warpepisode == x && warpmap == y)
440                 {
441                     TXT_SelectWidget(window, button);
442                 }
443             }
444         }
445     }
446     else
447     {
448         TXT_SetTableColumns(window, 6);
449 
450         for (i=0; i<60; ++i)
451         {
452             x = i % 6;
453             y = i / 6;
454 
455             l = x * 10 + y + 1;
456 
457             if (!D_ValidEpisodeMap(iwad->mission, iwad->mode, 1, l))
458             {
459                 TXT_AddWidget(window, NULL);
460                 continue;
461             }
462 
463             M_snprintf(buf, sizeof(buf), " MAP%02" PRIiPTR " ", l);
464             button = TXT_NewButton(buf);
465             TXT_SignalConnect(button, "pressed",
466                               SetMAPxyWarp, (void *) l);
467             TXT_SignalConnect(button, "pressed",
468                               CloseLevelSelectDialog, window);
469             TXT_AddWidget(window, button);
470 
471             if (warpmap == l)
472             {
473                 TXT_SelectWidget(window, button);
474             }
475         }
476     }
477 }
478 
IWADSelected(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (unused))479 static void IWADSelected(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused))
480 {
481     const iwad_t *iwad;
482 
483     // Find the iwad_t selected
484 
485     iwad = GetCurrentIWAD();
486 
487     // Update iwadfile
488 
489     iwadfile = iwad->name;
490 }
491 
492 // Called when the IWAD button is changed, to update warptype.
493 
UpdateWarpType(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (unused))494 static void UpdateWarpType(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused))
495 {
496     warptype_t new_warptype;
497     const iwad_t *iwad;
498 
499     // Get the selected IWAD
500 
501     iwad = GetCurrentIWAD();
502 
503     // Find the new warp type
504 
505     if (D_IsEpisodeMap(iwad->mission))
506     {
507         new_warptype = WARP_ExMy;
508     }
509     else
510     {
511         new_warptype = WARP_MAPxy;
512     }
513 
514     // Reset to E1M1 / MAP01 when the warp type is changed.
515 
516     if (new_warptype != warptype)
517     {
518         warpepisode = 1;
519         warpmap = 1;
520     }
521 
522     warptype = new_warptype;
523 
524     UpdateWarpButton();
525     UpdateSkillButton();
526 }
527 
528 // Get an IWAD list with a default fallback IWAD that is appropriate
529 // for the game we are configuring (matches gamemission global variable).
530 
GetFallbackIwadList(void)531 static const iwad_t **GetFallbackIwadList(void)
532 {
533     static const iwad_t *fallback_iwad_list[2];
534     unsigned int i;
535 
536     // Default to use if we don't find something better.
537 
538     fallback_iwad_list[0] = &fallback_iwads[0];
539     fallback_iwad_list[1] = NULL;
540 
541     for (i = 0; i < arrlen(fallback_iwads); ++i)
542     {
543         if (gamemission == fallback_iwads[i].mission)
544         {
545             fallback_iwad_list[0] = &fallback_iwads[i];
546             break;
547         }
548     }
549 
550     return fallback_iwad_list;
551 }
552 
IWADSelector(void)553 static txt_widget_t *IWADSelector(void)
554 {
555     txt_dropdown_list_t *dropdown;
556     txt_widget_t *result;
557     int num_iwads;
558     unsigned int i;
559 
560     // Find out what WADs are installed
561 
562     found_iwads = GetIwads();
563 
564     // Build a list of the descriptions for all installed IWADs
565 
566     num_iwads = 0;
567 
568     for (i=0; found_iwads[i] != NULL; ++i)
569     {
570          ++num_iwads;
571     }
572 
573     iwad_labels = malloc(sizeof(*iwad_labels) * num_iwads);
574 
575     for (i=0; i < num_iwads; ++i)
576     {
577         iwad_labels[i] = found_iwads[i]->description;
578     }
579 
580     // If no IWADs are found, provide Doom 2 as an option, but
581     // we're probably screwed.
582 
583     if (num_iwads == 0)
584     {
585         found_iwads = GetFallbackIwadList();
586         num_iwads = 1;
587     }
588 
589     // Build a dropdown list of IWADs
590 
591     if (num_iwads < 2)
592     {
593         // We have only one IWAD.  Show as a label.
594 
595         result = (txt_widget_t *) TXT_NewLabel(found_iwads[0]->description);
596     }
597     else
598     {
599         // Dropdown list allowing IWAD to be selected.
600 
601         dropdown = TXT_NewDropdownList(&found_iwad_selected,
602                                        iwad_labels, num_iwads);
603 
604         TXT_SignalConnect(dropdown, "changed", IWADSelected, NULL);
605 
606         result = (txt_widget_t *) dropdown;
607     }
608 
609     // The first time the dialog is opened, found_iwad_selected=-1,
610     // so select the first IWAD in the list. Don't lose the setting
611     // if we close and reopen the dialog.
612 
613     if (found_iwad_selected < 0 || found_iwad_selected >= num_iwads)
614     {
615         found_iwad_selected = 0;
616     }
617 
618     IWADSelected(NULL, NULL);
619 
620     return result;
621 }
622 
623 // Create the window action button to start the game.  This invokes
624 // a different callback depending on whether to start a multiplayer
625 // or single player game.
626 
StartGameAction(int multiplayer)627 static txt_window_action_t *StartGameAction(int multiplayer)
628 {
629     txt_window_action_t *action;
630     TxtWidgetSignalFunc callback;
631 
632     action = TXT_NewWindowAction(KEY_F10, "Start");
633 
634     if (multiplayer)
635     {
636         callback = StartServerGame;
637     }
638     else
639     {
640         callback = StartSinglePlayerGame;
641     }
642 
643     TXT_SignalConnect(action, "pressed", callback, NULL);
644 
645     return action;
646 }
647 
OpenWadsWindow(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (user_data))648 static void OpenWadsWindow(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(user_data))
649 {
650     txt_window_t *window;
651     int i;
652 
653     window = TXT_NewWindow("Add WADs");
654 
655     for (i=0; i<NUM_WADS; ++i)
656     {
657         TXT_AddWidget(window,
658                       TXT_NewFileSelector(&wads[i], 60, "Select a WAD file",
659                                           wad_extensions));
660     }
661 }
662 
OpenExtraParamsWindow(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (user_data))663 static void OpenExtraParamsWindow(TXT_UNCAST_ARG(widget),
664                                   TXT_UNCAST_ARG(user_data))
665 {
666     txt_window_t *window;
667     int i;
668 
669     window = TXT_NewWindow("Extra command line parameters");
670 
671     for (i=0; i<NUM_EXTRA_PARAMS; ++i)
672     {
673         TXT_AddWidget(window, TXT_NewInputBox(&extra_params[i], 70));
674     }
675 }
676 
WadWindowAction(void)677 static txt_window_action_t *WadWindowAction(void)
678 {
679     txt_window_action_t *action;
680 
681     action = TXT_NewWindowAction('w', "Add WADs");
682     TXT_SignalConnect(action, "pressed", OpenWadsWindow, NULL);
683 
684     return action;
685 }
686 
GameTypeDropdown(void)687 static txt_dropdown_list_t *GameTypeDropdown(void)
688 {
689     switch (gamemission)
690     {
691         case doom:
692         default:
693             return TXT_NewDropdownList(&deathmatch, gamemodes, 4);
694 
695         // Heretic and Hexen don't support Deathmatch II:
696 
697         case heretic:
698         case hexen:
699             return TXT_NewDropdownList(&deathmatch, gamemodes, 2);
700 
701         // Strife supports both deathmatch modes, but doesn't support
702         // multiplayer co-op. Use a different variable to indicate whether
703         // to use altdeath or not.
704 
705         case strife:
706             return TXT_NewDropdownList(&strife_altdeath, strife_gamemodes, 2);
707     }
708 }
709 
710 // "Start game" menu.  This is used for the start server window
711 // and the single player warp menu.  The parameters specify
712 // the window title and whether to display multiplayer options.
713 
StartGameMenu(const char * window_title,int multiplayer)714 static void StartGameMenu(const char *window_title, int multiplayer)
715 {
716     txt_window_t *window;
717     txt_widget_t *iwad_selector;
718 
719     window = TXT_NewWindow(window_title);
720     TXT_SetTableColumns(window, 2);
721     TXT_SetColumnWidths(window, 12, 6);
722 
723     if (multiplayer)
724     {
725         TXT_SetWindowHelpURL(window, MULTI_START_HELP_URL);
726     }
727     else
728     {
729         TXT_SetWindowHelpURL(window, LEVEL_WARP_HELP_URL);
730     }
731 
732     TXT_SetWindowAction(window, TXT_HORIZ_CENTER, WadWindowAction());
733     TXT_SetWindowAction(window, TXT_HORIZ_RIGHT, StartGameAction(multiplayer));
734 
735     TXT_AddWidgets(window,
736                    TXT_NewLabel("Game"),
737                    iwad_selector = IWADSelector(),
738                    NULL);
739 
740     if (gamemission == hexen)
741     {
742         txt_dropdown_list_t *cc_dropdown;
743         TXT_AddWidgets(window,
744                        TXT_NewLabel("Character class "),
745                        cc_dropdown = TXT_NewDropdownList(&character_class,
746                                                          character_classes, 3),
747                        NULL);
748 
749         // Update skill level dropdown when the character class is changed:
750 
751         TXT_SignalConnect(cc_dropdown, "changed", UpdateWarpType, NULL);
752     }
753 
754     TXT_AddWidgets(window,
755                    TXT_NewLabel("Skill"),
756                    skillbutton = TXT_NewDropdownList(&skill, doom_skills, 5),
757                    TXT_NewLabel("Level warp"),
758                    warpbutton = TXT_NewButton2("?", LevelSelectDialog, NULL),
759                    NULL);
760 
761     if (multiplayer)
762     {
763         TXT_AddWidgets(window,
764                TXT_NewLabel("Game type"),
765                GameTypeDropdown(),
766                TXT_NewLabel("Time limit"),
767                TXT_NewHorizBox(TXT_NewIntInputBox(&timer, 2),
768                                TXT_NewLabel("minutes"),
769                                NULL),
770                NULL);
771     }
772 
773     TXT_AddWidgets(window,
774                    TXT_NewSeparator("Monster options"),
775                    TXT_NewInvertedCheckBox("Monsters enabled", &nomonsters),
776                    TXT_TABLE_OVERFLOW_RIGHT,
777                    TXT_NewCheckBox("Fast monsters", &fast),
778                    TXT_TABLE_OVERFLOW_RIGHT,
779                    TXT_NewCheckBox("Respawning monsters", &respawn),
780                    TXT_TABLE_OVERFLOW_RIGHT,
781                    NULL);
782 
783     if (multiplayer)
784     {
785         TXT_AddWidgets(window,
786                        TXT_NewSeparator("Advanced"),
787                        TXT_NewLabel("UDP port"),
788                        TXT_NewIntInputBox(&udpport, 5),
789                        TXT_NewInvertedCheckBox("Register with master server",
790                                                &privateserver),
791                        TXT_TABLE_OVERFLOW_RIGHT,
792                        NULL);
793     }
794 
795     TXT_AddWidgets(window,
796                    TXT_NewButton2("Add extra parameters...",
797                                   OpenExtraParamsWindow, NULL),
798                    TXT_TABLE_OVERFLOW_RIGHT,
799                    NULL);
800 
801     TXT_SignalConnect(iwad_selector, "changed", UpdateWarpType, NULL);
802 
803     UpdateWarpType(NULL, NULL);
804     UpdateWarpButton();
805 }
806 
StartMultiGame(TXT_UNCAST_ARG (widget),void * user_data)807 void StartMultiGame(TXT_UNCAST_ARG(widget), void *user_data)
808 {
809     StartGameMenu("Start multiplayer game", 1);
810 }
811 
WarpMenu(TXT_UNCAST_ARG (widget),void * user_data)812 void WarpMenu(TXT_UNCAST_ARG(widget), void *user_data)
813 {
814     StartGameMenu("Level Warp", 0);
815 }
816 
DoJoinGame(void * unused1,void * unused2)817 static void DoJoinGame(void *unused1, void *unused2)
818 {
819     execute_context_t *exec;
820 
821     if (connect_address == NULL || strlen(connect_address) <= 0)
822     {
823         TXT_MessageBox(NULL, "Please enter a server address\n"
824                              "to connect to.");
825         return;
826     }
827 
828     exec = NewExecuteContext();
829 
830     AddCmdLineParameter(exec, "-connect %s", connect_address);
831 
832     if (gamemission == hexen)
833     {
834         AddCmdLineParameter(exec, "-class %i", character_class);
835     }
836 
837     // Extra parameters come first, so that they can be used to override
838     // the other parameters.
839 
840     AddExtraParameters(exec);
841     AddIWADParameter(exec);
842     AddWADs(exec);
843 
844     TXT_Shutdown();
845 
846     M_SaveDefaults();
847 
848     PassThroughArguments(exec);
849 
850     ExecuteDoom(exec);
851 
852     exit(0);
853 }
854 
JoinGameAction(void)855 static txt_window_action_t *JoinGameAction(void)
856 {
857     txt_window_action_t *action;
858 
859     action = TXT_NewWindowAction(KEY_F10, "Connect");
860     TXT_SignalConnect(action, "pressed", DoJoinGame, NULL);
861 
862     return action;
863 }
864 
SelectQueryAddress(TXT_UNCAST_ARG (button),TXT_UNCAST_ARG (querydata))865 static void SelectQueryAddress(TXT_UNCAST_ARG(button),
866                                TXT_UNCAST_ARG(querydata))
867 {
868     TXT_CAST_ARG(txt_button_t, button);
869     TXT_CAST_ARG(net_querydata_t, querydata);
870     int i;
871 
872     if (querydata->server_state != 0)
873     {
874         TXT_MessageBox("Cannot connect to server",
875                        "Gameplay is already in progress\n"
876                        "on this server.");
877         return;
878     }
879 
880     // Set address to connect to:
881 
882     free(connect_address);
883     connect_address = M_StringDuplicate(button->label);
884 
885     // Auto-choose IWAD if there is already a player connected.
886 
887     if (querydata->num_players > 0)
888     {
889         for (i = 0; found_iwads[i] != NULL; ++i)
890         {
891             if (found_iwads[i]->mode == querydata->gamemode
892              && found_iwads[i]->mission == querydata->gamemission)
893             {
894                 found_iwad_selected = i;
895                 iwadfile = found_iwads[i]->name;
896                 break;
897             }
898         }
899 
900         if (found_iwads[i] == NULL)
901         {
902             TXT_MessageBox(NULL,
903                            "The game on this server seems to be:\n"
904                            "\n"
905                            "   %s\n"
906                            "\n"
907                            "but the IWAD file %s is not found!\n"
908                            "Without the required IWAD file, it may not be\n"
909                            "possible to join this game.",
910                            D_SuggestGameName(querydata->gamemission,
911                                              querydata->gamemode),
912                            D_SuggestIWADName(querydata->gamemission,
913                                              querydata->gamemode));
914         }
915     }
916 
917     // Finished with search.
918 
919     TXT_CloseWindow(query_window);
920 }
921 
QueryResponseCallback(net_addr_t * addr,net_querydata_t * querydata,unsigned int ping_time,TXT_UNCAST_ARG (results_table))922 static void QueryResponseCallback(net_addr_t *addr,
923                                   net_querydata_t *querydata,
924                                   unsigned int ping_time,
925                                   TXT_UNCAST_ARG(results_table))
926 {
927     TXT_CAST_ARG(txt_table_t, results_table);
928     char ping_time_str[16];
929     char description[47];
930 
931     // When we connect we'll have to negotiate a common protocol that we
932     // can agree upon between the client and server. If we can't then we
933     // won't be able to connect, so it's pointless to include it in the
934     // results list. If protocol==NET_PROTOCOL_UNKNOWN then this may be
935     // an old, pre-3.0 Chocolate Doom server that doesn't support the new
936     // protocol negotiation mechanism, or it may be an incompatible fork.
937     if (querydata->protocol == NET_PROTOCOL_UNKNOWN)
938     {
939         return;
940     }
941 
942     M_snprintf(ping_time_str, sizeof(ping_time_str), "%ims", ping_time);
943 
944     // Build description from server name field. Because there is limited
945     // space, we only include the player count if there are already players
946     // connected to the server.
947     if (querydata->num_players > 0)
948     {
949         M_snprintf(description, sizeof(description), "(%d/%d) ",
950                    querydata->num_players, querydata->max_players);
951     }
952     else
953     {
954         M_StringCopy(description, "", sizeof(description));
955     }
956 
957     M_StringConcat(description, querydata->description, sizeof(description));
958 
959     TXT_AddWidgets(results_table,
960                    TXT_NewLabel(ping_time_str),
961                    TXT_NewButton2(NET_AddrToString(addr),
962                                   SelectQueryAddress, querydata),
963                    TXT_NewLabel(description),
964                    NULL);
965 
966     ++query_servers_found;
967 }
968 
QueryPeriodicCallback(TXT_UNCAST_ARG (results_table))969 static void QueryPeriodicCallback(TXT_UNCAST_ARG(results_table))
970 {
971     TXT_CAST_ARG(txt_table_t, results_table);
972 
973     if (!NET_Query_Poll(QueryResponseCallback, results_table))
974     {
975         TXT_SetPeriodicCallback(NULL, NULL, 0);
976 
977         if (query_servers_found == 0)
978         {
979             TXT_AddWidgets(results_table,
980                 TXT_TABLE_EMPTY,
981                 TXT_NewLabel("No compatible servers found."),
982                 NULL
983             );
984         }
985     }
986 }
987 
QueryWindowClosed(TXT_UNCAST_ARG (window),void * unused)988 static void QueryWindowClosed(TXT_UNCAST_ARG(window), void *unused)
989 {
990     TXT_SetPeriodicCallback(NULL, NULL, 0);
991 }
992 
ServerQueryWindow(const char * title)993 static void ServerQueryWindow(const char *title)
994 {
995     txt_table_t *results_table;
996 
997     query_servers_found = 0;
998 
999     query_window = TXT_NewWindow(title);
1000 
1001     TXT_AddWidget(query_window,
1002                   TXT_NewScrollPane(70, 10,
1003                                     results_table = TXT_NewTable(3)));
1004 
1005     TXT_SetColumnWidths(results_table, 7, 22, 40);
1006     TXT_SetPeriodicCallback(QueryPeriodicCallback, results_table, 1);
1007 
1008     TXT_SignalConnect(query_window, "closed", QueryWindowClosed, NULL);
1009 }
1010 
FindInternetServer(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (user_data))1011 static void FindInternetServer(TXT_UNCAST_ARG(widget),
1012                                TXT_UNCAST_ARG(user_data))
1013 {
1014     NET_StartMasterQuery();
1015     ServerQueryWindow("Find Internet server");
1016 }
1017 
FindLANServer(TXT_UNCAST_ARG (widget),TXT_UNCAST_ARG (user_data))1018 static void FindLANServer(TXT_UNCAST_ARG(widget),
1019                           TXT_UNCAST_ARG(user_data))
1020 {
1021     NET_StartLANQuery();
1022     ServerQueryWindow("Find LAN server");
1023 }
1024 
JoinMultiGame(TXT_UNCAST_ARG (widget),void * user_data)1025 void JoinMultiGame(TXT_UNCAST_ARG(widget), void *user_data)
1026 {
1027     txt_window_t *window;
1028     txt_inputbox_t *address_box;
1029 
1030     window = TXT_NewWindow("Join multiplayer game");
1031     TXT_SetTableColumns(window, 2);
1032     TXT_SetColumnWidths(window, 12, 12);
1033 
1034     TXT_SetWindowHelpURL(window, MULTI_JOIN_HELP_URL);
1035 
1036     TXT_AddWidgets(window,
1037                    TXT_NewLabel("Game"),
1038                    IWADSelector(),
1039                    NULL);
1040 
1041     if (gamemission == hexen)
1042     {
1043         TXT_AddWidgets(window,
1044                        TXT_NewLabel("Character class "),
1045                        TXT_NewDropdownList(&character_class,
1046                                            character_classes, 3),
1047                        NULL);
1048     }
1049 
1050     TXT_AddWidgets(window,
1051                    TXT_NewSeparator("Server"),
1052                    TXT_NewLabel("Connect to address: "),
1053                    address_box = TXT_NewInputBox(&connect_address, 30),
1054 
1055                    TXT_NewButton2("Find server on Internet...",
1056                                   FindInternetServer, NULL),
1057                    TXT_TABLE_OVERFLOW_RIGHT,
1058                    TXT_NewButton2("Find server on local network...",
1059                                   FindLANServer, NULL),
1060                    TXT_TABLE_OVERFLOW_RIGHT,
1061                    TXT_NewStrut(0, 1),
1062                    TXT_TABLE_OVERFLOW_RIGHT,
1063                    TXT_NewButton2("Add extra parameters...",
1064                                   OpenExtraParamsWindow, NULL),
1065                    NULL);
1066 
1067     TXT_SelectWidget(window, address_box);
1068 
1069     TXT_SetWindowAction(window, TXT_HORIZ_CENTER, WadWindowAction());
1070     TXT_SetWindowAction(window, TXT_HORIZ_RIGHT, JoinGameAction());
1071 }
1072 
SetChatMacroDefaults(void)1073 void SetChatMacroDefaults(void)
1074 {
1075     int i;
1076     const char *const defaults[] =
1077     {
1078         HUSTR_CHATMACRO0,
1079         HUSTR_CHATMACRO1,
1080         HUSTR_CHATMACRO2,
1081         HUSTR_CHATMACRO3,
1082         HUSTR_CHATMACRO4,
1083         HUSTR_CHATMACRO5,
1084         HUSTR_CHATMACRO6,
1085         HUSTR_CHATMACRO7,
1086         HUSTR_CHATMACRO8,
1087         HUSTR_CHATMACRO9,
1088     };
1089 
1090     // If the chat macros have not been set, initialize with defaults.
1091 
1092     for (i=0; i<10; ++i)
1093     {
1094         if (chat_macros[i] == NULL)
1095         {
1096             chat_macros[i] = M_StringDuplicate(defaults[i]);
1097         }
1098     }
1099 }
1100 
SetPlayerNameDefault(void)1101 void SetPlayerNameDefault(void)
1102 {
1103     if (net_player_name == NULL)
1104     {
1105         net_player_name = NET_GetRandomPetName();
1106     }
1107 }
1108 
MultiplayerConfig(TXT_UNCAST_ARG (widget),void * user_data)1109 void MultiplayerConfig(TXT_UNCAST_ARG(widget), void *user_data)
1110 {
1111     txt_window_t *window;
1112     txt_label_t *label;
1113     txt_table_t *table;
1114     char buf[10];
1115     int i;
1116 
1117     window = TXT_NewWindow("Multiplayer Configuration");
1118     TXT_SetWindowHelpURL(window, MULTI_CONFIG_HELP_URL);
1119 
1120     TXT_AddWidgets(window,
1121                    TXT_NewStrut(0, 1),
1122                    TXT_NewHorizBox(TXT_NewLabel("Player name:  "),
1123                                    TXT_NewInputBox(&net_player_name, 25),
1124                                    NULL),
1125                    TXT_NewStrut(0, 1),
1126                    TXT_NewSeparator("Chat macros"),
1127                    NULL);
1128 
1129     table = TXT_NewTable(2);
1130 
1131     for (i=0; i<10; ++i)
1132     {
1133         M_snprintf(buf, sizeof(buf), "#%i ", i + 1);
1134 
1135         label = TXT_NewLabel(buf);
1136         TXT_SetFGColor(label, TXT_COLOR_BRIGHT_CYAN);
1137 
1138         TXT_AddWidgets(table,
1139                        label,
1140                        TXT_NewInputBox(&chat_macros[(i + 1) % 10], 40),
1141                        NULL);
1142     }
1143 
1144     TXT_AddWidget(window, table);
1145 }
1146 
BindMultiplayerVariables(void)1147 void BindMultiplayerVariables(void)
1148 {
1149     char buf[15];
1150     int i;
1151 
1152     M_BindStringVariable("player_name", &net_player_name);
1153 
1154     for (i=0; i<10; ++i)
1155     {
1156         M_snprintf(buf, sizeof(buf), "chatmacro%i", i);
1157         M_BindStringVariable(buf, &chat_macros[i]);
1158     }
1159 
1160     switch (gamemission)
1161     {
1162         case doom:
1163             M_BindChatControls(4);
1164             key_multi_msgplayer[0] = 'g';
1165             key_multi_msgplayer[1] = 'i';
1166             key_multi_msgplayer[2] = 'b';
1167             key_multi_msgplayer[3] = 'r';
1168             break;
1169 
1170         case heretic:
1171             M_BindChatControls(4);
1172             key_multi_msgplayer[0] = 'g';
1173             key_multi_msgplayer[1] = 'y';
1174             key_multi_msgplayer[2] = 'r';
1175             key_multi_msgplayer[3] = 'b';
1176             break;
1177 
1178         case hexen:
1179             M_BindChatControls(8);
1180             key_multi_msgplayer[0] = 'b';
1181             key_multi_msgplayer[1] = 'r';
1182             key_multi_msgplayer[2] = 'y';
1183             key_multi_msgplayer[3] = 'g';
1184             key_multi_msgplayer[4] = 'j';
1185             key_multi_msgplayer[5] = 'w';
1186             key_multi_msgplayer[6] = 'h';
1187             key_multi_msgplayer[7] = 'p';
1188             break;
1189 
1190         default:
1191             break;
1192     }
1193 }
1194 
1195