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