1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 1999-2004 Eidos Interactive
4 Copyright (C) 2005-2020 Warzone 2100 Project
5
6 Warzone 2100 is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 Warzone 2100 is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Warzone 2100; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /*
21 * MultiInt.c
22 *
23 * Alex Lee, 98. Pumpkin Studios, Bath.
24 * Functions to display and handle the multiplayer interface screens,
25 * along with connection and game options.
26 */
27
28 #include "lib/framework/wzapp.h"
29 #include "lib/framework/wzconfig.h"
30 #include "lib/framework/wzpaths.h"
31
32 #include <time.h>
33
34 #include "lib/framework/frameresource.h"
35 #include "lib/framework/file.h"
36 #include "lib/framework/stdio_ext.h"
37 #include "lib/framework/physfs_ext.h"
38
39 /* Includes direct access to render library */
40 #include "lib/ivis_opengl/bitimage.h"
41 #include "lib/ivis_opengl/pieblitfunc.h"
42 #include "lib/ivis_opengl/pietypes.h"
43 #include "lib/ivis_opengl/piestate.h"
44 #include "lib/ivis_opengl/pieclip.h"
45 #include "lib/ivis_opengl/piemode.h"
46 #include "lib/ivis_opengl/piepalette.h"
47 #include "lib/ivis_opengl/screen.h"
48
49 #include "lib/sound/audio.h"
50 #include "lib/sound/audio_id.h"
51
52 #include "lib/gamelib/gtime.h"
53 #include "lib/netplay/netplay.h"
54 #include "lib/widget/editbox.h"
55 #include "lib/widget/button.h"
56 #include "lib/widget/scrollablelist.h"
57 #include "lib/widget/widget.h"
58 #include "lib/widget/widgint.h"
59 #include "lib/widget/label.h"
60 #include "lib/widget/paragraph.h"
61 #include "lib/widget/multibutform.h"
62
63 #include "challenge.h"
64 #include "main.h"
65 #include "levels.h"
66 #include "objects.h"
67 #include "display.h"// pal stuff
68 #include "display3d.h"
69 #include "objmem.h"
70 #include "gateway.h"
71 #include "clparse.h"
72 #include "configuration.h"
73 #include "intdisplay.h"
74 #include "design.h"
75 #include "hci.h"
76 #include "power.h"
77 #include "loadsave.h" // for blueboxes.
78 #include "component.h"
79 #include "map.h"
80 #include "console.h" // chat box stuff
81 #include "frend.h"
82 #include "advvis.h"
83 #include "frontend.h"
84 #include "data.h"
85 #include "keymap.h"
86 #include "game.h"
87 #include "warzoneconfig.h"
88 #include "modding.h"
89 #include "qtscript.h"
90 #include "random.h"
91 #include "notifications.h"
92 #include "lib/framework/wztime.h"
93
94 #include "multiplay.h"
95 #include "multiint.h"
96 #include "multijoin.h"
97 #include "multistat.h"
98 #include "multirecv.h"
99 #include "multimenu.h"
100 #include "multilimit.h"
101 #include "multigifts.h"
102
103 #include "titleui/titleui.h"
104
105 #include "warzoneconfig.h"
106
107 #include "init.h"
108 #include "levels.h"
109 #include "wrappers.h"
110 #include "faction.h"
111
112 #include "activity.h"
113 #include <algorithm>
114
115 #define MAP_PREVIEW_DISPLAY_TIME 2500 // number of milliseconds to show map in preview
116 #define VOTE_TAG "voting"
117 #define KICK_REASON_TAG "kickReason"
118
119 // ////////////////////////////////////////////////////////////////////////////
120 // tertile dependent colors for map preview
121
122 // C1 - Arizona type
123 #define WZCOL_TERC1_CLIFF_LOW pal_Colour(0x68, 0x3C, 0x24)
124 #define WZCOL_TERC1_CLIFF_HIGH pal_Colour(0xE8, 0x84, 0x5C)
125 #define WZCOL_TERC1_WATER pal_Colour(0x3F, 0x68, 0x9A)
126 #define WZCOL_TERC1_ROAD_LOW pal_Colour(0x24, 0x1F, 0x16)
127 #define WZCOL_TERC1_ROAD_HIGH pal_Colour(0xB2, 0x9A, 0x66)
128 #define WZCOL_TERC1_GROUND_LOW pal_Colour(0x24, 0x1F, 0x16)
129 #define WZCOL_TERC1_GROUND_HIGH pal_Colour(0xCC, 0xB2, 0x80)
130 // C2 - Urban type
131 #define WZCOL_TERC2_CLIFF_LOW pal_Colour(0x3C, 0x3C, 0x3C)
132 #define WZCOL_TERC2_CLIFF_HIGH pal_Colour(0x84, 0x84, 0x84)
133 #define WZCOL_TERC2_WATER WZCOL_TERC1_WATER
134 #define WZCOL_TERC2_ROAD_LOW pal_Colour(0x00, 0x00, 0x00)
135 #define WZCOL_TERC2_ROAD_HIGH pal_Colour(0x24, 0x1F, 0x16)
136 #define WZCOL_TERC2_GROUND_LOW pal_Colour(0x1F, 0x1F, 0x1F)
137 #define WZCOL_TERC2_GROUND_HIGH pal_Colour(0xB2, 0xB2, 0xB2)
138 // C3 - Rockies type
139 #define WZCOL_TERC3_CLIFF_LOW pal_Colour(0x3C, 0x3C, 0x3C)
140 #define WZCOL_TERC3_CLIFF_HIGH pal_Colour(0xFF, 0xFF, 0xFF)
141 #define WZCOL_TERC3_WATER WZCOL_TERC1_WATER
142 #define WZCOL_TERC3_ROAD_LOW pal_Colour(0x24, 0x1F, 0x16)
143 #define WZCOL_TERC3_ROAD_HIGH pal_Colour(0x3D, 0x21, 0x0A)
144 #define WZCOL_TERC3_GROUND_LOW pal_Colour(0x00, 0x1C, 0x0E)
145 #define WZCOL_TERC3_GROUND_HIGH WZCOL_TERC3_CLIFF_HIGH
146
147 // ////////////////////////////////////////////////////////////////////////////
148 // vars
149 extern char MultiCustomMapsPath[PATH_MAX];
150 extern char MultiPlayersPath[PATH_MAX];
151 extern bool bSendingMap; // used to indicate we are sending a map
152
153 enum RoomMessageType {
154 RoomMessagePlayer,
155 RoomMessageSystem,
156 RoomMessageNotify
157 };
158
159 struct RoomMessage
160 {
161 std::shared_ptr<PlayerReference> sender = nullptr;
162 std::string text;
163 std::time_t time = std::time(nullptr);
164 RoomMessageType type;
165
playerRoomMessage166 static RoomMessage player(uint32_t messageSender, std::string messageText)
167 {
168 auto message = RoomMessage(RoomMessagePlayer, messageText);
169 message.sender = NetPlay.playerReferences[messageSender];
170 return message;
171 }
172
systemRoomMessage173 static RoomMessage system(std::string messageText)
174 {
175 return RoomMessage(RoomMessageSystem, messageText);
176 }
177
notifyRoomMessage178 static RoomMessage notify(std::string messageText)
179 {
180 return RoomMessage(RoomMessageNotify, messageText);
181 }
182
183 private:
RoomMessageRoomMessage184 RoomMessage(RoomMessageType messageType, std::string messageText)
185 {
186 type = messageType;
187 text = messageText;
188 }
189 };
190
191 char sPlayer[128] = {'\0'}; // player name (to be used)
192 bool multiintDisableLobbyRefresh = false; // if we allow lobby to be refreshed or not.
193
194 static UDWORD hideTime = 0;
195 static uint8_t playerVotes[MAX_PLAYERS];
196 LOBBY_ERROR_TYPES LobbyError = ERROR_NOERROR;
197 static bool bInActualHostedLobby = false;
198 /// end of globals.
199 // ////////////////////////////////////////////////////////////////////////////
200 // Function protos
201
202 // widget functions
203 static bool addMultiEditBox(UDWORD formid, UDWORD id, UDWORD x, UDWORD y, char const *tip, char const *tipres, UDWORD icon, UDWORD iconhi, UDWORD iconid);
204 static W_FORM * addBlueForm(UDWORD parent, UDWORD id, UDWORD x, UDWORD y, UDWORD w, UDWORD h);
205 static void drawReadyButton(UDWORD player);
206
207 // Drawing Functions
208 static void displayChatEdit(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
209 static void displayPlayer(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
210 static void displayPosition(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
211 static void displayColour(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
212 static void displayFaction(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
213 static void displayTeamChooser(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
214 static void displayAi(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
215 static void displayDifficulty(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
216 static void displayMultiEditBox(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
217
218 // pUserData structures used by drawing functions
219 struct DisplayPlayerCache {
220 std::string fullMainText; // the “full” main text (used for storing the full player name when displaying a player)
221 WzText wzMainText; // the main text
222
223 WzText wzSubText; // the sub text (used for players)
224 WzText wzEloText; // the elo text (used for players)
225 };
226 struct DisplayPositionCache {
227 WzText wzPositionText;
228 };
229 struct DisplayAICache {
230 WzText wzText;
231 };
232 struct DisplayDifficultyCache {
233 WzText wzDifficultyText;
234 };
235
236 // Game option functions
237 static void addGameOptions();
238 static void addChatBox(bool preserveOldChat = false);
239 static void disableMultiButs();
240 static void SendFireUp();
241
242 static void decideWRF();
243
244 static bool SendColourRequest(UBYTE player, UBYTE col);
245 static bool SendFactionRequest(UBYTE player, UBYTE faction);
246 static bool SendPositionRequest(UBYTE player, UBYTE chosenPlayer);
247 bool changeReadyStatus(UBYTE player, bool bReady);
248 static void stopJoining(std::shared_ptr<WzTitleUI> parent);
249 static int difficultyIcon(int difficulty);
250
251 static void sendRoomChatMessage(char const *text);
252
253 static int factionIcon(FactionID faction);
254
255 static bool multiplayPlayersReady();
256 static bool multiplayIsStartingGame();
257 // ////////////////////////////////////////////////////////////////////////////
258 // map previews..
259
260 static const char *difficultyList[] = { N_("Easy"), N_("Medium"), N_("Hard"), N_("Insane") };
261 static const AIDifficulty difficultyValue[] = { AIDifficulty::EASY, AIDifficulty::MEDIUM, AIDifficulty::HARD, AIDifficulty::INSANE };
262 static struct
263 {
264 bool scavengers;
265 bool alliances;
266 bool teams;
267 bool power;
268 bool difficulty;
269 bool ai;
270 bool position;
271 bool bases;
272 } locked;
273
274 struct AIDATA
275 {
AIDATAAIDATA276 AIDATA() : assigned(0) {}
277 char name[MAX_LEN_AI_NAME];
278 char js[MAX_LEN_AI_NAME];
279 char tip[255 + 128]; ///< may contain optional AI tournament data
280 char difficultyTips[4][255]; ///< optional difficulty level info
281 int assigned; ///< How many AIs have we assigned of this type
282 };
283 static std::vector<AIDATA> aidata;
284
285 struct WzMultiButton : public W_BUTTON
286 {
WzMultiButtonWzMultiButton287 WzMultiButton() : W_BUTTON() {}
288
289 void display(int xOffset, int yOffset) override;
290
291 Image imNormal;
292 Image imDown;
293 unsigned doHighlight;
294 unsigned tc;
295 };
296
297 class ChatBoxWidget : public IntFormAnimated
298 {
299 protected:
ChatBoxWidget()300 ChatBoxWidget(): IntFormAnimated(true) {}
301 virtual void initialize();
302
303 public:
make()304 static std::shared_ptr<ChatBoxWidget> make()
305 {
306 class make_shared_enabler: public ChatBoxWidget {};
307 auto widget = std::make_shared<make_shared_enabler>();
308 widget->initialize();
309 return widget;
310 }
311
312 virtual ~ChatBoxWidget();
313 void addMessage(RoomMessage const &message);
314 void initializeMessages(bool preserveOldChat);
315
316 protected:
317 void geometryChanged() override;
318
319 private:
320 std::shared_ptr<ScrollableListWidget> messages;
321 std::shared_ptr<W_EDITBOX> editBox;
322 std::shared_ptr<CONSOLE_MESSAGE_LISTENER> handleConsoleMessage;
323 void displayMessage(RoomMessage const &message);
324
325 static std::vector<RoomMessage> persistentMessageLocalStorage;
326 };
327
328 std::vector<RoomMessage> ChatBoxWidget::persistentMessageLocalStorage;
329
displayRoomMessage(RoomMessage const & message)330 void displayRoomMessage(RoomMessage const &message)
331 {
332 if (auto chatBox = (ChatBoxWidget *)widgGetFromID(psWScreen, MULTIOP_CHATBOX)) {
333 chatBox->addMessage(message);
334 }
335 }
336
displayRoomSystemMessage(char const * text)337 void displayRoomSystemMessage(char const *text)
338 {
339 displayRoomMessage(RoomMessage::system(text));
340 }
341
displayRoomNotifyMessage(char const * text)342 void displayRoomNotifyMessage(char const *text)
343 {
344 displayRoomMessage(RoomMessage::notify(text));
345 }
346
getAINames()347 const std::vector<WzString> getAINames()
348 {
349 std::vector<WzString> l;
350 for (const auto &i : aidata)
351 {
352 if (i.js[0] != '\0')
353 {
354 l.push_back(WzString::fromUtf8(i.js));
355 }
356 }
357 return l;
358 }
359
getAIName(int player)360 const char *getAIName(int player)
361 {
362 if (NetPlay.players[player].ai >= 0 && NetPlay.players[player].ai != AI_CUSTOM)
363 {
364 return aidata[NetPlay.players[player].ai].name;
365 }
366 else
367 {
368 return _("Commander");
369 }
370 }
371
loadMultiScripts()372 void loadMultiScripts()
373 {
374 bool defaultRules = true;
375 char aFileName[256];
376 char aPathName[256];
377 LEVEL_DATASET *psLevel = levFindDataSet(game.map, &game.hash);
378 ASSERT_OR_RETURN(, psLevel, "No level found for %s", game.map);
379 sstrcpy(aFileName, psLevel->apDataFiles[psLevel->game]);
380 aFileName[strlen(aFileName) - 4] = '\0';
381 sstrcpy(aPathName, aFileName);
382 sstrcat(aFileName, ".json");
383 sstrcat(aPathName, "/");
384 WzString ininame;
385 WzString path;
386 bool loadExtra = false;
387
388 if (challengeFileName.length() > 0)
389 {
390 ininame = challengeFileName;
391 path = "challenges/";
392 loadExtra = true;
393 }
394
395 if (getHostLaunch() == HostLaunch::Skirmish)
396 {
397 ininame = "tests/" + WzString::fromUtf8(wz_skirmish_test());
398 path = "tests/";
399 loadExtra = true;
400 }
401
402 if (getHostLaunch() == HostLaunch::Autohost)
403 {
404 ininame = "autohost/" + WzString::fromUtf8(wz_skirmish_test());
405 path = "autohost/";
406 loadExtra = true;
407 }
408
409 // Reset assigned counter
410 for (auto it = aidata.begin(); it < aidata.end(); ++it)
411 {
412 (*it).assigned = 0;
413 }
414
415 // Load map scripts
416 if (loadExtra && PHYSFS_exists(ininame.toUtf8().c_str()))
417 {
418 WzConfig ini(ininame, WzConfig::ReadOnly);
419 debug(LOG_SAVE, "Loading map scripts");
420 if (ini.beginGroup("scripts"))
421 {
422 if (ini.contains("extra"))
423 {
424 loadGlobalScript(path + ini.value("extra").toWzString());
425 }
426 if (ini.contains("rules"))
427 {
428 loadGlobalScript(path + ini.value("rules").toWzString());
429 defaultRules = false;
430 }
431 }
432 ini.endGroup();
433 }
434
435 // Load multiplayer rules
436 resForceBaseDir("messages/");
437 resLoadFile("SMSG", "multiplay.txt");
438 if (defaultRules)
439 {
440 debug(LOG_SAVE, "Loading default rules");
441 loadGlobalScript("multiplay/script/rules/init.js");
442 }
443
444 // Backup data hashes, since AI and scavenger scripts aren't run on all clients.
445 uint32_t oldHash1 = DataHash[DATA_SCRIPT];
446 uint32_t oldHash2 = DataHash[DATA_SCRIPTVAL];
447
448 // Load AI players for skirmish games
449 resForceBaseDir("multiplay/skirmish/");
450 if (bMultiPlayer && game.type == LEVEL_TYPE::SKIRMISH)
451 {
452 for (unsigned i = 0; i < game.maxPlayers; i++)
453 {
454 // Skip human players. Do not skip local player for autogames
455 const bool bIsLocalPlayer = i == selectedPlayer;
456 const bool bShouldSkipThisPlayer = !bIsLocalPlayer || !autogame_enabled();
457 if (NetPlay.players[i].allocated && bShouldSkipThisPlayer)
458 {
459 continue;
460 }
461
462 // Make sure local player has an AI in autogames
463 if (/*NetPlay.players[i].ai < 0 &&*/ bIsLocalPlayer && autogame_enabled())
464 {
465 //NetPlay.players[i].ai = 0;
466 // levLoadData handles making sure the local player has an AI in autogames
467 // TODO: clean this mess up, and pick one place to handle initializing the AI in autogames
468 continue;
469 }
470
471 if (NetPlay.players[i].ai >= 0 && myResponsibility(i))
472 {
473 if (aidata[NetPlay.players[i].ai].js[0] != '\0')
474 {
475 debug(LOG_SAVE, "Loading javascript AI for player %d", i);
476 loadPlayerScript(WzString("multiplay/skirmish/") + aidata[NetPlay.players[i].ai].js, i, NetPlay.players[i].difficulty);
477 }
478 }
479 }
480 }
481
482 // Load scavengers
483 if (game.scavengers && myResponsibility(scavengerPlayer()))
484 {
485 debug(LOG_SAVE, "Loading scavenger AI for player %d", scavengerPlayer());
486 loadPlayerScript("multiplay/script/scavengers/init.js", scavengerPlayer(), AIDifficulty::EASY);
487 }
488
489 // Restore data hashes, since AI and scavenger scripts aren't run on all clients.
490 DataHash[DATA_SCRIPT] = oldHash1; // Not all players load the same AI scripts.
491 DataHash[DATA_SCRIPTVAL] = oldHash2;
492
493 // Reset resource path, otherwise things break down the line
494 resForceBaseDir("");
495 }
496
guessMapTilesetType(LEVEL_DATASET * psLevel)497 static MAP_TILESET_TYPE guessMapTilesetType(LEVEL_DATASET *psLevel)
498 {
499 unsigned t = 0, c = 0;
500
501 if (psLevel->psBaseData && psLevel->psBaseData->pName)
502 {
503 if (sscanf(psLevel->psBaseData->pName, "MULTI_CAM_%u", &c) != 1)
504 {
505 sscanf(psLevel->psBaseData->pName, "MULTI_T%u_C%u", &t, &c);
506 }
507 }
508
509 switch (c)
510 {
511 case 1:
512 return TILESET_ARIZONA;
513 break;
514 case 2:
515 return TILESET_URBAN;
516 break;
517 case 3:
518 return TILESET_ROCKIES;
519 break;
520 }
521
522 debug(LOG_MAP, "Could not guess map tileset, using ARIZONA.");
523 return TILESET_ARIZONA;
524 }
525
loadEmptyMapPreview()526 static void loadEmptyMapPreview()
527 {
528 // No map is available to preview, so improvise.
529 char *imageData = (char *)malloc(BACKDROP_HACK_WIDTH * BACKDROP_HACK_HEIGHT * 3);
530 memset(imageData, 0, BACKDROP_HACK_WIDTH * BACKDROP_HACK_HEIGHT * 3); //dunno about background color
531 int const ex = 100, ey = 100, bx = 5, by = 8;
532 for (unsigned n = 0; n < 125; ++n)
533 {
534 int sx = rand() % (ex - bx), sy = rand() % (ey - by);
535 char col[3] = {char(rand() % 256), char(rand() % 256), char(rand() % 256)};
536 for (unsigned y = 0; y < by; ++y)
537 for (unsigned x = 0; x < bx; ++x)
538 if (("\2\1\261\11\6"[x] >> y & 1) == 1) // ?
539 {
540 memcpy(imageData + 3 * (sx + x + BACKDROP_HACK_WIDTH * (sy + y)), col, 3);
541 }
542 }
543
544 // Slight hack to init array with a special value used to determine how many players on map
545 Vector2i playerpos[MAX_PLAYERS];
546 for (size_t i = 0; i < MAX_PLAYERS; ++i)
547 {
548 playerpos[i] = Vector2i(0x77777777, 0x77777777);
549 }
550
551 screen_enableMapPreview(ex, ey, playerpos);
552
553 screen_Upload(imageData);
554 free(imageData);
555 }
556
557 /// Loads the entire map just to show a picture of it
loadMapPreview(bool hideInterface)558 void loadMapPreview(bool hideInterface)
559 {
560 static char aFileName[256];
561 UDWORD fileSize;
562 char *pFileData = nullptr;
563 LEVEL_DATASET *psLevel = nullptr;
564 PIELIGHT plCliffL, plCliffH, plWater, plRoadL, plRoadH, plGroundL, plGroundH;
565 UDWORD height;
566 UBYTE col;
567 MAPTILE *psTile, *WTile;
568 UDWORD oursize;
569 Vector2i playerpos[MAX_PLAYERS]; // Will hold player positions
570 char *ptr = nullptr, *imageData = nullptr;
571
572 // absurd hack, since there is a problem with updating this crap piece of info, we're setting it to
573 // true by default for now, like it used to be
574 game.mapHasScavengers = true; // this is really the wrong place for it, but this is where it has to be
575
576 if (psMapTiles)
577 {
578 mapShutdown();
579 }
580
581 // load the terrain types
582 psLevel = levFindDataSet(game.map, &game.hash);
583 if (psLevel == nullptr)
584 {
585 debug(LOG_INFO, "Could not find level dataset \"%s\" %s. We %s waiting for a download.", game.map, game.hash.toString().c_str(), !NetPlay.wzFiles.empty() ? "are" : "aren't");
586 loadEmptyMapPreview();
587 return;
588 }
589 if (psLevel->realFileName == nullptr)
590 {
591 builtInMap = true;
592 debug(LOG_WZ, "Loading map preview: \"%s\" builtin t%d", psLevel->pName, psLevel->dataDir);
593 }
594 else
595 {
596 builtInMap = false;
597 debug(LOG_WZ, "Loading map preview: \"%s\" in (%s)\"%s\" %s t%d", psLevel->pName, WZ_PHYSFS_getRealDir_String(psLevel->realFileName).c_str(), psLevel->realFileName, psLevel->realFileHash.toString().c_str(), psLevel->dataDir);
598 }
599 rebuildSearchPath(psLevel->dataDir, false, psLevel->realFileName);
600 sstrcpy(aFileName, psLevel->apDataFiles[psLevel->game]);
601 aFileName[strlen(aFileName) - 4] = '\0';
602 sstrcat(aFileName, "/ttypes.ttp");
603 pFileData = fileLoadBuffer;
604
605 if (!loadFileToBuffer(aFileName, pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
606 {
607 debug(LOG_ERROR, "Failed to load terrain types file: [%s]", aFileName);
608 return;
609 }
610 if (!loadTerrainTypeMap(pFileData, fileSize))
611 {
612 debug(LOG_ERROR, "Failed to load terrain types");
613 return;
614 }
615
616 // load the map data
617 ptr = strrchr(aFileName, '/');
618 ASSERT_OR_RETURN(, ptr, "this string was supposed to contain a /");
619 strcpy(ptr, "/game.js");
620 bool haveScript = PHYSFS_exists(aFileName);
621 ScriptMapData data;
622 if (haveScript)
623 {
624 data = runMapScript(aFileName, rand(), true);
625 }
626 else
627 {
628 strcpy(ptr, "/game.map");
629 }
630 if (haveScript? !mapLoadFromScriptData(data, true) : !mapLoad(aFileName, true))
631 {
632 debug(LOG_ERROR, "Failed to load map");
633 return;
634 }
635 gwShutDown();
636
637 // set tileset colors
638 switch (guessMapTilesetType(psLevel))
639 {
640 case TILESET_ARIZONA:
641 plCliffL = WZCOL_TERC1_CLIFF_LOW;
642 plCliffH = WZCOL_TERC1_CLIFF_HIGH;
643 plWater = WZCOL_TERC1_WATER;
644 plRoadL = WZCOL_TERC1_ROAD_LOW;
645 plRoadH = WZCOL_TERC1_ROAD_HIGH;
646 plGroundL = WZCOL_TERC1_GROUND_LOW;
647 plGroundH = WZCOL_TERC1_GROUND_HIGH;
648 break;
649 case TILESET_URBAN:
650 plCliffL = WZCOL_TERC2_CLIFF_LOW;
651 plCliffH = WZCOL_TERC2_CLIFF_HIGH;
652 plWater = WZCOL_TERC2_WATER;
653 plRoadL = WZCOL_TERC2_ROAD_LOW;
654 plRoadH = WZCOL_TERC2_ROAD_HIGH;
655 plGroundL = WZCOL_TERC2_GROUND_LOW;
656 plGroundH = WZCOL_TERC2_GROUND_HIGH;
657 break;
658 case TILESET_ROCKIES:
659 plCliffL = WZCOL_TERC3_CLIFF_LOW;
660 plCliffH = WZCOL_TERC3_CLIFF_HIGH;
661 plWater = WZCOL_TERC3_WATER;
662 plRoadL = WZCOL_TERC3_ROAD_LOW;
663 plRoadH = WZCOL_TERC3_ROAD_HIGH;
664 plGroundL = WZCOL_TERC3_GROUND_LOW;
665 plGroundH = WZCOL_TERC3_GROUND_HIGH;
666 break;
667 default:
668 debug(LOG_FATAL, "Invalid tileset type");
669 // silence warnings
670 abort();
671 return;
672 }
673
674 oursize = sizeof(char) * BACKDROP_HACK_WIDTH * BACKDROP_HACK_HEIGHT;
675 imageData = (char *)malloc(oursize * 3); // used for the texture
676 if (!imageData)
677 {
678 debug(LOG_FATAL, "Out of memory for texture!");
679 abort(); // should be a fatal error ?
680 return;
681 }
682 ptr = imageData;
683 memset(ptr, 0, sizeof(char) * BACKDROP_HACK_WIDTH * BACKDROP_HACK_HEIGHT * 3); //dunno about background color
684 psTile = psMapTiles;
685
686 for (int y = 0; y < mapHeight; ++y)
687 {
688 WTile = psTile;
689 for (int x = 0; x < mapWidth; ++x)
690 {
691 char *const p = imageData + (3 * (y * BACKDROP_HACK_WIDTH + x));
692 height = WTile->height / ELEVATION_SCALE;
693 col = height;
694
695 switch (terrainType(WTile))
696 {
697 case TER_CLIFFFACE:
698 p[0] = plCliffL.byte.r + (plCliffH.byte.r - plCliffL.byte.r) * col / 256;
699 p[1] = plCliffL.byte.g + (plCliffH.byte.g - plCliffL.byte.g) * col / 256;
700 p[2] = plCliffL.byte.b + (plCliffH.byte.b - plCliffL.byte.b) * col / 256;
701 break;
702 case TER_WATER:
703 p[0] = plWater.byte.r;
704 p[1] = plWater.byte.g;
705 p[2] = plWater.byte.b;
706 break;
707 case TER_ROAD:
708 p[0] = plRoadL.byte.r + (plRoadH.byte.r - plRoadL.byte.r) * col / 256;
709 p[1] = plRoadL.byte.g + (plRoadH.byte.g - plRoadL.byte.g) * col / 256;
710 p[2] = plRoadL.byte.b + (plRoadH.byte.b - plRoadL.byte.b) * col / 256;
711 break;
712 default:
713 p[0] = plGroundL.byte.r + (plGroundH.byte.r - plGroundL.byte.r) * col / 256;
714 p[1] = plGroundL.byte.g + (plGroundH.byte.g - plGroundL.byte.g) * col / 256;
715 p[2] = plGroundL.byte.b + (plGroundH.byte.b - plGroundL.byte.b) * col / 256;
716 break;
717 }
718 WTile += 1;
719 }
720 psTile += mapWidth;
721 }
722 // Slight hack to init array with a special value used to determine how many players on map
723 for (size_t i = 0; i < MAX_PLAYERS; ++i)
724 {
725 playerpos[i] = Vector2i(0x77777777, 0x77777777);
726 }
727 // color our texture with clancolors @ correct position
728 if (haveScript)
729 {
730 plotStructurePreviewScript(data, imageData, playerpos);
731 }
732 else
733 {
734 plotStructurePreview16(imageData, playerpos);
735 }
736
737 screen_enableMapPreview(mapWidth, mapHeight, playerpos);
738
739 screen_Upload(imageData);
740
741 free(imageData);
742
743 if (hideInterface)
744 {
745 hideTime = gameTime;
746 }
747 mapShutdown();
748 }
749
750 // ////////////////////////////////////////////////////////////////////////////
751 // helper func
752
matchAIbyName(const char * name)753 int matchAIbyName(const char *name)
754 {
755 int i = 0;
756
757 if (name[0] == '\0')
758 {
759 return AI_CLOSED;
760 }
761 for (auto it = aidata.cbegin(); it < aidata.cend(); ++it, i++)
762 {
763 if (strncasecmp(name, (*it).name, MAX_LEN_AI_NAME) == 0)
764 {
765 return i;
766 }
767 }
768 return AI_NOT_FOUND;
769 }
770
readAIs()771 void readAIs()
772 {
773 char basepath[PATH_MAX];
774 const char *sSearchPath = "multiplay/skirmish/";
775
776 aidata.clear();
777
778 sstrcpy(basepath, sSearchPath);
779 WZ_PHYSFS_enumerateFiles(basepath, [&](const char *file) -> bool {
780 char path[PATH_MAX];
781 // See if this filename contains the extension we're looking for
782 if (!strstr(file, ".json"))
783 {
784 return true; // continue;
785 }
786 sstrcpy(path, basepath);
787 sstrcat(path, file);
788 WzConfig aiconf(path, WzConfig::ReadOnly);
789 AIDATA ai;
790 aiconf.beginGroup("AI");
791
792 if (aiconf.contains("name"))
793 {
794 sstrcpy(ai.name, _(aiconf.value("name", "").toWzString().toUtf8().c_str()));
795 }
796 else
797 {
798 sstrcpy(ai.name, _("MISSING AI NAME"));
799 }
800
801 sstrcpy(ai.js, aiconf.value("js", "").toWzString().toUtf8().c_str());
802
803 const char *difficultyKeys[] = { "easy_tip", "medium_tip", "hard_tip", "insane_tip" };
804 for (int i = 0; i < ARRAY_SIZE(difficultyKeys); i++)
805 {
806 if (aiconf.contains(difficultyKeys[i]))
807 {
808 sstrcpy(ai.difficultyTips[i], _(aiconf.value(difficultyKeys[i], "").toWzString().toUtf8().c_str()));
809 }
810 else
811 {
812 // note that the empty string "" must never be translated
813 sstrcpy(ai.difficultyTips[i], "");
814 }
815 }
816
817 if (aiconf.contains("tip"))
818 {
819 sstrcpy(ai.tip, _(aiconf.value("tip", "").toWzString().toUtf8().c_str()));
820 }
821 else
822 {
823 sstrcpy(ai.tip, _("MISSING AI DESCRIPTION"));
824 }
825
826 int wins = aiconf.value("wins", 0).toInt();
827 int losses = aiconf.value("losses", 0).toInt();
828 int draws = aiconf.value("draws", 0).toInt();
829 int total = wins + losses + draws;
830 if (total)
831 {
832 float win_percentage = static_cast<float>(wins) / total * 100;
833 float loss_percentage = static_cast<float>(losses) / total * 100;
834 float draw_percentage = static_cast<float>(draws) / total * 100;
835 sstrcat(ai.tip, "\n");
836 char statistics[127];
837 ssprintf(statistics, _("AI tournament: %3.1f%% wins, %3.1f%% losses, %3.1f%% draws"), win_percentage, loss_percentage, draw_percentage);
838 sstrcat(ai.tip, statistics);
839 }
840
841 if (strcmp(file, "nb_generic.json") == 0)
842 {
843 aidata.insert(aidata.begin(), ai);
844 }
845 else
846 {
847 aidata.push_back(ai);
848 }
849 aiconf.endGroup();
850 return true; // continue
851 });
852 }
853
854 //sets sWRFILE form game.map
decideWRF()855 static void decideWRF()
856 {
857 // try and load it from the maps directory first,
858 sstrcpy(aLevelName, MultiCustomMapsPath);
859 sstrcat(aLevelName, game.map);
860 sstrcat(aLevelName, ".wrf");
861 debug(LOG_WZ, "decideWRF: %s", aLevelName);
862 //if the file exists in the downloaded maps dir then use that one instead.
863 // FIXME: Try to incorporate this into physfs setup somehow for sane paths
864 if (!PHYSFS_exists(aLevelName))
865 {
866 sstrcpy(aLevelName, game.map); // doesn't exist, must be a predefined one.
867 }
868 }
869
870 // ////////////////////////////////////////////////////////////////////////
871 // Lobby error reading
getLobbyError()872 LOBBY_ERROR_TYPES getLobbyError()
873 {
874 return LobbyError;
875 }
876
setLobbyError(LOBBY_ERROR_TYPES error_type)877 void setLobbyError(LOBBY_ERROR_TYPES error_type)
878 {
879 LobbyError = error_type;
880 if (LobbyError <= ERROR_FULL)
881 {
882 multiintDisableLobbyRefresh = false;
883 }
884 else
885 {
886 multiintDisableLobbyRefresh = true;
887 }
888 }
889
890 // NOTE: Must call NETinit(true); before this will actually work
findLobbyGame(const std::string & lobbyAddress,unsigned int lobbyPort,uint32_t lobbyGameId)891 std::vector<JoinConnectionDescription> findLobbyGame(const std::string& lobbyAddress, unsigned int lobbyPort, uint32_t lobbyGameId)
892 {
893 WzString originalLobbyServerName = WzString::fromUtf8(NETgetMasterserverName());
894 unsigned int originalLobbyServerPort = NETgetMasterserverPort();
895
896 if (!lobbyAddress.empty())
897 {
898 if (lobbyPort == 0)
899 {
900 debug(LOG_ERROR, "Invalid lobby port #");
901 return {};
902 }
903 NETsetMasterserverName(lobbyAddress.c_str());
904 NETsetMasterserverPort(lobbyPort);
905 }
906
907 auto cleanup = [&]() {
908 NETsetMasterserverName(originalLobbyServerName.toUtf8().c_str());
909 NETsetMasterserverPort(originalLobbyServerPort);
910 };
911
912 if (getLobbyError() != ERROR_INVALID)
913 {
914 setLobbyError(ERROR_NOERROR);
915 }
916
917 GAMESTRUCT lobbyGame;
918 memset(&lobbyGame, 0x00, sizeof(lobbyGame));
919 if (!NETfindGame(lobbyGameId, lobbyGame))
920 {
921 // failed to get list of games from lobby server
922 debug(LOG_ERROR, "Failed to find gameId in lobby server");
923 cleanup();
924 return {};
925 }
926
927 if (getLobbyError())
928 {
929 debug(LOG_ERROR, "Lobby error: %d", (int)getLobbyError());
930 cleanup();
931 return {};
932 }
933
934 if (lobbyGame.desc.dwSize == 0)
935 {
936 debug(LOG_ERROR, "Invalid game struct");
937 cleanup();
938 return {};
939 }
940
941 if (lobbyGame.gameId != lobbyGameId)
942 {
943 ASSERT(lobbyGame.gameId == lobbyGameId, "NETfindGame returned a non-matching game"); // logic error
944 cleanup();
945 return {};
946 }
947
948 // found the game id, but is it compatible?
949
950 if (!NETisCorrectVersion(lobbyGame.game_version_major, lobbyGame.game_version_minor))
951 {
952 // incompatible version
953 debug(LOG_ERROR, "Failed to find a matching + compatible game in the lobby server");
954 cleanup();
955 return {};
956 }
957
958 // found the game
959 if (strlen(lobbyGame.desc.host) == 0)
960 {
961 debug(LOG_ERROR, "Found the game, but no host details available");
962 cleanup();
963 return {};
964 }
965 std::string host = lobbyGame.desc.host;
966 return {JoinConnectionDescription(host, lobbyGame.hostPort)};
967 }
968
969 static JoinGameResult joinGameInternal(std::vector<JoinConnectionDescription> connection_list, std::shared_ptr<WzTitleUI> oldUI);
970 static JoinGameResult joinGameInternalConnect(const char *host, uint32_t port, std::shared_ptr<WzTitleUI> oldUI);
971
joinGame(const char * connectionString)972 JoinGameResult joinGame(const char *connectionString)
973 {
974 if (strchr(connectionString, '[') == NULL || strchr(connectionString, ']') == NULL) // it is not IPv6. For more see rfc3986 section-3.2.2
975 {
976 const char* ddch = strchr(connectionString, ':');
977 if(ddch != NULL)
978 {
979 uint32_t serverPort = atoi(ddch+1);
980 std::string serverIP = "";
981 serverIP.assign(connectionString, ddch - connectionString);
982 debug(LOG_INFO, "Connecting to ip [%s] port %d", serverIP.c_str(), serverPort);
983 return joinGame(serverIP.c_str(), serverPort);
984 }
985 }
986 return joinGame(connectionString, 0);
987 }
988
joinGame(const char * host,uint32_t port)989 JoinGameResult joinGame(const char *host, uint32_t port)
990 {
991 std::string hostStr = (host != nullptr) ? std::string(host) : std::string();
992 return joinGame(std::vector<JoinConnectionDescription>({JoinConnectionDescription(hostStr, port)}));
993 }
994
joinGame(const std::vector<JoinConnectionDescription> & connection_list)995 JoinGameResult joinGame(const std::vector<JoinConnectionDescription>& connection_list) {
996 return joinGameInternal(connection_list, wzTitleUICurrent);
997 }
998
joinGameInternal(std::vector<JoinConnectionDescription> connection_list,std::shared_ptr<WzTitleUI> oldUI)999 static JoinGameResult joinGameInternal(std::vector<JoinConnectionDescription> connection_list, std::shared_ptr<WzTitleUI> oldUI){
1000
1001 if (connection_list.size() > 1)
1002 {
1003 // sort the list, based on NETgetJoinPreferenceIPv6
1004 // preserve the original relative order amongst each class of IPv4/IPv6 addresses
1005 bool bSortIPv6First = NETgetJoinPreferenceIPv6();
1006 std::stable_sort(connection_list.begin(), connection_list.end(), [bSortIPv6First](const JoinConnectionDescription& a, const JoinConnectionDescription& b) -> bool {
1007 bool a_isIPv6 = a.host.find(":") != std::string::npos; // this is a very simplistic test - if the host contains ":" we treat it as IPv6
1008 bool b_isIPv6 = b.host.find(":") != std::string::npos;
1009 return (bSortIPv6First) ? (a_isIPv6 && !b_isIPv6) : (!a_isIPv6 && b_isIPv6);
1010 });
1011 }
1012
1013 for (const auto& connDesc : connection_list)
1014 {
1015 JoinGameResult result = joinGameInternalConnect(connDesc.host.c_str(), connDesc.port, oldUI);
1016 switch (result)
1017 {
1018 case JoinGameResult::FAILED:
1019 continue;
1020 case JoinGameResult::PENDING_PASSWORD:
1021 return result;
1022 case JoinGameResult::JOINED:
1023 ActivityManager::instance().joinGameSucceeded(connDesc.host.c_str(), connDesc.port);
1024 return result;
1025 }
1026 }
1027
1028 // Failed to connect to all IPs / options in list
1029 // Change to an error display.
1030 changeTitleUI(std::make_shared<WzMsgBoxTitleUI>(WzString(_("Error while joining.")), wzTitleUICurrent));
1031 ActivityManager::instance().joinGameFailed(connection_list);
1032 return JoinGameResult::FAILED;
1033 }
1034
1035 /**
1036 * Try connecting to the given host, show a password screen, the multiplayer screen or an error.
1037 * The reason for this having a third parameter is so that the password dialog
1038 * doesn't turn into the parent of the next connection attempt.
1039 * Any other barriers/auth methods/whatever would presumably benefit in the same way.
1040 */
joinGameInternalConnect(const char * host,uint32_t port,std::shared_ptr<WzTitleUI> oldUI)1041 static JoinGameResult joinGameInternalConnect(const char *host, uint32_t port, std::shared_ptr<WzTitleUI> oldUI)
1042 {
1043 // oldUI may get captured for use in the password dialog, among other things.
1044 PLAYERSTATS playerStats;
1045
1046 if (ingame.localJoiningInProgress)
1047 {
1048 return JoinGameResult::FAILED;
1049 }
1050
1051 if (!NETjoinGame(host, port, (char *)sPlayer)) // join
1052 {
1053 switch (getLobbyError())
1054 {
1055 case ERROR_HOSTDROPPED:
1056 setLobbyError(ERROR_NOERROR);
1057 break;
1058 case ERROR_WRONGPASSWORD:
1059 {
1060 std::string capturedHost = host;
1061 changeTitleUI(std::make_shared<WzPassBoxTitleUI>([=](const char * pass) {
1062 if (!pass) {
1063 changeTitleUI(oldUI);
1064 } else {
1065 NETsetGamePassword(pass);
1066 JoinConnectionDescription conn(capturedHost, port);
1067 joinGameInternal({conn}, oldUI);
1068 }
1069 }));
1070 return JoinGameResult::PENDING_PASSWORD;
1071 }
1072 default:
1073 break;
1074 }
1075
1076 return JoinGameResult::FAILED;
1077 }
1078 ingame.localJoiningInProgress = true;
1079
1080 loadMultiStats(sPlayer, &playerStats);
1081 setMultiStats(selectedPlayer, playerStats, false);
1082 setMultiStats(selectedPlayer, playerStats, true);
1083
1084 changeTitleUI(std::make_shared<WzMultiplayerOptionsTitleUI>(oldUI));
1085
1086 if (war_getMPcolour() >= 0)
1087 {
1088 SendColourRequest(selectedPlayer, war_getMPcolour());
1089 }
1090
1091 return JoinGameResult::JOINED;
1092 }
1093
1094 // ////////////////////////////////////////////////////////////////////////////
1095 // Game Options Screen.
1096
1097 // ////////////////////////////////////////////////////////////////////////////
1098
addInlineChooserBlueForm(const std::shared_ptr<W_SCREEN> & psScreen,W_FORM * psParent,UDWORD id,WzString txt,UDWORD x,UDWORD y,UDWORD w,UDWORD h)1099 static void addInlineChooserBlueForm(const std::shared_ptr<W_SCREEN> &psScreen, W_FORM *psParent, UDWORD id, WzString txt, UDWORD x, UDWORD y, UDWORD w, UDWORD h)
1100 {
1101 W_FORMINIT sFormInit; // draw options box.
1102 sFormInit.formID = MULTIOP_INLINE_OVERLAY_ROOT_FRM;
1103 sFormInit.id = id;
1104 sFormInit.x = (UWORD) x;
1105 sFormInit.y = (UWORD) y;
1106 sFormInit.style = WFORM_PLAIN;
1107 sFormInit.width = (UWORD)w;//190;
1108 sFormInit.height = (UWORD)h; //27;
1109 sFormInit.pDisplay = intDisplayFeBox;
1110
1111 std::weak_ptr<WIDGET> psWeakParent(psParent->shared_from_this());
1112 sFormInit.calcLayout = [x, y, psWeakParent](WIDGET *psWidget, unsigned int, unsigned int, unsigned int, unsigned int){
1113 if (auto psParent = psWeakParent.lock())
1114 {
1115 psWidget->move(psParent->screenPosX() + x, psParent->screenPosY() + y);
1116 }
1117 };
1118 widgAddForm(psScreen, &sFormInit);
1119 }
1120
addBlueForm(UDWORD parent,UDWORD id,UDWORD x,UDWORD y,UDWORD w,UDWORD h)1121 static W_FORM * addBlueForm(UDWORD parent, UDWORD id, UDWORD x, UDWORD y, UDWORD w, UDWORD h)
1122 {
1123 W_FORMINIT sFormInit; // draw options box.
1124 sFormInit.formID = parent;
1125 sFormInit.id = id;
1126 sFormInit.x = (UWORD) x;
1127 sFormInit.y = (UWORD) y;
1128 sFormInit.style = WFORM_PLAIN;
1129 sFormInit.width = (UWORD)w;//190;
1130 sFormInit.height = (UWORD)h; //27;
1131 sFormInit.pDisplay = intDisplayFeBox;
1132 return widgAddForm(psWScreen, &sFormInit);
1133 }
1134
1135
1136 struct LimitIcon
1137 {
1138 char const *stat;
1139 char const *desc;
1140 int icon;
1141 };
1142 static const LimitIcon limitIcons[] =
1143 {
1144 {"A0LightFactory", N_("Tanks disabled!!"), IMAGE_NO_TANK},
1145 {"A0CyborgFactory", N_("Cyborgs disabled."), IMAGE_NO_CYBORG},
1146 {"A0VTolFactory1", N_("VTOLs disabled."), IMAGE_NO_VTOL},
1147 {"A0Sat-linkCentre", N_("Satellite Uplink disabled."), IMAGE_NO_UPLINK},
1148 {"A0LasSatCommand", N_("Laser Satellite disabled."), IMAGE_NO_LASSAT},
1149 {nullptr, N_("Structure Limits Enforced."), IMAGE_DARK_LOCKED},
1150 };
1151
updateStructureDisabledFlags()1152 void updateStructureDisabledFlags()
1153 {
1154 // The host works out the flags.
1155 if (ingame.side != InGameSide::HOST_OR_SINGLEPLAYER)
1156 {
1157 return;
1158 }
1159
1160 unsigned flags = ingame.flags & MPFLAGS_FORCELIMITS;
1161
1162 assert(MPFLAGS_FORCELIMITS == (1 << (ARRAY_SIZE(limitIcons) - 1)));
1163 for (unsigned i = 0; i < ARRAY_SIZE(limitIcons) - 1; ++i) // skip last item, MPFLAGS_FORCELIMITS
1164 {
1165 int stat = getStructStatFromName(limitIcons[i].stat);
1166 bool disabled = stat >= 0 && asStructureStats[stat].upgrade[0].limit == 0;
1167 flags |= disabled << i;
1168 }
1169
1170 ingame.flags = flags;
1171 }
1172
updateLimitIcons()1173 static void updateLimitIcons()
1174 {
1175 widgDelete(psWScreen, MULTIOP_NO_SOMETHING);
1176 int y = 2;
1177 bool formBackgroundAdded = false;
1178 for (int i = 0; i < ARRAY_SIZE(limitIcons); ++i)
1179 {
1180 if ((ingame.flags & 1 << i) != 0)
1181 {
1182 // only add the background once. Must be added *before* the "icons" as the form acts as their parent
1183 if (!formBackgroundAdded)
1184 {
1185 addBlueForm(MULTIOP_OPTIONS, MULTIOP_NO_SOMETHING, MULTIOP_HOSTX, MULTIOP_NO_SOMETHINGY, MULTIOP_ICON_LIMITS_X2, MULTIOP_ICON_LIMITS_Y2);
1186 formBackgroundAdded = true;
1187 }
1188
1189 addMultiBut(psWScreen, MULTIOP_NO_SOMETHING, MULTIOP_NO_SOMETHINGY + i, MULTIOP_NO_SOMETHINGX, y,
1190 35, 28, _(limitIcons[i].desc),
1191 limitIcons[i].icon, limitIcons[i].icon, limitIcons[i].icon);
1192 y += 28 + 3;
1193 }
1194 }
1195 }
1196
formatGameName(WzString name)1197 WzString formatGameName(WzString name)
1198 {
1199 WzString withoutTechlevel = WzString::fromUtf8(mapNameWithoutTechlevel(name.toUtf8().c_str()));
1200 return withoutTechlevel + " (T" + WzString::number(game.techLevel) + " " + WzString::number(game.maxPlayers) + "P)";
1201 }
1202
resetVoteData()1203 void resetVoteData()
1204 {
1205 for (unsigned int i = 0; i < MAX_PLAYERS; ++i)
1206 {
1207 playerVotes[i] = 0;
1208 }
1209 }
1210
sendVoteData(uint8_t currentVote)1211 static void sendVoteData(uint8_t currentVote)
1212 {
1213 NETbeginEncode(NETbroadcastQueue(), NET_VOTE);
1214 NETuint32_t(&selectedPlayer);
1215 NETuint8_t(¤tVote);
1216 NETend();
1217 }
1218
getVoteTotal()1219 static uint8_t getVoteTotal()
1220 {
1221 ASSERT_HOST_ONLY(return true);
1222
1223 uint8_t total = 0;
1224
1225 for (unsigned i = 0; i < MAX_PLAYERS; ++i)
1226 {
1227 if (isHumanPlayer(i))
1228 {
1229 if (selectedPlayer == i)
1230 {
1231 // always count the host as a "yes" vote.
1232 playerVotes[i] = 1;
1233 }
1234 total += playerVotes[i];
1235 }
1236 else
1237 {
1238 playerVotes[i] = 0;
1239 }
1240 }
1241
1242 return total;
1243 }
1244
recvVote(NETQUEUE queue)1245 static bool recvVote(NETQUEUE queue)
1246 {
1247 ASSERT_HOST_ONLY(return true);
1248
1249 uint8_t newVote;
1250 uint32_t player;
1251
1252 NETbeginDecode(queue, NET_VOTE);
1253 NETuint32_t(&player);
1254 NETuint8_t(&newVote);
1255 NETend();
1256
1257 if (player >= MAX_PLAYERS)
1258 {
1259 debug(LOG_ERROR, "Invalid NET_VOTE from player %d: player id = %d", queue.index, static_cast<int>(player));
1260 return false;
1261 }
1262
1263 playerVotes[player] = (newVote == 1) ? 1 : 0;
1264
1265 debug(LOG_NET, "total votes: %d/%d", static_cast<int>(getVoteTotal()), static_cast<int>(NET_numHumanPlayers()));
1266
1267 return true;
1268 }
1269
1270 // Show a vote popup to allow changing maps or using the randomization feature.
setupVoteChoice()1271 static void setupVoteChoice()
1272 {
1273 //This shouldn't happen...
1274 if (NetPlay.isHost)
1275 {
1276 ASSERT(false, "Host tried to send vote data to themself");
1277 return;
1278 }
1279
1280 if (!hasNotificationsWithTag(VOTE_TAG))
1281 {
1282 WZ_Notification notification;
1283 notification.duration = 0;
1284 notification.contentTitle = _("Vote");
1285 notification.contentText = _("Allow host to change map or randomize?");
1286 notification.action = WZ_Notification_Action("Allow", [](const WZ_Notification&) {
1287 uint8_t vote = 1;
1288 sendVoteData(vote);
1289 });
1290 notification.tag = VOTE_TAG;
1291
1292 addNotification(notification, WZ_Notification_Trigger(GAME_TICKS_PER_SEC * 1));
1293 }
1294 }
1295
canChangeMapOrRandomize()1296 static bool canChangeMapOrRandomize()
1297 {
1298 ASSERT_HOST_ONLY(return true);
1299
1300 uint8_t numHumans = NET_numHumanPlayers();
1301 bool allowed = (static_cast<float>(getVoteTotal()) / static_cast<float>(numHumans)) > 0.5f;
1302
1303 resetVoteData(); //So the host can only do one change every vote session
1304
1305 if (numHumans == 1)
1306 {
1307 return true;
1308 }
1309
1310 if (!allowed)
1311 {
1312 //setup a vote popup for the clients
1313 NETbeginEncode(NETbroadcastQueue(), NET_VOTE_REQUEST);
1314 NETend();
1315
1316 displayRoomSystemMessage(_("Not enough votes to randomize or change the map."));
1317 }
1318
1319 return allowed;
1320 }
1321
addMultiButton(std::shared_ptr<MultibuttonWidget> mbw,int value,Image image,Image imageDown,char const * tip)1322 static void addMultiButton(std::shared_ptr<MultibuttonWidget> mbw, int value, Image image, Image imageDown, char const *tip)
1323 {
1324 auto button = std::make_shared<W_BUTTON>();
1325 button->setImages(image, imageDown, mpwidgetGetFrontHighlightImage(image));
1326 button->setTip(tip);
1327
1328 mbw->addButton(value, button);
1329 }
1330
1331 // need to check for side effects.
addGameOptions()1332 static void addGameOptions()
1333 {
1334 widgDelete(psWScreen, MULTIOP_OPTIONS); // clear options list
1335 widgDelete(psWScreen, FRONTEND_SIDETEXT3); // del text..
1336
1337 WIDGET *parent = widgGetFromID(psWScreen, FRONTEND_BACKDROP);
1338
1339 // draw options box.
1340 auto optionsForm = std::make_shared<IntFormAnimated>(false);
1341 parent->attach(optionsForm);
1342 optionsForm->id = MULTIOP_OPTIONS;
1343 optionsForm->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
1344 psWidget->setGeometry(MULTIOP_OPTIONSX, MULTIOP_OPTIONSY, MULTIOP_OPTIONSW, MULTIOP_OPTIONSH);
1345 }));
1346
1347 addSideText(FRONTEND_SIDETEXT3, MULTIOP_OPTIONSX - 3 , MULTIOP_OPTIONSY, _("OPTIONS"));
1348
1349 // game name box
1350 if (!NetPlay.bComms)
1351 {
1352 addMultiEditBox(MULTIOP_OPTIONS, MULTIOP_GNAME, MCOL0, MROW2, _("Game Name"),
1353 challengeActive ? game.name : _("One-Player Skirmish"), IMAGE_EDIT_GAME,
1354 IMAGE_EDIT_GAME_HI, MULTIOP_GNAME_ICON);
1355 // disable for one-player skirmish
1356 widgSetButtonState(psWScreen, MULTIOP_GNAME, WEDBS_DISABLE);
1357 }
1358 else
1359 {
1360 addMultiEditBox(MULTIOP_OPTIONS, MULTIOP_GNAME, MCOL0, MROW2, _("Select Game Name"), game.name, IMAGE_EDIT_GAME, IMAGE_EDIT_GAME_HI, MULTIOP_GNAME_ICON);
1361 }
1362 widgSetButtonState(psWScreen, MULTIOP_GNAME_ICON, WBUT_DISABLE);
1363
1364 // map chooser
1365
1366 // This is a bit complicated, but basically, see addMultiEditBox,
1367 // and then consider that the two buttons are relative to MCOL0, MROW3.
1368 // MCOL for N >= 1 is basically useless because that's not the actual rule followed by addMultiEditBox.
1369 // And that's what this panel is meant to align to.
1370 addBlueForm(MULTIOP_OPTIONS, MULTIOP_MAP, MCOL0, MROW3, MULTIOP_EDITBOXW + MULTIOP_EDITBOXH, MULTIOP_EDITBOXH);
1371 W_LABINIT sLabInit;
1372 sLabInit.formID = MULTIOP_MAP;
1373 sLabInit.id = MULTIOP_MAP + 1;
1374 sLabInit.x = 3;
1375 sLabInit.y = 4;
1376 sLabInit.width = MULTIOP_EDITBOXW - 24 - 5;
1377 sLabInit.height = 20;
1378 sLabInit.pText = formatGameName(game.map);
1379 widgAddLabel(psWScreen, &sLabInit);
1380 addMultiBut(psWScreen, MULTIOP_MAP, MULTIOP_MAP_ICON, MULTIOP_EDITBOXW + 2, 2, MULTIOP_EDITBOXH, MULTIOP_EDITBOXH, _("Select Map\nCan be blocked by players' votes"), IMAGE_EDIT_MAP, IMAGE_EDIT_MAP_HI, true);
1381 addMultiBut(psWScreen, MULTIOP_MAP, MULTIOP_MAP_MOD, MULTIOP_EDITBOXW - 14, 1, 12, 12, _("Map-Mod!"), IMAGE_LAMP_RED, IMAGE_LAMP_AMBER, false);
1382 addMultiBut(psWScreen, MULTIOP_MAP, MULTIOP_MAP_RANDOM, MULTIOP_EDITBOXW - 24, 15, 12, 12, _("Random map!"), IMAGE_WEE_DIE, IMAGE_WEE_DIE, false);
1383 if (!game.isMapMod)
1384 {
1385 widgHide(psWScreen, MULTIOP_MAP_MOD);
1386 }
1387 if (!game.isRandom)
1388 {
1389 widgHide(psWScreen, MULTIOP_MAP_RANDOM);
1390 }
1391 // disable for challenges
1392 if (challengeActive)
1393 {
1394 widgSetButtonState(psWScreen, MULTIOP_MAP_ICON, WBUT_DISABLE);
1395 }
1396 // password box
1397 if (NetPlay.bComms && ingame.side == InGameSide::HOST_OR_SINGLEPLAYER)
1398 {
1399 addMultiEditBox(MULTIOP_OPTIONS, MULTIOP_PASSWORD_EDIT, MCOL0, MROW4, _("Click to set Password"), NetPlay.gamePassword, IMAGE_UNLOCK_BLUE, IMAGE_LOCK_BLUE, MULTIOP_PASSWORD_BUT);
1400 auto *pPasswordButton = dynamic_cast<WzMultiButton*>(widgGetFromID(psWScreen, MULTIOP_PASSWORD_BUT));
1401 if (pPasswordButton)
1402 {
1403 pPasswordButton->minClickInterval = GAME_TICKS_PER_SEC / 2;
1404 }
1405 if (NetPlay.GamePassworded)
1406 {
1407 widgSetButtonState(psWScreen, MULTIOP_PASSWORD_BUT, WBUT_CLICKLOCK);
1408 widgSetButtonState(psWScreen, MULTIOP_PASSWORD_EDIT, WEDBS_DISABLE);
1409 }
1410 }
1411
1412 //just display the game options.
1413 addMultiEditBox(MULTIOP_OPTIONS, MULTIOP_PNAME, MCOL0, MROW1, _("Select Player Name"), (char *) sPlayer, IMAGE_EDIT_PLAYER, IMAGE_EDIT_PLAYER_HI, MULTIOP_PNAME_ICON);
1414
1415 auto optionsList = std::make_shared<ListWidget>();
1416 optionsForm->attach(optionsList);
1417 optionsList->setChildSize(MULTIOP_BLUEFORMW, 29);
1418 optionsList->setChildSpacing(2, 2);
1419 optionsList->setGeometry(MCOL0, MROW5, MULTIOP_BLUEFORMW, optionsForm->height() - MROW5);
1420
1421 auto scavengerChoice = std::make_shared<MultichoiceWidget>(game.scavengers);
1422 optionsList->attach(scavengerChoice);
1423 scavengerChoice->id = MULTIOP_GAMETYPE;
1424 scavengerChoice->setLabel(_("Scavengers"));
1425 if (game.mapHasScavengers)
1426 {
1427 addMultiButton(scavengerChoice, true, Image(FrontImages, IMAGE_SCAVENGERS_ON), Image(FrontImages, IMAGE_SCAVENGERS_ON_HI), _("Scavengers"));
1428 }
1429 addMultiButton(scavengerChoice, false, Image(FrontImages, IMAGE_SCAVENGERS_OFF), Image(FrontImages, IMAGE_SCAVENGERS_OFF_HI), _("No Scavengers"));
1430 scavengerChoice->enable(!locked.scavengers);
1431 optionsList->addWidgetToLayout(scavengerChoice);
1432
1433 auto allianceChoice = std::make_shared<MultichoiceWidget>(game.alliance);
1434 optionsList->attach(allianceChoice);
1435 allianceChoice->id = MULTIOP_ALLIANCES;
1436 allianceChoice->setLabel(_("Alliances"));
1437 addMultiButton(allianceChoice, NO_ALLIANCES, Image(FrontImages, IMAGE_NOALLI), Image(FrontImages, IMAGE_NOALLI_HI), _("No Alliances"));
1438 addMultiButton(allianceChoice, ALLIANCES, Image(FrontImages, IMAGE_ALLI), Image(FrontImages, IMAGE_ALLI_HI), _("Allow Alliances"));
1439 addMultiButton(allianceChoice, ALLIANCES_UNSHARED, Image(FrontImages, IMAGE_ALLI_UNSHARED), Image(FrontImages, IMAGE_ALLI_UNSHARED_HI), _("Locked Teams, No Shared Research"));
1440 addMultiButton(allianceChoice, ALLIANCES_TEAMS, Image(FrontImages, IMAGE_ALLI_TEAMS), Image(FrontImages, IMAGE_ALLI_TEAMS_HI), _("Locked Teams"));
1441 allianceChoice->enable(!locked.alliances);
1442 optionsList->addWidgetToLayout(allianceChoice);
1443
1444 auto powerChoice = std::make_shared<MultichoiceWidget>(game.power);
1445 optionsList->attach(powerChoice);
1446 powerChoice->id = MULTIOP_POWER;
1447 powerChoice->setLabel(_("Power"));
1448 addMultiButton(powerChoice, LEV_LOW, Image(FrontImages, IMAGE_POWLO), Image(FrontImages, IMAGE_POWLO_HI), _("Low Power Levels"));
1449 addMultiButton(powerChoice, LEV_MED, Image(FrontImages, IMAGE_POWMED), Image(FrontImages, IMAGE_POWMED_HI), _("Medium Power Levels"));
1450 addMultiButton(powerChoice, LEV_HI, Image(FrontImages, IMAGE_POWHI), Image(FrontImages, IMAGE_POWHI_HI), _("High Power Levels"));
1451 powerChoice->enable(!locked.power);
1452 optionsList->addWidgetToLayout(powerChoice);
1453
1454 auto baseTypeChoice = std::make_shared<MultichoiceWidget>(game.base);
1455 optionsList->attach(baseTypeChoice);
1456 baseTypeChoice->id = MULTIOP_BASETYPE;
1457 baseTypeChoice->setLabel(_("Base"));
1458 addMultiButton(baseTypeChoice, CAMP_CLEAN, Image(FrontImages, IMAGE_NOBASE), Image(FrontImages, IMAGE_NOBASE_HI), _("Start with No Bases"));
1459 addMultiButton(baseTypeChoice, CAMP_BASE, Image(FrontImages, IMAGE_SBASE), Image(FrontImages, IMAGE_SBASE_HI), _("Start with Bases"));
1460 addMultiButton(baseTypeChoice, CAMP_WALLS, Image(FrontImages, IMAGE_LBASE), Image(FrontImages, IMAGE_LBASE_HI), _("Start with Advanced Bases"));
1461 baseTypeChoice->enable(!locked.bases);
1462 optionsList->addWidgetToLayout(baseTypeChoice);
1463
1464 auto mapPreviewButton = std::make_shared<MultibuttonWidget>();
1465 optionsList->attach(mapPreviewButton);
1466 mapPreviewButton->id = MULTIOP_MAP_PREVIEW;
1467 mapPreviewButton->setLabel(_("Map Preview"));
1468 addMultiButton(mapPreviewButton, 0, Image(FrontImages, IMAGE_FOG_OFF), Image(FrontImages, IMAGE_FOG_OFF_HI), _("Click to see Map"));
1469 optionsList->addWidgetToLayout(mapPreviewButton);
1470
1471 /* Add additional controls if we are (or going to be) hosting the game */
1472 if (ingame.side == InGameSide::HOST_OR_SINGLEPLAYER)
1473 {
1474 auto structureLimitsLabel = challengeActive ? _("Show Structure Limits") : _("Set Structure Limits");
1475 auto structLimitsButton = std::make_shared<MultibuttonWidget>();
1476 optionsList->attach(structLimitsButton);
1477 structLimitsButton->id = MULTIOP_STRUCTLIMITS;
1478 structLimitsButton->setLabel(structureLimitsLabel);
1479 addMultiButton(structLimitsButton, 0, Image(FrontImages, IMAGE_SLIM), Image(FrontImages, IMAGE_SLIM_HI), structureLimitsLabel);
1480 optionsList->addWidgetToLayout(structLimitsButton);
1481
1482 /* ...and even more controls if we are not starting a challenge */
1483 if (!challengeActive)
1484 {
1485 auto randomButton = std::make_shared<MultibuttonWidget>();
1486 optionsList->attach(randomButton);
1487 randomButton->id = MULTIOP_RANDOM;
1488 randomButton->setLabel(_("Random Game Options"));
1489 addMultiButton(randomButton, 0, Image(FrontImages, IMAGE_RELOAD), Image(FrontImages, IMAGE_RELOAD), _("Random Game Options\nCan be blocked by players' votes"));
1490 randomButton->setButtonMinClickInterval(GAME_TICKS_PER_SEC / 2);
1491 optionsList->addWidgetToLayout(randomButton);
1492
1493 /* Add the tech level choice if we have already started hosting. The only real reason this is displayed only after
1494 starting the host is due to the fact that there is not enough room before the "Host Game" button is hidden. */
1495 if (NetPlay.isHost)
1496 {
1497 auto TechnologyChoice = std::make_shared<MultichoiceWidget>(game.techLevel);
1498 optionsList->attach(TechnologyChoice);
1499 TechnologyChoice->id = MULTIOP_TECHLEVEL;
1500 TechnologyChoice->setLabel(_("Tech"));
1501 addMultiButton(TechnologyChoice, TECH_1, Image(FrontImages, IMAGE_TECHLO), Image(FrontImages, IMAGE_TECHLO_HI), _("Technology Level 1"));
1502 addMultiButton(TechnologyChoice, TECH_2, Image(FrontImages, IMAGE_TECHMED), Image(FrontImages, IMAGE_TECHMED_HI), _("Technology Level 2"));
1503 addMultiButton(TechnologyChoice, TECH_3, Image(FrontImages, IMAGE_TECHHI), Image(FrontImages, IMAGE_TECHHI_HI), _("Technology Level 3"));
1504 addMultiButton(TechnologyChoice, TECH_4, Image(FrontImages, IMAGE_COMPUTER_Y), Image(FrontImages, IMAGE_COMPUTER_Y_HI), _("Technology Level 4"));
1505 optionsList->addWidgetToLayout(TechnologyChoice);
1506 }
1507 /* If not hosting (yet), add the button for starting the host. */
1508 else
1509 {
1510 auto hostButton = std::make_shared<MultibuttonWidget>();
1511 optionsList->attach(hostButton);
1512 hostButton->id = MULTIOP_HOST;
1513 hostButton->setLabel(_("Start Hosting Game"));
1514 addMultiButton(hostButton, 0, Image(FrontImages, IMAGE_HOST), Image(FrontImages, IMAGE_HOST_HI), _("Start Hosting Game"));
1515 optionsList->addWidgetToLayout(hostButton);
1516 }
1517 }
1518 }
1519
1520 // cancel
1521 addMultiBut(psWScreen, MULTIOP_OPTIONS, CON_CANCEL,
1522 MULTIOP_CANCELX, MULTIOP_CANCELY,
1523 iV_GetImageWidth(FrontImages, IMAGE_RETURN),
1524 iV_GetImageHeight(FrontImages, IMAGE_RETURN),
1525 _("Return To Previous Screen"), IMAGE_RETURN, IMAGE_RETURN_HI, IMAGE_RETURN_HI);
1526
1527 // Add any relevant factory disabled icons.
1528 updateStructureDisabledFlags();
1529 updateLimitIcons();
1530 }
1531
1532 // ////////////////////////////////////////////////////////////////////////////
1533 // Colour functions
1534
safeToUseColour(unsigned player,unsigned otherPlayer)1535 static bool safeToUseColour(unsigned player, unsigned otherPlayer)
1536 {
1537 // Player wants to take the colour from otherPlayer. May not take from a human otherPlayer, unless we're the host.
1538 return player == otherPlayer || NetPlay.isHost || !isHumanPlayer(otherPlayer);
1539 }
1540
getPlayerTeam(int i)1541 static int getPlayerTeam(int i)
1542 {
1543 return alliancesSetTeamsBeforeGame(game.alliance) ? NetPlay.players[i].team : i;
1544 }
1545
1546 /**
1547 * Checks if all players are on the same team. If so, return that team; if not, return -1;
1548 * if there are no players, return team MAX_PLAYERS.
1549 */
allPlayersOnSameTeam(int except)1550 static int allPlayersOnSameTeam(int except)
1551 {
1552 int minTeam = MAX_PLAYERS, maxTeam = 0, numPlayers = 0;
1553 for (unsigned i = 0; i < game.maxPlayers; ++i)
1554 {
1555 if (i != except && (NetPlay.players[i].allocated || NetPlay.players[i].ai >= 0))
1556 {
1557 int team = getPlayerTeam(i);
1558 minTeam = std::min(minTeam, team);
1559 maxTeam = std::max(maxTeam, team);
1560 ++numPlayers;
1561 }
1562 }
1563 if (minTeam == MAX_PLAYERS || minTeam == maxTeam)
1564 {
1565 return minTeam; // Players all on same team.
1566 }
1567 return -1; // Players not all on same team.
1568 }
1569
playerBoxHeight(int player)1570 static int playerBoxHeight(int player)
1571 {
1572 int gap = MULTIOP_PLAYERSH - MULTIOP_TEAMSHEIGHT * game.maxPlayers;
1573 int gapDiv = game.maxPlayers - 1;
1574 gap = std::min(gap, 5 * gapDiv);
1575 STATIC_ASSERT(MULTIOP_TEAMSHEIGHT == MULTIOP_PLAYERHEIGHT); // Why are these different defines?
1576 return (MULTIOP_TEAMSHEIGHT * gapDiv + gap) * NetPlay.players[player].position / gapDiv;
1577 }
1578
closeAllChoosers()1579 void WzMultiplayerOptionsTitleUI::closeAllChoosers()
1580 {
1581 closeColourChooser();
1582 closeTeamChooser();
1583 closeFactionChooser();
1584 closePositionChooser();
1585
1586 // AiChooser and DifficultyChooser currently use the same form, so to avoid a double-delete-later, do it once explicitly here
1587 widgDeleteLater(psInlineChooserOverlayScreen, MULTIOP_AI_FORM);
1588 widgDeleteLater(psInlineChooserOverlayScreen, FRONTEND_SIDETEXT2);
1589 aiChooserUp = -1;
1590 difficultyChooserUp = -1;
1591 widgRemoveOverlayScreen(psInlineChooserOverlayScreen);
1592 }
1593
initInlineChooser(uint32_t player)1594 void WzMultiplayerOptionsTitleUI::initInlineChooser(uint32_t player)
1595 {
1596 // delete everything on that player's row,
1597 widgDelete(psWScreen, MULTIOP_PLAYER_START + player);
1598 widgDelete(psWScreen, MULTIOP_TEAMS_START + player);
1599 widgDelete(psWScreen, MULTIOP_READY_FORM_ID + player);
1600 widgDelete(psWScreen, MULTIOP_COLOUR_START + player);
1601 widgDelete(psWScreen, MULTIOP_FACTION_START + player);
1602
1603 // remove any choosers already up
1604 closeAllChoosers();
1605
1606 widgRegisterOverlayScreen(psInlineChooserOverlayScreen, 1);
1607 }
1608
initRightSideChooser(const char * sideText)1609 IntFormAnimated* WzMultiplayerOptionsTitleUI::initRightSideChooser(const char* sideText)
1610 {
1611 // remove any choosers already up
1612 closeAllChoosers();
1613
1614 // delete everything on that player's row,
1615 widgDelete(psWScreen, MULTIOP_PLAYERS);
1616 widgDelete(psWScreen, FRONTEND_SIDETEXT2);
1617
1618 widgRegisterOverlayScreen(psInlineChooserOverlayScreen, 1);
1619
1620 WIDGET* psParent = widgGetFromID(psWScreen, FRONTEND_BACKDROP);
1621 if (psParent == nullptr)
1622 {
1623 return nullptr;
1624 }
1625 std::weak_ptr<WIDGET> psWeakParent(psParent->shared_from_this());
1626
1627 WIDGET *chooserParent = widgGetFromID(psInlineChooserOverlayScreen, MULTIOP_INLINE_OVERLAY_ROOT_FRM);
1628
1629 auto aiForm = std::make_shared<IntFormAnimated>(false);
1630 chooserParent->attach(aiForm);
1631 aiForm->id = MULTIOP_AI_FORM;
1632 aiForm->setCalcLayout([psWeakParent](WIDGET *psWidget, unsigned int, unsigned int, unsigned int, unsigned int){
1633 if (auto psParent = psWeakParent.lock())
1634 {
1635 psWidget->setGeometry(psParent->screenPosX() + MULTIOP_PLAYERSX, psParent->screenPosY() + MULTIOP_PLAYERSY, MULTIOP_PLAYERSW, MULTIOP_PLAYERSH);
1636 }
1637 });
1638
1639 W_LABEL *psSideTextLabel = addSideText(psInlineChooserOverlayScreen, MULTIOP_INLINE_OVERLAY_ROOT_FRM, FRONTEND_SIDETEXT2, MULTIOP_PLAYERSX - 3, MULTIOP_PLAYERSY, sideText);
1640 if (psSideTextLabel)
1641 {
1642 psSideTextLabel->setCalcLayout([psWeakParent](WIDGET *psWidget, unsigned int, unsigned int, unsigned int, unsigned int){
1643 if (auto psParent = psWeakParent.lock())
1644 {
1645 psWidget->setGeometry(psParent->screenPosX() + MULTIOP_PLAYERSX - 3, psParent->screenPosY() + MULTIOP_PLAYERSY, MULTIOP_PLAYERSW, MULTIOP_PLAYERSH);
1646 }
1647 });
1648 }
1649
1650 return aiForm.get();
1651 }
1652
addMultiButWithClickHandler(const std::shared_ptr<W_SCREEN> & screen,UDWORD formid,UDWORD id,UDWORD x,UDWORD y,UDWORD width,UDWORD height,const char * tipres,UDWORD norm,UDWORD down,UDWORD hi,const W_BUTTON::W_BUTTON_ONCLICK_FUNC & clickHandler,unsigned tc=MAX_PLAYERS)1653 static bool addMultiButWithClickHandler(const std::shared_ptr<W_SCREEN> &screen, UDWORD formid, UDWORD id, UDWORD x, UDWORD y, UDWORD width, UDWORD height, const char *tipres, UDWORD norm, UDWORD down, UDWORD hi, const W_BUTTON::W_BUTTON_ONCLICK_FUNC& clickHandler, unsigned tc = MAX_PLAYERS)
1654 {
1655 if (!addMultiBut(screen, formid, id, x, y, width, height, tipres, norm, down, hi, tc))
1656 {
1657 return false;
1658 }
1659 WzMultiButton *psButton = static_cast<WzMultiButton*>(widgGetFromID(screen, id));
1660 if (!psButton)
1661 {
1662 return false;
1663 }
1664 psButton->addOnClickHandler(clickHandler);
1665 return true;
1666 }
1667
openDifficultyChooser(uint32_t player)1668 void WzMultiplayerOptionsTitleUI::openDifficultyChooser(uint32_t player)
1669 {
1670 IntFormAnimated *aiForm = initRightSideChooser(_("DIFFICULTY"));
1671 if (!aiForm)
1672 {
1673 debug(LOG_ERROR, "Failed to initialize right-side chooser?");
1674 return;
1675 }
1676
1677 auto psWeakTitleUI = std::weak_ptr<WzMultiplayerOptionsTitleUI>(std::dynamic_pointer_cast<WzMultiplayerOptionsTitleUI>(shared_from_this()));
1678
1679 for (int difficultyIdx = 0; difficultyIdx < 4; difficultyIdx++)
1680 {
1681 auto onClickHandler = [psWeakTitleUI, difficultyIdx, player](W_BUTTON& clickedButton) {
1682 auto pStrongPtr = psWeakTitleUI.lock();
1683 ASSERT_OR_RETURN(, pStrongPtr.operator bool(), "WzMultiplayerOptionsTitleUI no longer exists");
1684 NetPlay.players[player].difficulty = difficultyValue[difficultyIdx];
1685 NETBroadcastPlayerInfo(player);
1686 pStrongPtr->closeDifficultyChooser();
1687 pStrongPtr->addPlayerBox(true);
1688 resetReadyStatus(false);
1689 };
1690
1691 W_BUTINIT sButInit;
1692 sButInit.formID = MULTIOP_AI_FORM;
1693 sButInit.id = MULTIOP_DIFFICULTY_CHOOSE_START + difficultyIdx;
1694 sButInit.x = 7;
1695 sButInit.y = (MULTIOP_PLAYERHEIGHT + 5) * difficultyIdx + 4;
1696 sButInit.width = MULTIOP_PLAYERWIDTH + 1;
1697 sButInit.height = MULTIOP_PLAYERHEIGHT;
1698 switch (difficultyIdx)
1699 {
1700 case 0: sButInit.pTip = _("Starts disadvantaged"); break;
1701 case 1: sButInit.pTip = _("Plays nice"); break;
1702 case 2: sButInit.pTip = _("No holds barred"); break;
1703 case 3: sButInit.pTip = _("Starts with advantages"); break;
1704 }
1705 const char *difficultyTip = aidata[NetPlay.players[player].ai].difficultyTips[difficultyIdx];
1706 if (strcmp(difficultyTip, "") != 0)
1707 {
1708 sButInit.pTip += "\n";
1709 sButInit.pTip += difficultyTip;
1710 }
1711 sButInit.pDisplay = displayDifficulty;
1712 sButInit.UserData = difficultyIdx;
1713 sButInit.pUserData = new DisplayDifficultyCache();
1714 sButInit.onDelete = [](WIDGET *psWidget) {
1715 assert(psWidget->pUserData != nullptr);
1716 delete static_cast<DisplayDifficultyCache *>(psWidget->pUserData);
1717 psWidget->pUserData = nullptr;
1718 };
1719 auto psButton = widgAddButton(psInlineChooserOverlayScreen, &sButInit);
1720 if (psButton)
1721 {
1722 psButton->addOnClickHandler(onClickHandler);
1723 }
1724 }
1725
1726 difficultyChooserUp = player;
1727 }
1728
openAiChooser(uint32_t player)1729 void WzMultiplayerOptionsTitleUI::openAiChooser(uint32_t player)
1730 {
1731 IntFormAnimated *aiForm = initRightSideChooser(_("CHOOSE AI"));
1732 if (!aiForm)
1733 {
1734 debug(LOG_ERROR, "Failed to initialize right-side chooser?");
1735 return;
1736 }
1737
1738 auto psWeakTitleUI = std::weak_ptr<WzMultiplayerOptionsTitleUI>(std::dynamic_pointer_cast<WzMultiplayerOptionsTitleUI>(shared_from_this()));
1739
1740 W_BUTINIT sButInit;
1741 sButInit.formID = MULTIOP_AI_FORM;
1742 sButInit.x = 7;
1743 sButInit.width = MULTIOP_PLAYERWIDTH + 1;
1744 sButInit.height = MULTIOP_PLAYERHEIGHT;
1745 sButInit.pDisplay = displayAi;
1746 sButInit.initPUserDataFunc = []() -> void * { return new DisplayAICache(); };
1747 sButInit.onDelete = [](WIDGET *psWidget) {
1748 assert(psWidget->pUserData != nullptr);
1749 delete static_cast<DisplayAICache *>(psWidget->pUserData);
1750 psWidget->pUserData = nullptr;
1751 };
1752
1753 // only need this button in (true) mp games
1754 int mpbutton = NetPlay.bComms ? 1 : 0;
1755
1756 auto openCloseOnClickHandler = [psWeakTitleUI, player](W_BUTTON& clickedButton) {
1757 auto pStrongPtr = psWeakTitleUI.lock();
1758 ASSERT_OR_RETURN(, pStrongPtr.operator bool(), "WzMultiplayerOptionsTitleUI no longer exists");
1759
1760 switch (clickedButton.id)
1761 {
1762 case MULTIOP_AI_CLOSED:
1763 NetPlay.players[player].ai = AI_CLOSED;
1764 break;
1765 case MULTIOP_AI_OPEN:
1766 NetPlay.players[player].ai = AI_OPEN;
1767 break;
1768 default:
1769 debug(LOG_ERROR, "Unexpected button id");
1770 return;
1771 break;
1772 }
1773
1774 // common code
1775 NetPlay.players[player].difficulty = AIDifficulty::DISABLED; // disable AI for this slot
1776 NETBroadcastPlayerInfo(player);
1777 pStrongPtr->closeAiChooser();
1778 pStrongPtr->addPlayerBox(true);
1779 resetReadyStatus(false);
1780 ActivityManager::instance().updateMultiplayGameData(game, ingame, NETGameIsLocked());
1781 };
1782
1783 // Open button
1784 if (mpbutton)
1785 {
1786 sButInit.id = MULTIOP_AI_OPEN;
1787 sButInit.pTip = _("Allow human players to join in this slot");
1788 sButInit.UserData = (UDWORD)AI_OPEN;
1789 sButInit.y = 3; //Top most position
1790 auto psButton = widgAddButton(psInlineChooserOverlayScreen, &sButInit);
1791 if (psButton)
1792 {
1793 psButton->addOnClickHandler(openCloseOnClickHandler);
1794 }
1795 }
1796
1797 // Closed button
1798 sButInit.pTip = _("Leave this slot unused");
1799 sButInit.id = MULTIOP_AI_CLOSED;
1800 sButInit.UserData = (UDWORD)AI_CLOSED;
1801 if (mpbutton)
1802 {
1803 sButInit.y = sButInit.y + sButInit.height;
1804 }
1805 else
1806 {
1807 sButInit.y = 3; //since we don't have the lone mpbutton, we can start at position 0
1808 }
1809 auto psCloseButton = widgAddButton(psInlineChooserOverlayScreen, &sButInit);
1810 if (psCloseButton)
1811 {
1812 psCloseButton->addOnClickHandler(openCloseOnClickHandler);
1813 }
1814
1815 auto pAIScrollableList = ScrollableListWidget::make();
1816 aiForm->attach(pAIScrollableList);
1817 pAIScrollableList->setBackgroundColor(WZCOL_TRANSPARENT_BOX);
1818 int aiListStartXPos = sButInit.x;
1819 int aiListStartYPos = (sButInit.height + sButInit.y) + 10;
1820 int aiListEntryHeight = sButInit.height;
1821 int aiListEntryWidth = sButInit.width;
1822 int aiListHeight = aiListEntryHeight * (mpbutton ? 7 : 8);
1823 pAIScrollableList->setCalcLayout([aiListStartXPos, aiListStartYPos, aiListEntryWidth, aiListHeight](WIDGET *psWidget, unsigned int, unsigned int, unsigned int, unsigned int){
1824 psWidget->setGeometry(aiListStartXPos, aiListStartYPos, aiListEntryWidth, aiListHeight);
1825 });
1826
1827 W_BUTINIT emptyInit;
1828 for (size_t aiIdx = 0; aiIdx < aidata.size(); aiIdx++)
1829 {
1830 auto pAIRow = std::make_shared<W_BUTTON>(&emptyInit);
1831 pAIRow->setTip(aidata[aiIdx].tip);
1832 pAIRow->id = MULTIOP_AI_START + aiIdx;
1833 pAIRow->UserData = aiIdx;
1834 pAIRow->setGeometry(0, 0, sButInit.width, sButInit.height);
1835 pAIRow->displayFunction = displayAi;
1836 pAIRow->pUserData = new DisplayAICache();
1837 pAIRow->setOnDelete([](WIDGET *psWidget) {
1838 assert(psWidget->pUserData != nullptr);
1839 delete static_cast<DisplayAICache *>(psWidget->pUserData);
1840 psWidget->pUserData = nullptr;
1841 });
1842 pAIRow->addOnClickHandler([psWeakTitleUI, aiIdx, player](W_BUTTON& clickedButton) {
1843 auto pStrongPtr = psWeakTitleUI.lock();
1844 ASSERT_OR_RETURN(, pStrongPtr.operator bool(), "WzMultiplayerOptionsTitleUI no longer exists");
1845 NetPlay.players[player].ai = aiIdx;
1846 sstrcpy(NetPlay.players[player].name, getAIName(player));
1847 NetPlay.players[player].difficulty = AIDifficulty::MEDIUM;
1848 NETBroadcastPlayerInfo(player);
1849 pStrongPtr->closeAiChooser();
1850 pStrongPtr->addPlayerBox(true);
1851 resetReadyStatus(false);
1852 ActivityManager::instance().updateMultiplayGameData(game, ingame, NETGameIsLocked());
1853 });
1854 pAIScrollableList->addItem(pAIRow);
1855 }
1856
1857 aiChooserUp = player;
1858 }
1859
openPositionChooser(uint32_t player)1860 void WzMultiplayerOptionsTitleUI::openPositionChooser(uint32_t player)
1861 {
1862 closeAllChoosers();
1863
1864 positionChooserUp = player;
1865 addPlayerBox(true);
1866 }
1867
1868 static bool SendTeamRequest(UBYTE player, UBYTE chosenTeam); // forward-declare
1869
openTeamChooser(uint32_t player)1870 void WzMultiplayerOptionsTitleUI::openTeamChooser(uint32_t player)
1871 {
1872 UDWORD i;
1873 int disallow = allPlayersOnSameTeam(player);
1874
1875 bool canChangeTeams = !locked.teams;
1876 bool canKickPlayer = (player != selectedPlayer && NetPlay.bComms && NetPlay.isHost && NetPlay.players[player].allocated);
1877 if (!canChangeTeams && !canKickPlayer)
1878 {
1879 return;
1880 }
1881
1882 debug(LOG_NET, "Opened team chooser for %d, current team: %d", player, NetPlay.players[player].team);
1883
1884 initInlineChooser(player);
1885
1886 // add form.
1887 auto psParentForm = (W_FORM *)widgGetFromID(psWScreen, MULTIOP_PLAYERS);
1888 addInlineChooserBlueForm(psInlineChooserOverlayScreen, psParentForm, MULTIOP_TEAMCHOOSER_FORM, "", 8, playerBoxHeight(player), MULTIOP_ROW_WIDTH, MULTIOP_TEAMSHEIGHT);
1889
1890 auto psWeakTitleUI = std::weak_ptr<WzMultiplayerOptionsTitleUI>(std::dynamic_pointer_cast<WzMultiplayerOptionsTitleUI>(shared_from_this()));
1891
1892 if (canChangeTeams)
1893 {
1894 int teamW = iV_GetImageWidth(FrontImages, IMAGE_TEAM0);
1895 int teamH = iV_GetImageHeight(FrontImages, IMAGE_TEAM0);
1896 int space = MULTIOP_ROW_WIDTH - 4 - teamW * (game.maxPlayers + 1);
1897 int spaceDiv = game.maxPlayers;
1898 space = std::min(space, 3 * spaceDiv);
1899
1900 auto onClickHandler = [player, psWeakTitleUI](W_BUTTON &button) {
1901 UDWORD id = button.id;
1902 auto pStrongPtr = psWeakTitleUI.lock();
1903 ASSERT_OR_RETURN(, pStrongPtr.operator bool(), "WzMultiplayerOptionsTitleUI no longer exists");
1904
1905 //clicked on a team
1906 STATIC_ASSERT(MULTIOP_TEAMCHOOSER + MAX_PLAYERS - 1 <= MULTIOP_TEAMCHOOSER_END);
1907 if (id >= MULTIOP_TEAMCHOOSER && id <= MULTIOP_TEAMCHOOSER + MAX_PLAYERS - 1)
1908 {
1909 ASSERT(id >= MULTIOP_TEAMCHOOSER
1910 && (id - MULTIOP_TEAMCHOOSER) < MAX_PLAYERS, "processMultiopWidgets: wrong id - MULTIOP_TEAMCHOOSER value (%d)", id - MULTIOP_TEAMCHOOSER);
1911
1912 resetReadyStatus(false); // will reset only locally if not a host
1913
1914 SendTeamRequest(player, (UBYTE)id - MULTIOP_TEAMCHOOSER);
1915
1916 debug(LOG_WZ, "Changed team for player %d to %d", player, NetPlay.players[player].team);
1917
1918 pStrongPtr->closeTeamChooser();
1919
1920 // restore player list
1921 pStrongPtr->addPlayerBox(true);
1922 }
1923 };
1924
1925 // add the teams, skipping the one we CAN'T be on (if applicable)
1926 for (i = 0; i < game.maxPlayers; i++)
1927 {
1928 if (i != disallow)
1929 {
1930 addMultiButWithClickHandler(psInlineChooserOverlayScreen, MULTIOP_TEAMCHOOSER_FORM, MULTIOP_TEAMCHOOSER + i, i * (teamW * spaceDiv + space) / spaceDiv + 3, 6, teamW, teamH, _("Team"), IMAGE_TEAM0 + i, IMAGE_TEAM0_HI + i, IMAGE_TEAM0_HI + i, onClickHandler);
1931 }
1932 // may want to add some kind of 'can't do' icon instead of being blank?
1933 }
1934 }
1935
1936 // add a kick button
1937 if (canKickPlayer)
1938 {
1939 auto onClickHandler = [player, psWeakTitleUI](W_BUTTON &button) {
1940 auto pStrongPtr = psWeakTitleUI.lock();
1941 ASSERT_OR_RETURN(, pStrongPtr.operator bool(), "WzMultiplayerOptionsTitleUI no longer exists");
1942
1943 std::string msg = astringf(_("The host has kicked %s from the game!"), getPlayerName(player));
1944 kickPlayer(player, _("The host has kicked you from the game."), ERROR_KICKED);
1945 sendRoomSystemMessage(msg.c_str());
1946 resetReadyStatus(true); //reset and send notification to all clients
1947 pStrongPtr->closeTeamChooser();
1948 };
1949
1950 const int imgwidth = iV_GetImageWidth(FrontImages, IMAGE_NOJOIN);
1951 const int imgheight = iV_GetImageHeight(FrontImages, IMAGE_NOJOIN);
1952 addMultiButWithClickHandler(psInlineChooserOverlayScreen, MULTIOP_TEAMCHOOSER_FORM, MULTIOP_TEAMCHOOSER_KICK, MULTIOP_ROW_WIDTH - imgwidth - 4, 8, imgwidth, imgheight,
1953 ("Kick player"), IMAGE_NOJOIN, IMAGE_NOJOIN, IMAGE_NOJOIN, onClickHandler);
1954 }
1955
1956 inlineChooserUp = player;
1957 }
1958
openColourChooser(uint32_t player)1959 void WzMultiplayerOptionsTitleUI::openColourChooser(uint32_t player)
1960 {
1961 ASSERT_OR_RETURN(, player < MAX_PLAYERS, "Invalid player number");
1962 initInlineChooser(player);
1963
1964 // add form.
1965 auto psParentForm = (W_FORM *)widgGetFromID(psWScreen, MULTIOP_PLAYERS);
1966 addInlineChooserBlueForm(psInlineChooserOverlayScreen, psParentForm, MULTIOP_COLCHOOSER_FORM, "",
1967 7,
1968 playerBoxHeight(player),
1969 MULTIOP_ROW_WIDTH, MULTIOP_PLAYERHEIGHT);
1970
1971 // add the flags
1972 int flagW = iV_GetImageWidth(FrontImages, IMAGE_PLAYERN);
1973 int flagH = iV_GetImageHeight(FrontImages, IMAGE_PLAYERN);
1974 int space = MULTIOP_ROW_WIDTH - 0 - flagW * MAX_PLAYERS_IN_GUI;
1975 int spaceDiv = MAX_PLAYERS_IN_GUI;
1976 space = std::min(space, 5 * spaceDiv);
1977
1978 auto psWeakTitleUI = std::weak_ptr<WzMultiplayerOptionsTitleUI>(std::dynamic_pointer_cast<WzMultiplayerOptionsTitleUI>(shared_from_this()));
1979
1980 for (unsigned i = 0; i < MAX_PLAYERS_IN_GUI; i++)
1981 {
1982 auto onClickHandler = [player, psWeakTitleUI](W_BUTTON &button) {
1983 UDWORD id = button.id;
1984 auto pStrongPtr = psWeakTitleUI.lock();
1985 ASSERT_OR_RETURN(, pStrongPtr.operator bool(), "WzMultiplayerOptionsTitleUI no longer exists");
1986
1987 STATIC_ASSERT(MULTIOP_COLCHOOSER + MAX_PLAYERS - 1 <= MULTIOP_COLCHOOSER_END);
1988 if (id >= MULTIOP_COLCHOOSER && id < MULTIOP_COLCHOOSER + MAX_PLAYERS - 1) // chose a new colour.
1989 {
1990 resetReadyStatus(false, true); // will reset only locally if not a host
1991 SendColourRequest(player, id - MULTIOP_COLCHOOSER);
1992 pStrongPtr->closeColourChooser();
1993 pStrongPtr->addPlayerBox(true);
1994 }
1995 };
1996
1997 addMultiButWithClickHandler(psInlineChooserOverlayScreen, MULTIOP_COLCHOOSER_FORM, MULTIOP_COLCHOOSER + getPlayerColour(i),
1998 i * (flagW * spaceDiv + space) / spaceDiv + 4, 4, // x, y
1999 flagW, flagH, // w, h
2000 getPlayerColourName(i), IMAGE_PLAYERN, IMAGE_PLAYERN_HI, IMAGE_PLAYERN_HI, onClickHandler, getPlayerColour(i)
2001 );
2002
2003 if (!safeToUseColour(selectedPlayer, i))
2004 {
2005 widgSetButtonState(psInlineChooserOverlayScreen, MULTIOP_COLCHOOSER + getPlayerColour(i), WBUT_DISABLE);
2006 }
2007 }
2008
2009 inlineChooserUp = player;
2010 }
2011
closeColourChooser()2012 void WzMultiplayerOptionsTitleUI::closeColourChooser()
2013 {
2014 inlineChooserUp = -1;
2015 widgDeleteLater(psInlineChooserOverlayScreen, MULTIOP_COLCHOOSER_FORM);
2016 widgRemoveOverlayScreen(psInlineChooserOverlayScreen);
2017 }
2018
closeTeamChooser()2019 void WzMultiplayerOptionsTitleUI::closeTeamChooser()
2020 {
2021 inlineChooserUp = -1;
2022 widgDeleteLater(psInlineChooserOverlayScreen, MULTIOP_TEAMCHOOSER_FORM);
2023 widgRemoveOverlayScreen(psInlineChooserOverlayScreen);
2024 }
2025
closeFactionChooser()2026 void WzMultiplayerOptionsTitleUI::closeFactionChooser()
2027 {
2028 inlineChooserUp = -1;
2029 widgDeleteLater(psInlineChooserOverlayScreen, MULTIOP_FACCHOOSER_FORM);
2030 widgRemoveOverlayScreen(psInlineChooserOverlayScreen);
2031 }
2032
closeAiChooser()2033 void WzMultiplayerOptionsTitleUI::closeAiChooser()
2034 {
2035 // AiChooser and DifficultyChooser currently use the same formID
2036 // Just call closeAllChoosers() for now
2037 closeAllChoosers();
2038 }
2039
closeDifficultyChooser()2040 void WzMultiplayerOptionsTitleUI::closeDifficultyChooser()
2041 {
2042 // AiChooser and DifficultyChooser currently use the same formID
2043 // Just call closeAllChoosers() for now
2044 closeAllChoosers();
2045 }
2046
closePositionChooser()2047 void WzMultiplayerOptionsTitleUI::closePositionChooser()
2048 {
2049 positionChooserUp = -1;
2050 }
2051
openFactionChooser(uint32_t player)2052 void WzMultiplayerOptionsTitleUI::openFactionChooser(uint32_t player)
2053 {
2054 ASSERT_OR_RETURN(, player < MAX_PLAYERS, "Invalid player number");
2055 initInlineChooser(player);
2056
2057 // add form.
2058 auto psParentForm = (W_FORM *)widgGetFromID(psWScreen, MULTIOP_PLAYERS);
2059 addInlineChooserBlueForm(psInlineChooserOverlayScreen, psParentForm, MULTIOP_FACCHOOSER_FORM, "",
2060 7,
2061 playerBoxHeight(player),
2062 MULTIOP_ROW_WIDTH, MULTIOP_PLAYERHEIGHT);
2063
2064 // add the flags
2065 int flagW = iV_GetImageWidth(FrontImages, IMAGE_FACTION_NORMAL) + 4;
2066 int flagH = iV_GetImageHeight(FrontImages, IMAGE_PLAYERN);
2067 int space = MULTIOP_ROW_WIDTH - 0 - flagW * NUM_FACTIONS;
2068 int spaceDiv = NUM_FACTIONS;
2069 space = std::min(space, 5 * spaceDiv);
2070
2071 auto psWeakTitleUI = std::weak_ptr<WzMultiplayerOptionsTitleUI>(std::dynamic_pointer_cast<WzMultiplayerOptionsTitleUI>(shared_from_this()));
2072
2073 for (unsigned int i = 0; i < NUM_FACTIONS; i++)
2074 {
2075 auto onClickHandler = [player, psWeakTitleUI](W_BUTTON &button) {
2076 UDWORD id = button.id;
2077 auto pStrongPtr = psWeakTitleUI.lock();
2078 ASSERT_OR_RETURN(, pStrongPtr.operator bool(), "WzMultiplayerOptionsTitleUI no longer exists");
2079
2080 STATIC_ASSERT(MULTIOP_FACCHOOSER + NUM_FACTIONS - 1 <= MULTIOP_FACCHOOSER_END);
2081 if (id >= MULTIOP_FACCHOOSER && id <= MULTIOP_FACCHOOSER + NUM_FACTIONS -1)
2082 {
2083 resetReadyStatus(false, true);
2084 uint8_t idx = id - MULTIOP_FACCHOOSER;
2085 SendFactionRequest(player, idx);
2086 pStrongPtr->closeFactionChooser();
2087 pStrongPtr->addPlayerBox(true);
2088 }
2089 };
2090
2091 addMultiButWithClickHandler(psInlineChooserOverlayScreen, MULTIOP_FACCHOOSER_FORM, MULTIOP_FACCHOOSER + i,
2092 i * (flagW * spaceDiv + space) / spaceDiv + 7, 4, // x, y
2093 flagW, flagH, // w, h
2094 to_localized_string(static_cast<FactionID>(i)),
2095 IMAGE_FACTION_NORMAL+i, IMAGE_FACTION_NORMAL_HI+i, IMAGE_FACTION_NORMAL_HI+i, onClickHandler
2096 );
2097 }
2098
2099 inlineChooserUp = player;
2100 }
2101
changeTeam(UBYTE player,UBYTE team)2102 static void changeTeam(UBYTE player, UBYTE team)
2103 {
2104 NetPlay.players[player].team = team;
2105 debug(LOG_WZ, "set %d as new team for player %d", team, player);
2106 NETBroadcastPlayerInfo(player);
2107 netPlayersUpdated = true;
2108 }
2109
SendTeamRequest(UBYTE player,UBYTE chosenTeam)2110 static bool SendTeamRequest(UBYTE player, UBYTE chosenTeam)
2111 {
2112 if (NetPlay.isHost) // do or request the change.
2113 {
2114 changeTeam(player, chosenTeam); // do the change, remember only the host can do this to avoid confusion.
2115 }
2116 else
2117 {
2118 NETbeginEncode(NETnetQueue(NET_HOST_ONLY), NET_TEAMREQUEST);
2119
2120 NETuint8_t(&player);
2121 NETuint8_t(&chosenTeam);
2122
2123 NETend();
2124
2125 }
2126 return true;
2127 }
2128
recvTeamRequest(NETQUEUE queue)2129 bool recvTeamRequest(NETQUEUE queue)
2130 {
2131 ASSERT_HOST_ONLY(return true);
2132
2133 NETbeginDecode(queue, NET_TEAMREQUEST);
2134
2135 UBYTE player, team;
2136 NETuint8_t(&player);
2137 NETuint8_t(&team);
2138 NETend();
2139
2140 if (player >= MAX_PLAYERS || team >= MAX_PLAYERS)
2141 {
2142 debug(LOG_NET, "NET_TEAMREQUEST invalid, player %d team, %d", (int) player, (int) team);
2143 debug(LOG_ERROR, "Invalid NET_TEAMREQUEST from player %d: Tried to change player %d (team %d)",
2144 queue.index, (int)player, (int)team);
2145 return false;
2146 }
2147
2148 if (whosResponsible(player) != queue.index)
2149 {
2150 HandleBadParam("NET_TEAMREQUEST given incorrect params.", player, queue.index);
2151 return false;
2152 }
2153
2154 if (locked.teams)
2155 {
2156 return false;
2157 }
2158
2159 if (NetPlay.players[player].team != team)
2160 {
2161 resetReadyStatus(false);
2162 }
2163 debug(LOG_NET, "%s is now part of team: %d", NetPlay.players[player].name, (int) team);
2164 changeTeam(player, team); // we do this regardless, in case of sync issues
2165
2166 return true;
2167 }
2168
SendReadyRequest(UBYTE player,bool bReady)2169 static bool SendReadyRequest(UBYTE player, bool bReady)
2170 {
2171 if (NetPlay.isHost) // do or request the change.
2172 {
2173 return changeReadyStatus(player, bReady);
2174 }
2175 else
2176 {
2177 NETbeginEncode(NETnetQueue(NET_HOST_ONLY), NET_READY_REQUEST);
2178 NETuint8_t(&player);
2179 NETbool(&bReady);
2180 NETend();
2181 }
2182 return true;
2183 }
2184
recvReadyRequest(NETQUEUE queue)2185 bool recvReadyRequest(NETQUEUE queue)
2186 {
2187 ASSERT_HOST_ONLY(return true);
2188
2189 NETbeginDecode(queue, NET_READY_REQUEST);
2190
2191 UBYTE player;
2192 bool bReady = false;
2193 NETuint8_t(&player);
2194 NETbool(&bReady);
2195 NETend();
2196
2197 if (player >= MAX_PLAYERS)
2198 {
2199 debug(LOG_ERROR, "Invalid NET_READY_REQUEST from player %d: player id = %d",
2200 queue.index, (int)player);
2201 return false;
2202 }
2203
2204 if (whosResponsible(player) != queue.index)
2205 {
2206 HandleBadParam("NET_READY_REQUEST given incorrect params.", player, queue.index);
2207 return false;
2208 }
2209
2210 // do not allow players to select 'ready' if we are sending a map too them!
2211 // TODO: make a new icon to show this state?
2212 if (!NetPlay.players[player].wzFiles.empty())
2213 {
2214 return false;
2215 }
2216
2217 return changeReadyStatus((UBYTE)player, bReady);
2218 }
2219
changeReadyStatus(UBYTE player,bool bReady)2220 bool changeReadyStatus(UBYTE player, bool bReady)
2221 {
2222 NetPlay.players[player].ready = bReady;
2223 NETBroadcastPlayerInfo(player);
2224 netPlayersUpdated = true;
2225
2226 return true;
2227 }
2228
changePosition(UBYTE player,UBYTE position)2229 static bool changePosition(UBYTE player, UBYTE position)
2230 {
2231 int i;
2232
2233 for (i = 0; i < MAX_PLAYERS; i++)
2234 {
2235 if (NetPlay.players[i].position == position)
2236 {
2237 debug(LOG_NET, "Swapping positions between players %d(%d) and %d(%d)",
2238 player, NetPlay.players[player].position, i, NetPlay.players[i].position);
2239 std::swap(NetPlay.players[i].position, NetPlay.players[player].position);
2240 std::swap(NetPlay.players[i].team, NetPlay.players[player].team);
2241 NETBroadcastTwoPlayerInfo(player, i);
2242 netPlayersUpdated = true;
2243 return true;
2244 }
2245 }
2246 debug(LOG_ERROR, "Failed to swap positions for player %d, position %d", (int)player, (int)position);
2247 if (player < game.maxPlayers && position < game.maxPlayers)
2248 {
2249 debug(LOG_NET, "corrupted positions: player (%u) new position (%u) old position (%d)", player, position, NetPlay.players[player].position);
2250 // Positions were corrupted. Attempt to fix.
2251 NetPlay.players[player].position = position;
2252 NETBroadcastPlayerInfo(player);
2253 netPlayersUpdated = true;
2254 return true;
2255 }
2256 return false;
2257 }
2258
changeColour(unsigned player,int col,bool isHost)2259 bool changeColour(unsigned player, int col, bool isHost)
2260 {
2261 if (col < 0 || col >= MAX_PLAYERS_IN_GUI)
2262 {
2263 return true;
2264 }
2265
2266 if (getPlayerColour(player) == col)
2267 {
2268 return true; // Nothing to do.
2269 }
2270
2271 for (unsigned i = 0; i < MAX_PLAYERS; ++i)
2272 {
2273 if (getPlayerColour(i) == col)
2274 {
2275 if (!isHost && NetPlay.players[i].allocated)
2276 {
2277 return true; // May not swap.
2278 }
2279
2280 debug(LOG_NET, "Swapping colours between players %d(%d) and %d(%d)",
2281 player, getPlayerColour(player), i, getPlayerColour(i));
2282 setPlayerColour(i, getPlayerColour(player));
2283 setPlayerColour(player, col);
2284 NETBroadcastTwoPlayerInfo(player, i);
2285 netPlayersUpdated = true;
2286 return true;
2287 }
2288 }
2289 debug(LOG_ERROR, "Failed to swap colours for player %d, colour %d", (int)player, (int)col);
2290 if (player < game.maxPlayers && col < MAX_PLAYERS)
2291 {
2292 // Colours were corrupted. Attempt to fix.
2293 debug(LOG_NET, "corrupted colours: player (%u) new colour (%u) old colour (%d)", player, col, NetPlay.players[player].colour);
2294 setPlayerColour(player, col);
2295 NETBroadcastPlayerInfo(player);
2296 netPlayersUpdated = true;
2297 return true;
2298 }
2299 return false;
2300 }
2301
SendColourRequest(UBYTE player,UBYTE col)2302 static bool SendColourRequest(UBYTE player, UBYTE col)
2303 {
2304 if (NetPlay.isHost) // do or request the change
2305 {
2306 return changeColour(player, col, true);
2307 }
2308 else
2309 {
2310 // clients tell the host which color they want
2311 NETbeginEncode(NETnetQueue(NET_HOST_ONLY), NET_COLOURREQUEST);
2312 NETuint8_t(&player);
2313 NETuint8_t(&col);
2314 NETend();
2315 }
2316 return true;
2317 }
2318
SendFactionRequest(UBYTE player,UBYTE faction)2319 static bool SendFactionRequest(UBYTE player, UBYTE faction)
2320 {
2321 // TODO: needs to be rewritten from scratch
2322 ASSERT_OR_RETURN(false, faction <= static_cast<UBYTE>(MAX_FACTION_ID), "Invalid faction: %u", (unsigned int)faction);
2323 if (NetPlay.isHost) // do or request the change
2324 {
2325 NetPlay.players[player].faction = static_cast<FactionID>(faction);
2326 NETBroadcastPlayerInfo(player);
2327 return true;
2328 }
2329 else
2330 {
2331 // clients tell the host which color they want
2332 NETbeginEncode(NETnetQueue(NET_HOST_ONLY), NET_FACTIONREQUEST);
2333 NETuint8_t(&player);
2334 NETuint8_t(&faction);
2335 NETend();
2336 }
2337 return true;
2338 }
2339
SendPositionRequest(UBYTE player,UBYTE position)2340 static bool SendPositionRequest(UBYTE player, UBYTE position)
2341 {
2342 if (NetPlay.isHost) // do or request the change
2343 {
2344 return changePosition(player, position);
2345 }
2346 else
2347 {
2348 debug(LOG_NET, "Requesting the host to change our position. From %d to %d", player, position);
2349 // clients tell the host which position they want
2350 NETbeginEncode(NETnetQueue(NET_HOST_ONLY), NET_POSITIONREQUEST);
2351 NETuint8_t(&player);
2352 NETuint8_t(&position);
2353 NETend();
2354 }
2355 return true;
2356 }
2357
recvFactionRequest(NETQUEUE queue)2358 bool recvFactionRequest(NETQUEUE queue)
2359 {
2360 ASSERT_HOST_ONLY(return true);
2361
2362 NETbeginDecode(queue, NET_FACTIONREQUEST);
2363
2364 UBYTE player, faction;
2365 NETuint8_t(&player);
2366 NETuint8_t(&faction);
2367 NETend();
2368
2369 if (player >= MAX_PLAYERS)
2370 {
2371 debug(LOG_ERROR, "Invalid NET_FACTIONREQUEST from player %d: Tried to change player %d to faction %d",
2372 queue.index, (int)player, (int)faction);
2373 return false;
2374 }
2375
2376 if (whosResponsible(player) != queue.index)
2377 {
2378 HandleBadParam("NET_FACTIONREQUEST given incorrect params.", player, queue.index);
2379 return false;
2380 }
2381
2382 auto newFactionId = uintToFactionID(faction);
2383 if (!newFactionId.has_value())
2384 {
2385 HandleBadParam("NET_FACTIONREQUEST given incorrect params.", player, queue.index);
2386 return false;
2387 }
2388
2389 resetReadyStatus(false, true);
2390
2391 NetPlay.players[player].faction = newFactionId.value();
2392 NETBroadcastPlayerInfo(player);
2393 return true;
2394 }
2395
recvColourRequest(NETQUEUE queue)2396 bool recvColourRequest(NETQUEUE queue)
2397 {
2398 ASSERT_HOST_ONLY(return true);
2399
2400 NETbeginDecode(queue, NET_COLOURREQUEST);
2401
2402 UBYTE player, col;
2403 NETuint8_t(&player);
2404 NETuint8_t(&col);
2405 NETend();
2406
2407 if (player >= MAX_PLAYERS)
2408 {
2409 debug(LOG_ERROR, "Invalid NET_COLOURREQUEST from player %d: Tried to change player %d to colour %d",
2410 queue.index, (int)player, (int)col);
2411 return false;
2412 }
2413
2414 if (whosResponsible(player) != queue.index)
2415 {
2416 HandleBadParam("NET_COLOURREQUEST given incorrect params.", player, queue.index);
2417 return false;
2418 }
2419
2420 resetReadyStatus(false, true);
2421
2422 return changeColour(player, col, false);
2423 }
2424
recvPositionRequest(NETQUEUE queue)2425 bool recvPositionRequest(NETQUEUE queue)
2426 {
2427 ASSERT_HOST_ONLY(return true);
2428
2429 NETbeginDecode(queue, NET_POSITIONREQUEST);
2430
2431 UBYTE player, position;
2432 NETuint8_t(&player);
2433 NETuint8_t(&position);
2434 NETend();
2435 debug(LOG_NET, "Host received position request from player %d to %d", player, position);
2436
2437 if (player >= MAX_PLAYERS || position >= MAX_PLAYERS)
2438 {
2439 debug(LOG_ERROR, "Invalid NET_POSITIONREQUEST from player %d: Tried to change player %d to %d",
2440 queue.index, (int)player, (int)position);
2441 return false;
2442 }
2443
2444 if (whosResponsible(player) != queue.index)
2445 {
2446 HandleBadParam("NET_POSITIONREQUEST given incorrect params.", player, queue.index);
2447 return false;
2448 }
2449
2450 if (locked.position)
2451 {
2452 return false;
2453 }
2454
2455 resetReadyStatus(false);
2456
2457 return changePosition(player, position);
2458 }
2459
2460
drawReadyButton(UDWORD player)2461 static void drawReadyButton(UDWORD player)
2462 {
2463 int disallow = allPlayersOnSameTeam(-1);
2464
2465 // delete 'ready' botton form
2466 WIDGET *parent = widgGetFromID(psWScreen, MULTIOP_READY_FORM_ID + player);
2467
2468 if (!parent)
2469 {
2470 // add form to hold 'ready' botton
2471 parent = addBlueForm(MULTIOP_PLAYERS, MULTIOP_READY_FORM_ID + player,
2472 7 + MULTIOP_PLAYERWIDTH - MULTIOP_READY_WIDTH,
2473 playerBoxHeight(player),
2474 MULTIOP_READY_WIDTH, MULTIOP_READY_HEIGHT);
2475 }
2476
2477
2478 auto deleteExistingReadyButton = [player]() {
2479 widgDelete(widgGetFromID(psWScreen, MULTIOP_READY_START + player));
2480 widgDelete(widgGetFromID(psWScreen, MULTIOP_READY_START + MAX_PLAYERS + player)); // "Ready?" text label
2481 };
2482 auto deleteExistingDifficultyButton = [player]() {
2483 widgDelete(widgGetFromID(psWScreen, MULTIOP_DIFFICULTY_INIT_START + player));
2484 };
2485
2486 if (!NetPlay.players[player].allocated && NetPlay.players[player].ai >= 0)
2487 {
2488 deleteExistingReadyButton();
2489 int playerDifficulty = static_cast<int8_t>(NetPlay.players[player].difficulty);
2490 int icon = difficultyIcon(playerDifficulty);
2491 char tooltip[128 + 255];
2492 sstrcpy(tooltip, _(difficultyList[playerDifficulty]));
2493 const char *difficultyTip = aidata[NetPlay.players[player].ai].difficultyTips[playerDifficulty];
2494 if (strcmp(difficultyTip, "") != 0)
2495 {
2496 sstrcat(tooltip, "\n");
2497 sstrcat(tooltip, difficultyTip);
2498 }
2499 addMultiBut(psWScreen, MULTIOP_READY_FORM_ID + player, MULTIOP_DIFFICULTY_INIT_START + player, 6, 4, MULTIOP_READY_WIDTH, MULTIOP_READY_HEIGHT,
2500 (NetPlay.isHost && !locked.difficulty) ? _("Click to change difficulty") : tooltip, icon, icon, icon);
2501 return;
2502 }
2503 else if (!NetPlay.players[player].allocated)
2504 {
2505 // closed or open - remove ready / difficulty button
2506 deleteExistingReadyButton();
2507 deleteExistingDifficultyButton();
2508 return;
2509 }
2510
2511 if (disallow != -1)
2512 {
2513 // remove ready / difficulty button
2514 deleteExistingReadyButton();
2515 deleteExistingDifficultyButton();
2516 return;
2517 }
2518
2519 bool isMe = player == selectedPlayer;
2520 int isReady = NETgetDownloadProgress(player) != 100 ? 2 : NetPlay.players[player].ready ? 1 : 0;
2521 char const *const toolTips[2][3] = {{_("Waiting for player"), _("Player is ready"), _("Player is downloading")}, {_("Click when ready"), _("Waiting for other players"), _("Waiting for download")}};
2522 unsigned images[2][3] = {{IMAGE_CHECK_OFF, IMAGE_CHECK_ON, IMAGE_CHECK_DOWNLOAD}, {IMAGE_CHECK_OFF_HI, IMAGE_CHECK_ON_HI, IMAGE_CHECK_DOWNLOAD_HI}};
2523
2524 // draw 'ready' button
2525 auto pReadyBut = addMultiBut(psWScreen, MULTIOP_READY_FORM_ID + player, MULTIOP_READY_START + player, 3, 10, MULTIOP_READY_WIDTH, MULTIOP_READY_HEIGHT,
2526 toolTips[isMe][isReady], images[0][isReady], images[0][isReady], images[isMe][isReady]);
2527 ASSERT_OR_RETURN(, pReadyBut != nullptr, "Failed to create ready button");
2528 pReadyBut->minClickInterval = GAME_TICKS_PER_SEC;
2529 pReadyBut->unlock();
2530
2531 std::shared_ptr<W_LABEL> label;
2532 auto existingLabel = widgFormGetFromID(parent->shared_from_this(), MULTIOP_READY_START + MAX_PLAYERS + player);
2533 if (existingLabel)
2534 {
2535 label = std::dynamic_pointer_cast<W_LABEL>(existingLabel);
2536 }
2537 if (label == nullptr)
2538 {
2539 label = std::make_shared<W_LABEL>();
2540 parent->attach(label);
2541 label->id = MULTIOP_READY_START + MAX_PLAYERS + player;
2542 }
2543 label->setGeometry(0, 0, MULTIOP_READY_WIDTH, 17);
2544 label->setTextAlignment(WLAB_ALIGNBOTTOM);
2545 label->setFont(font_small, WZCOL_TEXT_BRIGHT);
2546 label->setString(_("READY?"));
2547 }
2548
canChooseTeamFor(int i)2549 static bool canChooseTeamFor(int i)
2550 {
2551 return (i == selectedPlayer || NetPlay.isHost);
2552 }
2553
2554 // ////////////////////////////////////////////////////////////////////////////
2555 // box for players.
2556
addPlayerBox(bool players)2557 void WzMultiplayerOptionsTitleUI::addPlayerBox(bool players)
2558 {
2559 // if background isn't there, then return since were not ready to draw the box yet!
2560 if (widgGetFromID(psWScreen, FRONTEND_BACKDROP) == nullptr)
2561 {
2562 return;
2563 }
2564
2565 widgDelete(psWScreen, MULTIOP_PLAYERS); // del player window
2566 widgDelete(psWScreen, FRONTEND_SIDETEXT2); // del text too,
2567
2568 if (aiChooserUp >= 0)
2569 {
2570 return;
2571 }
2572 else if (difficultyChooserUp >= 0)
2573 {
2574 return;
2575 }
2576
2577 // draw player window
2578 WIDGET *widgetParent = widgGetFromID(psWScreen, FRONTEND_BACKDROP);
2579
2580 auto playersForm = std::make_shared<IntFormAnimated>(false);
2581 widgetParent->attach(playersForm);
2582 playersForm->id = MULTIOP_PLAYERS;
2583 playersForm->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
2584 psWidget->setGeometry(MULTIOP_PLAYERSX, MULTIOP_PLAYERSY, MULTIOP_PLAYERSW, MULTIOP_PLAYERSH);
2585 }));
2586
2587 W_LABEL* pPlayersLabel = addSideText(FRONTEND_SIDETEXT2, MULTIOP_PLAYERSX - 3, MULTIOP_PLAYERSY, _("PLAYERS"));
2588 pPlayersLabel->hide(); // hide for now
2589
2590 if (players)
2591 {
2592 int team = -1;
2593 bool allOnSameTeam = true;
2594
2595 for (int i = 0; i < game.maxPlayers; i++)
2596 {
2597 if (NetPlay.players[i].difficulty != AIDifficulty::DISABLED || isHumanPlayer(i))
2598 {
2599 int myTeam = getPlayerTeam(i);
2600 if (team == -1)
2601 {
2602 team = myTeam;
2603 }
2604 else if (myTeam != team)
2605 {
2606 allOnSameTeam = false;
2607 break; // We just need to know if we have enough to start a game
2608 }
2609 }
2610 }
2611
2612 for (int i = 0; i < game.maxPlayers; i++)
2613 {
2614 if (positionChooserUp >= 0 && positionChooserUp != i && (NetPlay.isHost || !isHumanPlayer(i)))
2615 {
2616 W_BUTINIT sButInit;
2617 sButInit.formID = MULTIOP_PLAYERS;
2618 sButInit.id = MULTIOP_PLAYER_START + i;
2619 sButInit.x = 7;
2620 sButInit.y = playerBoxHeight(i);
2621 sButInit.width = MULTIOP_PLAYERWIDTH + 1;
2622 sButInit.height = MULTIOP_PLAYERHEIGHT;
2623 sButInit.pTip = _("Click to change to this slot");
2624 sButInit.pDisplay = displayPosition;
2625 sButInit.UserData = i;
2626 sButInit.pUserData = new DisplayPositionCache();
2627 sButInit.onDelete = [](WIDGET *psWidget) {
2628 assert(psWidget->pUserData != nullptr);
2629 delete static_cast<DisplayPositionCache *>(psWidget->pUserData);
2630 psWidget->pUserData = nullptr;
2631 };
2632 widgAddButton(psWScreen, &sButInit);
2633 continue;
2634 }
2635 else if (i == inlineChooserUp)
2636 {
2637 // skip adding player info box, since inline chooser is up for this player
2638 continue;
2639 }
2640 else if (ingame.localOptionsReceived)
2641 {
2642 //add team chooser
2643 W_BUTINIT sButInit;
2644 sButInit.formID = MULTIOP_PLAYERS;
2645 sButInit.id = MULTIOP_TEAMS_START + i;
2646 sButInit.x = 7;
2647 sButInit.y = playerBoxHeight(i);
2648 sButInit.width = MULTIOP_TEAMSWIDTH;
2649 sButInit.height = MULTIOP_TEAMSHEIGHT;
2650 if (canChooseTeamFor(i) && !locked.teams)
2651 {
2652 sButInit.pTip = _("Choose Team");
2653 }
2654 else if (locked.teams)
2655 {
2656 sButInit.pTip = _("Teams locked");
2657 }
2658 sButInit.pDisplay = displayTeamChooser;
2659 sButInit.UserData = i;
2660
2661 if (alliancesSetTeamsBeforeGame(game.alliance))
2662 {
2663 // only if not disabled and in locked teams mode
2664 widgAddButton(psWScreen, &sButInit);
2665 }
2666 }
2667
2668 // draw player colour
2669 W_BUTINIT sColInit;
2670 sColInit.formID = MULTIOP_PLAYERS;
2671 sColInit.id = MULTIOP_COLOUR_START + i;
2672 sColInit.x = 7 + MULTIOP_TEAMSWIDTH;
2673 sColInit.y = playerBoxHeight(i);
2674 sColInit.width = MULTIOP_COLOUR_WIDTH;
2675 sColInit.height = MULTIOP_PLAYERHEIGHT;
2676 if (selectedPlayer == i || NetPlay.isHost)
2677 {
2678 sColInit.pTip = _("Click to change player colour");
2679 }
2680 sColInit.pDisplay = displayColour;
2681 sColInit.UserData = i;
2682 widgAddButton(psWScreen, &sColInit);
2683
2684 // draw player faction
2685 W_BUTINIT sFacInit;
2686 sFacInit.formID = MULTIOP_PLAYERS;
2687 sFacInit.id = MULTIOP_FACTION_START+i;
2688 sFacInit.x = 7 + MULTIOP_TEAMSWIDTH+MULTIOP_COLOUR_WIDTH;
2689 sFacInit.y = playerBoxHeight(i);
2690 sFacInit.width = MULTIOP_FACTION_WIDTH;
2691 sFacInit.height = MULTIOP_PLAYERHEIGHT;
2692 if (selectedPlayer == i || NetPlay.isHost)
2693 {
2694 sFacInit.pTip = _("Click to change player faction");
2695 }
2696 sFacInit.pDisplay = displayFaction;
2697 sFacInit.UserData = i;
2698 widgAddButton(psWScreen, &sFacInit);
2699
2700 if (ingame.localOptionsReceived)
2701 {
2702 // do not draw "Ready" button if all players are on the same team,
2703 // but always draw the difficulty buttons for AI players
2704 if (!allOnSameTeam || (!NetPlay.players[i].allocated && NetPlay.players[i].ai >= 0))
2705 {
2706 drawReadyButton(i);
2707 }
2708
2709 // draw player info box
2710 W_BUTINIT sButInit;
2711 sButInit.formID = MULTIOP_PLAYERS;
2712 sButInit.id = MULTIOP_PLAYER_START + i;
2713 sButInit.x = 7 + MULTIOP_TEAMSWIDTH + MULTIOP_COLOUR_WIDTH + MULTIOP_FACTION_WIDTH;
2714 sButInit.y = playerBoxHeight(i);
2715 sButInit.width = MULTIOP_PLAYERWIDTH - MULTIOP_TEAMSWIDTH - MULTIOP_READY_WIDTH - MULTIOP_COLOUR_WIDTH - MULTIOP_FACTION_WIDTH;
2716 sButInit.height = MULTIOP_PLAYERHEIGHT;
2717 if ((selectedPlayer == i || NetPlay.isHost) && NetPlay.players[i].allocated && !locked.position)
2718 {
2719 sButInit.pTip = _("Click to change player position");
2720 }
2721 else if (!NetPlay.players[i].allocated)
2722 {
2723 if (NetPlay.isHost && !locked.ai)
2724 {
2725 sButInit.style |= WBUT_SECONDARY;
2726 sButInit.pTip = _("Click to change AI, right click to distribute choice");
2727 }
2728 else if (NetPlay.players[i].ai >= 0)
2729 {
2730 // show AI description. Useful for challenges.
2731 sButInit.pTip = aidata[NetPlay.players[i].ai].tip;
2732 }
2733 }
2734 if (NetPlay.players[i].allocated && !getMultiStats(i).identity.empty())
2735 {
2736 if (!sButInit.pTip.empty())
2737 {
2738 sButInit.pTip += "\n";
2739 }
2740 std::string hash = getMultiStats(i).identity.publicHashString();
2741 sButInit.pTip += _("Player ID: ");
2742 sButInit.pTip += hash.empty()? _("(none)") : hash;
2743 }
2744 sButInit.pDisplay = displayPlayer;
2745 sButInit.UserData = i;
2746 sButInit.pUserData = new DisplayPlayerCache();
2747 sButInit.onDelete = [](WIDGET *psWidget) {
2748 assert(psWidget->pUserData != nullptr);
2749 delete static_cast<DisplayPlayerCache *>(psWidget->pUserData);
2750 psWidget->pUserData = nullptr;
2751 };
2752 widgAddButton(psWScreen, &sButInit);
2753 }
2754 }
2755 }
2756 }
2757
2758 /*
2759 * Notify all players of host launching the game
2760 */
SendFireUp()2761 static void SendFireUp()
2762 {
2763 uint32_t randomSeed = rand(); // Pick a random random seed for the synchronised random number generator.
2764
2765 NETbeginEncode(NETbroadcastQueue(), NET_FIREUP);
2766 NETuint32_t(&randomSeed);
2767 NETend();
2768 printSearchPath();
2769 gameSRand(randomSeed); // Set the seed for the synchronised random number generator. The clients will use the same seed.
2770 }
2771
2772 // host kicks a player from a game.
kickPlayer(uint32_t player_id,const char * reason,LOBBY_ERROR_TYPES type)2773 void kickPlayer(uint32_t player_id, const char *reason, LOBBY_ERROR_TYPES type)
2774 {
2775 // send a kick msg
2776 NETbeginEncode(NETbroadcastQueue(), NET_KICK);
2777 NETuint32_t(&player_id);
2778 NETstring(reason, MAX_KICK_REASON);
2779 NETenum(&type);
2780 NETend();
2781 NETflush();
2782 wzDelay(300);
2783 debug(LOG_NET, "Kicking player %u (%s). Reason: %s", (unsigned int)player_id, getPlayerName(player_id), reason);
2784
2785 ActivityManager::instance().hostKickPlayer(NetPlay.players[player_id], type, reason);
2786
2787 NETplayerKicked(player_id);
2788 }
2789
displayKickReasonPopup(const std::string & reason)2790 void displayKickReasonPopup(const std::string &reason)
2791 {
2792 WZ_Notification notification;
2793 notification.duration = GAME_TICKS_PER_SEC * 10;
2794 notification.contentTitle = _("Kicked from game");
2795 notification.contentText = reason;
2796 notification.tag = KICK_REASON_TAG;
2797
2798 addNotification(notification, WZ_Notification_Trigger(GAME_TICKS_PER_SEC * 1));
2799 }
2800
buildMessage(int32_t sender,const char * text)2801 RoomMessage buildMessage(int32_t sender, const char *text)
2802 {
2803 switch (sender)
2804 {
2805 case SYSTEM_MESSAGE:
2806 return RoomMessage::system(text);
2807 case NOTIFY_MESSAGE:
2808 return RoomMessage::notify(text);
2809 default:
2810 if (sender >= 0 && sender < MAX_PLAYERS_IN_GUI)
2811 {
2812 return RoomMessage::player(sender, text);
2813 }
2814
2815 debug(LOG_ERROR, "Invalid message sender %d.", sender);
2816 return RoomMessage::system(text);
2817 }
2818 }
2819
initialize()2820 void ChatBoxWidget::initialize()
2821 {
2822 id = MULTIOP_CHATBOX;
2823
2824 attach(messages = ScrollableListWidget::make());
2825 messages->setSnapOffset(true);
2826 messages->setStickToBottom(true);
2827 messages->setPadding({3, 4, 3, 4});
2828 messages->setItemSpacing(1);
2829
2830 handleConsoleMessage = std::make_shared<CONSOLE_MESSAGE_LISTENER>([&](ConsoleMessage const &message) -> void
2831 {
2832 addMessage(buildMessage(message.sender, message.text));
2833 });
2834
2835 W_EDBINIT sEdInit;
2836 sEdInit.formID = MULTIOP_CHATBOX;
2837 sEdInit.id = MULTIOP_CHATEDIT;
2838 sEdInit.pUserData = nullptr;
2839 sEdInit.pBoxDisplay = displayChatEdit;
2840 editBox = std::make_shared<W_EDITBOX>(&sEdInit);
2841 attach(editBox);
2842
2843 consoleAddMessageListener(handleConsoleMessage);
2844 }
2845
~ChatBoxWidget()2846 ChatBoxWidget::~ChatBoxWidget()
2847 {
2848 consoleRemoveMessageListener(handleConsoleMessage);
2849 }
2850
2851
2852 class ChatBoxPlayerNameWidget: public WIDGET
2853 {
2854 public:
ChatBoxPlayerNameWidget(std::shared_ptr<PlayerReference> const & player)2855 ChatBoxPlayerNameWidget(std::shared_ptr<PlayerReference> const &player):
2856 WIDGET(WIDG_UNSPECIFIED_TYPE),
2857 player(player),
2858 font(font_regular),
2859 cachedText("", font)
2860 {
2861 updateLayout();
2862 }
2863
display(int xOffset,int yOffset)2864 void display(int xOffset, int yOffset) override
2865 {
2866 auto left = xOffset + x();
2867 auto top = yOffset + y();
2868 auto marginLeft = left + leftMargin;
2869 auto textX = marginLeft + horizontalPadding;
2870 auto textY = top - cachedText->aboveBase();
2871 pie_UniTransBoxFill(marginLeft, top, left + width(), top + height(), pal_GetTeamColour((*player)->colour));
2872 for (int32_t i = -1; i <= 1; i++)
2873 {
2874 for (int32_t j = -1; j <= 1; j++)
2875 {
2876 cachedText->render(textX + i, textY + j, {0, 0, 0, 128});
2877 }
2878 }
2879 cachedText->render(textX, textY, WZCOL_WHITE);
2880 }
2881
run(W_CONTEXT *)2882 void run(W_CONTEXT *) override
2883 {
2884 if (layoutName != (*player)->name)
2885 {
2886 updateLayout();
2887 }
2888 cachedText.tick();
2889 }
2890
2891 private:
2892 std::shared_ptr<PlayerReference> player;
2893 iV_fonts font;
2894 std::string layoutName;
2895 WzCachedText cachedText;
2896 int32_t horizontalPadding = 3;
2897 int32_t leftMargin = 3;
2898
updateLayout()2899 void updateLayout()
2900 {
2901 layoutName = (*player)->name;
2902 cachedText = WzCachedText(layoutName, font);
2903 setGeometry(x(), y(), cachedText->width() + leftMargin + 2 * horizontalPadding, cachedText->lineSize());
2904 }
2905 };
2906
addMessage(RoomMessage const & message)2907 void ChatBoxWidget::addMessage(RoomMessage const &message)
2908 {
2909 ChatBoxWidget::persistentMessageLocalStorage.push_back(message);
2910 displayMessage(message);
2911 }
2912
geometryChanged()2913 void ChatBoxWidget::geometryChanged()
2914 {
2915 auto messagesHeight = height() - MULTIOP_CHATEDITH - 1;
2916 messages->setGeometry(1, 1, width() - 2, messagesHeight);
2917 editBox->setGeometry(MULTIOP_CHATEDITX, messages->y() + messagesHeight, MULTIOP_CHATEDITW, MULTIOP_CHATEDITH);
2918 }
2919
initializeMessages(bool preserveOldChat)2920 void ChatBoxWidget::initializeMessages(bool preserveOldChat)
2921 {
2922 if (preserveOldChat)
2923 {
2924 for (auto message: ChatBoxWidget::persistentMessageLocalStorage)
2925 {
2926 displayMessage(message);
2927 }
2928 } else {
2929 ChatBoxWidget::persistentMessageLocalStorage.clear();
2930 }
2931 }
2932
displayMessage(RoomMessage const & message)2933 void ChatBoxWidget::displayMessage(RoomMessage const &message)
2934 {
2935 W_INIT paragraphInit;
2936 paragraphInit.width = messages->calculateListViewWidth();
2937 auto paragraph = std::make_shared<Paragraph>(¶graphInit);
2938
2939 switch (message.type)
2940 {
2941 case RoomMessageSystem:
2942 paragraph->setFontColour(WZCOL_CONS_TEXT_SYSTEM);
2943 paragraph->addText(message.text);
2944 break;
2945
2946 case RoomMessagePlayer:
2947 paragraph->setFont(font_small);
2948 paragraph->setFontColour({0xc0, 0xc0, 0xc0, 0xff});
2949 paragraph->addText(formatLocalDateTime("%H:%M", message.time));
2950
2951 paragraph->addWidget(std::make_shared<ChatBoxPlayerNameWidget>(message.sender), iV_GetTextAboveBase(font_regular));
2952
2953 paragraph->setFont(font_regular);
2954 paragraph->setShadeColour({0, 0, 0, 0});
2955 paragraph->setFontColour(WZCOL_WHITE);
2956 paragraph->addText(astringf(" %s", message.text.c_str()));
2957
2958 break;
2959
2960 case RoomMessageNotify:
2961 default:
2962 paragraph->setFontColour(WZCOL_YELLOW);
2963 paragraph->addText(message.text);
2964 break;
2965 }
2966
2967 messages->addItem(paragraph);
2968 }
2969
addChatBox(bool preserveOldChat)2970 static void addChatBox(bool preserveOldChat)
2971 {
2972 if (widgGetFromID(psWScreen, FRONTEND_TOPFORM))
2973 {
2974 widgDelete(psWScreen, FRONTEND_TOPFORM);
2975 }
2976
2977 if (widgGetFromID(psWScreen, MULTIOP_CHATBOX))
2978 {
2979 return;
2980 }
2981
2982 WIDGET *parent = widgGetFromID(psWScreen, FRONTEND_BACKDROP);
2983 auto chatBox = ChatBoxWidget::make();
2984 parent->attach(chatBox);
2985 chatBox->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
2986 if (auto parent = psWidget->parent())
2987 {
2988 psWidget->setGeometry(MULTIOP_CHATBOXX, MULTIOP_CHATBOXY, MULTIOP_CHATBOXW, parent->height() - MULTIOP_CHATBOXY);
2989 }
2990 }));
2991 chatBox->initializeMessages(preserveOldChat);
2992
2993 addSideText(FRONTEND_SIDETEXT4, MULTIOP_CHATBOXX - 3, MULTIOP_CHATBOXY, _("CHAT"));
2994
2995 if (!getModList().empty())
2996 {
2997 WzString modListMessage = _("Mod: ");
2998 modListMessage += getModList().c_str();
2999 displayRoomSystemMessage(modListMessage.toUtf8().c_str());
3000 }
3001 }
3002
3003 // ////////////////////////////////////////////////////////////////////////////
disableMultiButs()3004 static void disableMultiButs()
3005 {
3006 if (!NetPlay.isHost)
3007 {
3008 // edit box icons.
3009 widgSetButtonState(psWScreen, MULTIOP_GNAME_ICON, WBUT_DISABLE);
3010 widgSetButtonState(psWScreen, MULTIOP_MAP_ICON, WBUT_DISABLE);
3011
3012 // edit boxes
3013 widgSetButtonState(psWScreen, MULTIOP_GNAME, WEDBS_DISABLE);
3014
3015 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_GAMETYPE))->disable(); // Scavengers.
3016 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_BASETYPE))->disable(); // camapign subtype.
3017 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_POWER))->disable(); // pow levels
3018 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_ALLIANCES))->disable();
3019 }
3020 }
3021
3022 ////////////////////////////////////////////////////////////////////////////
stopJoining(std::shared_ptr<WzTitleUI> parent)3023 static void stopJoining(std::shared_ptr<WzTitleUI> parent)
3024 {
3025 bInActualHostedLobby = false;
3026
3027 reloadMPConfig(); // reload own settings
3028 cancelOrDismissNotificationsWithTag(VOTE_TAG);
3029
3030 debug(LOG_NET, "player %u (Host is %s) stopping.", selectedPlayer, NetPlay.isHost ? "true" : "false");
3031
3032 pie_LoadBackDrop(SCREEN_RANDOMBDROP);
3033
3034 if (NetPlay.isHost) // cancel a hosted game.
3035 {
3036 // annouce we are leaving...
3037 debug(LOG_NET, "Host is quitting game...");
3038 NETbeginEncode(NETbroadcastQueue(), NET_HOST_DROPPED);
3039 NETend();
3040 sendLeavingMsg(); // say goodbye
3041 NETclose(); // quit running game.
3042 ActivityManager::instance().hostLobbyQuit();
3043 changeTitleUI(wzTitleUICurrent); // refresh options screen.
3044 ingame.localJoiningInProgress = false;
3045 return;
3046 }
3047 else if (ingame.localJoiningInProgress) // cancel a joined game.
3048 {
3049 debug(LOG_NET, "Canceling game...");
3050 sendLeavingMsg(); // say goodbye
3051 NETclose(); // quit running game.
3052
3053 // if we were in a midle of transferring a file, then close the file handle
3054 for (auto const &file : NetPlay.wzFiles)
3055 {
3056 debug(LOG_NET, "closing aborted file"); // no need to delete it, we do size check on (map) file
3057 PHYSFS_close(file.handle);
3058 }
3059 NetPlay.wzFiles.clear();
3060 ingame.localJoiningInProgress = false; // reset local flags
3061 ingame.localOptionsReceived = false;
3062
3063 // joining and host was transferred.
3064 if (ingame.side == InGameSide::MULTIPLAYER_CLIENT && NetPlay.isHost)
3065 {
3066 NetPlay.isHost = false;
3067 }
3068
3069 ActivityManager::instance().joinedLobbyQuit();
3070 changeTitleMode(MULTI);
3071
3072 selectedPlayer = 0;
3073 realSelectedPlayer = 0;
3074 return;
3075 }
3076 debug(LOG_NET, "We have stopped joining.");
3077 ActivityManager::instance().joinedLobbyQuit();
3078 changeTitleUI(parent);
3079 selectedPlayer = 0;
3080 realSelectedPlayer = 0;
3081 for (auto& player : NetPlay.players)
3082 {
3083 player.resetAll();
3084 }
3085 }
3086
resetPlayerPositions()3087 static void resetPlayerPositions()
3088 {
3089 // Reset players' positions or bad things could happen to scavenger slot
3090
3091 for (unsigned int i = 0; i < MAX_PLAYERS; ++i)
3092 {
3093 NetPlay.players[i].position = i;
3094 NetPlay.players[i].team = i;
3095 NETBroadcastPlayerInfo(i);
3096 }
3097 }
3098
repositionHumanSlots()3099 static unsigned int repositionHumanSlots()
3100 {
3101 unsigned int pos = 0;
3102
3103 // First, put human players at the top
3104 for (unsigned int i = 0; i < MAX_PLAYERS; ++i)
3105 {
3106 if (isHumanPlayer(i))
3107 {
3108 // Skip the scavenger slot
3109 if (game.mapHasScavengers && pos == scavengerSlot())
3110 {
3111 ++pos;
3112 }
3113 NetPlay.players[i].position = pos;
3114 NETBroadcastPlayerInfo(i);
3115 ++pos;
3116 }
3117 }
3118
3119 return pos;
3120 }
3121
updateMapWidgets(LEVEL_DATASET * mapData)3122 static void updateMapWidgets(LEVEL_DATASET *mapData)
3123 {
3124 sstrcpy(game.map, mapData->pName);
3125 game.hash = levGetFileHash(mapData);
3126 game.maxPlayers = mapData->players;
3127 game.isMapMod = CheckForMod(mapData->realFileName);
3128 game.isRandom = CheckForRandom(mapData->realFileName, mapData->pName);
3129 if (game.isMapMod)
3130 {
3131 widgReveal(psWScreen, MULTIOP_MAP_MOD);
3132 }
3133 else
3134 {
3135 widgHide(psWScreen, MULTIOP_MAP_MOD);
3136 }
3137 (game.isRandom? widgReveal : widgHide)(psWScreen, MULTIOP_MAP_RANDOM);
3138
3139 WzString name = formatGameName(game.map);
3140 widgSetString(psWScreen, MULTIOP_MAP + 1, name.toUtf8().c_str()); //What a horrible, horrible way to do this! FIX ME! (See addBlueForm)
3141 }
3142
loadMapChallengeSettings(WzConfig & ini)3143 static void loadMapChallengeSettings(WzConfig& ini)
3144 {
3145 ini.beginGroup("locked"); // GUI lockdown
3146 {
3147 locked.power = ini.value("power", challengeActive).toBool();
3148 locked.alliances = ini.value("alliances", challengeActive).toBool();
3149 locked.teams = ini.value("teams", challengeActive).toBool();
3150 locked.difficulty = ini.value("difficulty", challengeActive).toBool();
3151 locked.ai = ini.value("ai", challengeActive).toBool();
3152 locked.scavengers = ini.value("scavengers", challengeActive).toBool();
3153 locked.position = ini.value("position", challengeActive).toBool();
3154 locked.bases = ini.value("bases", challengeActive).toBool();
3155 }
3156 ini.endGroup();
3157
3158 const bool bIsAutoHostOrAutoGame = getHostLaunch() == HostLaunch::Skirmish || getHostLaunch() == HostLaunch::Autohost;
3159 if (challengeActive || bIsAutoHostOrAutoGame)
3160 {
3161 ini.beginGroup("challenge");
3162 {
3163 sstrcpy(game.map, ini.value("map", game.map).toWzString().toUtf8().c_str());
3164 game.hash = levGetMapNameHash(game.map);
3165
3166 LEVEL_DATASET* mapData = levFindDataSet(game.map, &game.hash);
3167 if (!mapData)
3168 {
3169 code_part log_level = (bIsAutoHostOrAutoGame) ? LOG_ERROR : LOG_FATAL;
3170 debug(log_level, "Map %s not found!", game.map);
3171 if (bIsAutoHostOrAutoGame)
3172 {
3173 exit(1);
3174 }
3175 }
3176 game.maxPlayers = mapData->players;
3177
3178 uint8_t configuredMaxPlayers = ini.value("maxPlayers", game.maxPlayers).toUInt();
3179 if (getHostLaunch() == HostLaunch::Autohost)
3180 {
3181 // always use the autohost config - if it specifies an invalid number of players, this is a bug in the config
3182 game.maxPlayers = std::max((uint8_t)1u, configuredMaxPlayers);
3183 }
3184 else
3185 {
3186 game.maxPlayers = std::min(std::max((uint8_t)1u, configuredMaxPlayers), game.maxPlayers);
3187 }
3188 game.scavengers = ini.value("scavengers", game.scavengers).toBool();
3189 game.alliance = ini.value("alliances", ALLIANCES_TEAMS).toInt();
3190 game.power = ini.value("powerLevel", game.power).toInt();
3191 game.base = ini.value("bases", game.base + 1).toInt() - 1; // count from 1 like the humans do
3192 sstrcpy(game.name, ini.value("name").toWzString().toUtf8().c_str());
3193 game.techLevel = ini.value("techLevel", game.techLevel).toInt();
3194
3195 // DEPRECATED: This seems to have been odd workaround for not having the locked group handled.
3196 // Keeping it around in case mods use it.
3197 locked.position = !ini.value("allowPositionChange", !locked.position).toBool();
3198 }
3199 ini.endGroup();
3200 }
3201 else
3202 {
3203 ini.beginGroup("defaults");
3204 {
3205 game.scavengers = ini.value("scavengers", game.scavengers).toBool();
3206 game.base = ini.value("bases", game.base).toInt();
3207 game.alliance = ini.value("alliances", game.alliance).toInt();
3208 game.power = ini.value("powerLevel", game.power).toInt();
3209 }
3210 ini.endGroup();
3211 }
3212 }
3213
3214
resolveAIForPlayer(int player,WzString & aiValue)3215 static void resolveAIForPlayer(int player, WzString& aiValue)
3216 {
3217 if (aiValue.compare("null") == 0)
3218 {
3219 return;
3220 }
3221
3222 // strip given path down to filename
3223 WzString filename = WzString::fromUtf8(WzPathInfo::fromPlatformIndependentPath(aiValue.toUtf8()).fileName().c_str());
3224
3225 // look up AI value in vector of known skirmish AIs
3226 for (unsigned ai = 0; ai < aidata.size(); ++ai)
3227 {
3228 if (filename == aidata[ai].js)
3229 {
3230 NetPlay.players[player].ai = ai;
3231 return;
3232 }
3233 }
3234
3235 // did not find from known skirmish AIs, assume custom AI
3236 NetPlay.players[player].ai = AI_CUSTOM;
3237 }
3238
loadMapPlayerSettings(WzConfig & ini)3239 static void loadMapPlayerSettings(WzConfig& ini)
3240 {
3241 for (int i = 0; i < MAX_PLAYERS; i++)
3242 {
3243 ini.beginGroup("player_" + WzString::number(i));
3244 if (ini.contains("team"))
3245 {
3246 NetPlay.players[i].team = ini.value("team").toInt();
3247 }
3248 else if (challengeActive) // team is a required key for challenges
3249 {
3250 NetPlay.players[i].ai = AI_CLOSED;
3251 }
3252
3253 /* Load pre-configured AIs */
3254 if (ini.contains("ai"))
3255 {
3256 WzString val = ini.value("ai").toWzString();
3257 resolveAIForPlayer(i, val);
3258 }
3259
3260 /* Try finding a name field, if not found use AI names for AI players if in SP skirmish */
3261 if (ini.contains("name"))
3262 {
3263 sstrcpy(NetPlay.players[i].name, ini.value("name").toWzString().toUtf8().c_str());
3264 }
3265 else if (!NetPlay.bComms && i != selectedPlayer)
3266 {
3267 sstrcpy(NetPlay.players[i].name, getAIName(i));
3268 }
3269
3270 NetPlay.players[i].position = MAX_PLAYERS; // Invalid value, fix later.
3271 if (ini.contains("position"))
3272 {
3273 NetPlay.players[i].position = std::min(std::max(ini.value("position").toInt(), 0), MAX_PLAYERS);
3274 }
3275 if (ini.contains("difficulty"))
3276 {
3277 /* If difficulty is set, but we have no AI, use default */
3278 if (!ini.contains("ai"))
3279 {
3280 NetPlay.players[i].ai = 0;
3281 }
3282
3283 WzString value = ini.value("difficulty", "Medium").toWzString();
3284 for (unsigned j = 0; j < ARRAY_SIZE(difficultyList); ++j)
3285 {
3286 if (strcasecmp(difficultyList[j], value.toUtf8().c_str()) == 0)
3287 {
3288 NetPlay.players[i].difficulty = difficultyValue[j];
3289 }
3290 }
3291 }
3292 if (ini.contains("faction"))
3293 {
3294 WzString value = ini.value("faction", "Normal").toWzString();
3295 for (uint8_t f_id = 0; f_id < NUM_FACTIONS; ++f_id)
3296 {
3297 const FACTION* faction = getFactionByID(static_cast<FactionID>(f_id));
3298 if (faction->name == value)
3299 {
3300 NetPlay.players[i].faction = static_cast<FactionID>(f_id);
3301 }
3302 }
3303 }
3304 ini.endGroup();
3305 }
3306
3307 // Fix duplicate or unset player positions.
3308 PlayerMask havePosition = 0;
3309 for (int i = 0; i < MAX_PLAYERS; ++i)
3310 {
3311 if (NetPlay.players[i].position < MAX_PLAYERS)
3312 {
3313 PlayerMask old = havePosition;
3314 havePosition |= PlayerMask(1) << NetPlay.players[i].position;
3315 if (havePosition == old)
3316 {
3317 ASSERT(false, "Duplicate position %d", NetPlay.players[i].position);
3318 NetPlay.players[i].position = MAX_PLAYERS;
3319 }
3320 }
3321 }
3322 int pos = 0;
3323 for (int i = 0; i < MAX_PLAYERS; ++i)
3324 {
3325 if (NetPlay.players[i].position >= MAX_PLAYERS)
3326 {
3327 while ((havePosition & (PlayerMask(1) << pos)) != 0)
3328 {
3329 ++pos;
3330 }
3331 NetPlay.players[i].position = pos;
3332 ++pos;
3333 }
3334 }
3335 }
3336
playersPerTeam()3337 static int playersPerTeam()
3338 {
3339 for (unsigned numTeams = game.maxPlayers - 1; numTeams > 1; --numTeams)
3340 {
3341 if (game.maxPlayers % numTeams == 0)
3342 {
3343 return numTeams;
3344 }
3345 }
3346 return 1;
3347 }
3348
swapPlayerColours(uint32_t player1,uint32_t player2)3349 static void swapPlayerColours(uint32_t player1, uint32_t player2)
3350 {
3351 auto player1Colour = getPlayerColour(player1);
3352 setPlayerColour(player1, getPlayerColour(player2));
3353 setPlayerColour(player2, player1Colour);
3354 }
3355
3356 /**
3357 * Resets all player difficulties, positions, teams and colors etc.
3358 */
resetPlayerConfiguration(const bool bShouldResetLocal=false)3359 static void resetPlayerConfiguration(const bool bShouldResetLocal = false)
3360 {
3361 auto selectedPlayerPosition = bShouldResetLocal? 0: NetPlay.players[selectedPlayer].position;
3362 for (unsigned playerIndex = 0; playerIndex < MAX_PLAYERS_IN_GUI; playerIndex++)
3363 {
3364 setPlayerColour(playerIndex, playerIndex);
3365 swapPlayerColours(playerIndex, rand() % (playerIndex + 1));
3366 NetPlay.players[playerIndex].position = playerIndex;
3367
3368 if (!bShouldResetLocal && playerIndex == selectedPlayer)
3369 {
3370 continue;
3371 }
3372
3373 NetPlay.players[playerIndex].team = playerIndex / playersPerTeam();
3374
3375 if (NetPlay.bComms)
3376 {
3377 NetPlay.players[playerIndex].difficulty = AIDifficulty::DISABLED;
3378 NetPlay.players[playerIndex].ai = AI_OPEN;
3379 NetPlay.players[playerIndex].name[0] = '\0';
3380 }
3381 else
3382 {
3383 NetPlay.players[playerIndex].difficulty = AIDifficulty::DEFAULT;
3384 NetPlay.players[playerIndex].ai = 0;
3385
3386 /* ensure all players have a name in One Player Skirmish games */
3387 sstrcpy(NetPlay.players[playerIndex].name, getAIName(playerIndex));
3388 }
3389 }
3390
3391 if (!bShouldResetLocal && selectedPlayerPosition < game.maxPlayers && selectedPlayer != selectedPlayerPosition) {
3392 std::swap(NetPlay.players[selectedPlayer].position, NetPlay.players[selectedPlayerPosition].position);
3393 }
3394
3395 sstrcpy(NetPlay.players[selectedPlayer].name, sPlayer);
3396
3397 for (unsigned playerIndex = 0; playerIndex < MAX_PLAYERS; playerIndex++)
3398 {
3399 if (getPlayerColour(playerIndex) == war_getMPcolour())
3400 {
3401 swapPlayerColours(selectedPlayer, playerIndex);
3402 break;
3403 }
3404 }
3405 }
3406
3407 /**
3408 * Loads challenge and player configurations from level/autohost/test .json-files.
3409 */
loadMapChallengeAndPlayerSettings(bool forceLoadPlayers=false)3410 static void loadMapChallengeAndPlayerSettings(bool forceLoadPlayers = false)
3411 {
3412 char aFileName[256];
3413 LEVEL_DATASET* psLevel = levFindDataSet(game.map, &game.hash);
3414
3415 ASSERT_OR_RETURN(, psLevel, "No level found for %s", game.map);
3416 sstrcpy(aFileName, psLevel->apDataFiles[psLevel->game]);
3417 aFileName[std::max<size_t>(strlen(aFileName), 4) - 4] = '\0';
3418 sstrcat(aFileName, ".json");
3419
3420 WzString ininame = challengeActive ? sRequestResult : aFileName;
3421 bool warnIfMissing = false;
3422 if (getHostLaunch() == HostLaunch::Skirmish)
3423 {
3424 ininame = "tests/" + WzString::fromUtf8(wz_skirmish_test());
3425 warnIfMissing = true;
3426 }
3427 if (getHostLaunch() == HostLaunch::Autohost)
3428 {
3429 ininame = "autohost/" + WzString::fromUtf8(wz_skirmish_test());
3430 warnIfMissing = true;
3431 }
3432
3433 const bool bIsOnline = NetPlay.bComms && NetPlay.isHost;
3434 if (!PHYSFS_exists(ininame.toUtf8().c_str()))
3435 {
3436 if (warnIfMissing)
3437 {
3438 debug(LOG_ERROR, "Missing specified file: %s", ininame.toUtf8().c_str());
3439 }
3440
3441 /* Just reset the players if config is not found and host is not started yet */
3442 if (!bIsOnline) {
3443 resetPlayerConfiguration();
3444 }
3445
3446 return;
3447 }
3448 WzConfig ini(ininame, WzConfig::ReadOnly);
3449
3450 loadMapChallengeSettings(ini);
3451
3452 /* Do not load player settings if we are already hosting an online match */
3453 if (!bIsOnline || forceLoadPlayers)
3454 {
3455 loadMapPlayerSettings(ini);
3456 }
3457 }
3458
randomizeLimit(const char * name)3459 static void randomizeLimit(const char *name)
3460 {
3461 int stat = getStructStatFromName(name);
3462 if (rand() % 2 == 0)
3463 {
3464 asStructureStats[stat].upgrade[0].limit = asStructureStats[stat].base.limit;
3465 }
3466 else
3467 {
3468 asStructureStats[stat].upgrade[0].limit = 0;
3469 }
3470 }
3471
3472 /* Generate random options */
randomizeOptions()3473 static void randomizeOptions()
3474 {
3475 RUN_ONLY_ON_SIDE(InGameSide::HOST_OR_SINGLEPLAYER)
3476
3477 if (NetPlay.bComms && NetPlay.isHost && !canChangeMapOrRandomize())
3478 {
3479 return;
3480 }
3481
3482 resetPlayerPositions();
3483
3484 // Don't randomize the map once hosting for true multiplayer has started
3485 if (!NetPlay.isHost || !(bMultiPlayer && NetPlay.bComms != 0))
3486 {
3487 // Pick a map for a number of players and tech level
3488 game.techLevel = (rand() % 4) + 1;
3489 LEVEL_LIST levels;
3490 do
3491 {
3492 // don't kick out already joined players because of randomize
3493 int players = NET_numHumanPlayers();
3494 int minimumPlayers = std::max(players, 2);
3495 current_numplayers = minimumPlayers;
3496 if (minimumPlayers < MAX_PLAYERS_IN_GUI)
3497 {
3498 current_numplayers += (rand() % (MAX_PLAYERS_IN_GUI - minimumPlayers));
3499 }
3500 levels = enumerateMultiMaps(game.techLevel, current_numplayers);
3501 }
3502 while (levels.empty()); // restart when there are no maps for a random number of players
3503
3504 int pickedLevel = rand() % levels.size();
3505 LEVEL_DATASET *mapData = levels[pickedLevel];
3506
3507 updateMapWidgets(mapData);
3508 loadMapPreview(false);
3509 loadMapChallengeAndPlayerSettings();
3510 }
3511
3512 // Reset and randomize player positions, also to guard
3513 // against case where in the previous map some players
3514 // took slots, which aren't available anymore
3515
3516 unsigned pos = repositionHumanSlots();
3517
3518 // Fill with AIs if places remain
3519 for (int i = 0; i < current_numplayers; i++)
3520 {
3521 if (!isHumanPlayer(i) && !(game.mapHasScavengers && NetPlay.players[i].position == scavengerSlot()))
3522 {
3523 // Skip the scavenger slot
3524 if (game.mapHasScavengers && pos == scavengerSlot())
3525 {
3526 pos++;
3527 }
3528 NetPlay.players[i].position = pos;
3529 NETBroadcastPlayerInfo(i);
3530 pos++;
3531 }
3532 }
3533
3534 // Randomize positions
3535 for (int i = 0; i < current_numplayers; i++)
3536 {
3537 SendPositionRequest(i, NetPlay.players[rand() % current_numplayers].position);
3538 }
3539
3540 // Structure limits are simply 0 or max, only for NO_TANK, NO_CYBORG, NO_VTOL, NO_UPLINK, NO_LASSAT.
3541 if (!bLimiterLoaded || !asStructureStats)
3542 {
3543 initLoadingScreen(true);
3544 if (resLoad("wrf/limiter_data.wrf", 503))
3545 {
3546 bLimiterLoaded = true;
3547 }
3548 closeLoadingScreen();
3549 }
3550 resetLimits();
3551 for (int i = 0; i < ARRAY_SIZE(limitIcons) - 1; ++i) // skip last item, MPFLAGS_FORCELIMITS
3552 {
3553 randomizeLimit(limitIcons[i].stat);
3554 }
3555 if (rand() % 2 == 0)
3556 {
3557 ingame.flags |= MPFLAGS_FORCELIMITS;
3558 }
3559 else
3560 {
3561 ingame.flags &= ~MPFLAGS_FORCELIMITS;
3562 }
3563 createLimitSet();
3564 applyLimitSet();
3565 updateStructureDisabledFlags();
3566 updateLimitIcons();
3567
3568 // Game options
3569 if (!locked.scavengers && game.mapHasScavengers)
3570 {
3571 game.scavengers = rand() % 2;
3572 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_GAMETYPE))->choose(game.scavengers);
3573 }
3574
3575 if (!locked.alliances)
3576 {
3577 game.alliance = rand() % 4;
3578 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_ALLIANCES))->choose(game.alliance);
3579 }
3580 if (!locked.power)
3581 {
3582 game.power = rand() % 3;
3583 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_POWER))->choose(game.power);
3584 }
3585 if (!locked.bases)
3586 {
3587 game.base = rand() % 3;
3588 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_BASETYPE))->choose(game.base);
3589 }
3590 if (NetPlay.isHost)
3591 {
3592 game.techLevel = rand() % 4;
3593 ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_TECHLEVEL))->choose(game.techLevel);
3594
3595 resetReadyStatus(true);
3596 }
3597 }
3598
startHost()3599 bool WzMultiplayerOptionsTitleUI::startHost()
3600 {
3601 resetReadyStatus(false);
3602 removeWildcards((char*)sPlayer);
3603
3604 const bool bIsAutoHostOrAutoGame = getHostLaunch() == HostLaunch::Skirmish || getHostLaunch() == HostLaunch::Autohost;
3605 if (!hostCampaign((char*)game.name, (char*)sPlayer, bIsAutoHostOrAutoGame))
3606 {
3607 displayRoomSystemMessage(_("Sorry! Failed to host the game."));
3608 return false;
3609 }
3610
3611 bInActualHostedLobby = true;
3612
3613 widgDelete(psWScreen, MULTIOP_REFRESH);
3614 widgDelete(psWScreen, MULTIOP_HOST);
3615 widgDelete(psWScreen, MULTIOP_FILTER_TOGGLE);
3616
3617 ingame.localOptionsReceived = true;
3618
3619 addGameOptions(); // update game options box.
3620 addChatBox();
3621
3622 disableMultiButs();
3623 addPlayerBox(true);
3624
3625 return true;
3626 }
3627
3628 /*
3629 * Process click events on the multiplayer/skirmish options screen
3630 * 'id' is id of the button that was pressed
3631 */
processMultiopWidgets(UDWORD id)3632 void WzMultiplayerOptionsTitleUI::processMultiopWidgets(UDWORD id)
3633 {
3634 PLAYERSTATS playerStats;
3635
3636 // host, who is setting up the game
3637 if (ingame.side == InGameSide::HOST_OR_SINGLEPLAYER)
3638 {
3639 switch (id) // Options buttons
3640 {
3641 case MULTIOP_GNAME: // we get this when nec.
3642 sstrcpy(game.name, widgGetString(psWScreen, MULTIOP_GNAME));
3643 removeWildcards(game.name);
3644 widgSetString(psWScreen, MULTIOP_GNAME, game.name);
3645
3646 if (NetPlay.isHost && NetPlay.bComms)
3647 {
3648 NETsetLobbyOptField(game.name, NET_LOBBY_OPT_FIELD::GNAME);
3649 sendOptions();
3650 NETregisterServer(WZ_SERVER_UPDATE);
3651
3652 displayRoomSystemMessage(_("Game Name Updated."));
3653 }
3654 break;
3655
3656 case MULTIOP_GNAME_ICON:
3657 break;
3658
3659 case MULTIOP_MAP:
3660 widgDelete(psWScreen, MULTIOP_PLAYERS);
3661 widgDelete(psWScreen, FRONTEND_SIDETEXT2); // del text too,
3662
3663 debug(LOG_WZ, "processMultiopWidgets[MULTIOP_MAP_ICON]: %s.wrf", MultiCustomMapsPath);
3664 addMultiRequest(MultiCustomMapsPath, ".wrf", MULTIOP_MAP, 0, widgGetString(psWScreen, MULTIOP_MAP));
3665
3666 widgSetString(psWScreen, MULTIOP_MAP + 1 , game.map); //What a horrible hack! FIX ME! (See addBlueForm())
3667 widgReveal(psWScreen, MULTIOP_MAP_MOD);
3668 widgReveal(psWScreen, MULTIOP_MAP_RANDOM);
3669 break;
3670
3671 case MULTIOP_MAP_ICON:
3672 widgDelete(psWScreen, MULTIOP_PLAYERS);
3673 widgDelete(psWScreen, FRONTEND_SIDETEXT2); // del text too,
3674
3675 debug(LOG_WZ, "processMultiopWidgets[MULTIOP_MAP_ICON]: %s.wrf", MultiCustomMapsPath);
3676 addMultiRequest(MultiCustomMapsPath, ".wrf", MULTIOP_MAP, current_numplayers);
3677
3678 if (NetPlay.isHost && NetPlay.bComms)
3679 {
3680 sendOptions();
3681
3682 NETsetLobbyOptField(game.map, NET_LOBBY_OPT_FIELD::MAPNAME);
3683 NETregisterServer(WZ_SERVER_UPDATE);
3684 }
3685 break;
3686
3687 case MULTIOP_MAP_PREVIEW:
3688 loadMapPreview(true);
3689 break;
3690 }
3691 }
3692
3693 // host who is setting up or has hosted
3694 if (ingame.side == InGameSide::HOST_OR_SINGLEPLAYER)
3695 {
3696 switch (id)
3697 {
3698 case MULTIOP_GAMETYPE:
3699 game.scavengers = ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_GAMETYPE))->currentValue();
3700 resetReadyStatus(false);
3701 if (NetPlay.isHost)
3702 {
3703 sendOptions();
3704 }
3705 break;
3706
3707 case MULTIOP_BASETYPE:
3708 game.base = ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_BASETYPE))->currentValue();
3709 addGameOptions();
3710
3711 resetReadyStatus(false);
3712
3713 if (NetPlay.isHost)
3714 {
3715 sendOptions();
3716 }
3717 break;
3718
3719 case MULTIOP_ALLIANCES:
3720 game.alliance = ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_ALLIANCES))->currentValue();
3721
3722 resetReadyStatus(false);
3723 netPlayersUpdated = true;
3724
3725 if (NetPlay.isHost)
3726 {
3727 sendOptions();
3728 }
3729 break;
3730
3731 case MULTIOP_POWER: // set power level
3732 game.power = ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_POWER))->currentValue();
3733
3734 resetReadyStatus(false);
3735
3736 if (NetPlay.isHost)
3737 {
3738 sendOptions();
3739 }
3740 break;
3741
3742 case MULTIOP_TECHLEVEL:
3743 game.techLevel = ((MultichoiceWidget *)widgGetFromID(psWScreen, MULTIOP_TECHLEVEL))->currentValue();
3744 addGameOptions(); //refresh to see the proper tech level in the map name
3745
3746 resetReadyStatus(false);
3747
3748 if (NetPlay.isHost)
3749 {
3750 sendOptions();
3751 }
3752 break;
3753
3754 case MULTIOP_PASSWORD_EDIT:
3755 {
3756 unsigned result = widgGetButtonState(psWScreen, MULTIOP_PASSWORD_BUT);
3757 if (result != 0)
3758 {
3759 break;
3760 }
3761 }
3762 // fallthrough
3763 case MULTIOP_PASSWORD_BUT:
3764 {
3765 char buf[255];
3766
3767 UDWORD currentButState = widgGetButtonState(psWScreen, MULTIOP_PASSWORD_BUT);
3768 bool willSet = currentButState == 0;
3769 char game_password[password_string_size] = {0};
3770 sstrcpy(game_password, widgGetString(psWScreen, MULTIOP_PASSWORD_EDIT));
3771 const size_t passLength = strlen(game_password) > 0;
3772 willSet &= (passLength > 0);
3773 debug(LOG_NET, "Password button hit, %d", (int)willSet);
3774 widgSetButtonState(psWScreen, MULTIOP_PASSWORD_BUT, willSet ? WBUT_CLICKLOCK : 0);
3775 widgSetButtonState(psWScreen, MULTIOP_PASSWORD_EDIT, willSet ? WEDBS_DISABLE : 0);
3776 if (willSet)
3777 {
3778 NETsetGamePassword(game_password);
3779 // say password is now required to join games?
3780 ssprintf(buf, _("*** password [%s] is now required! ***"), NetPlay.gamePassword);
3781 displayRoomNotifyMessage(buf);
3782 }
3783 else
3784 {
3785 NETresetGamePassword();
3786 ssprintf(buf, "%s", _("*** password is NOT required! ***"));
3787 displayRoomNotifyMessage(buf);
3788 }
3789 }
3790 break;
3791 }
3792 }
3793
3794 // these work all the time.
3795 switch (id)
3796 {
3797 case MULTIOP_MAP_MOD:
3798 char buf[256];
3799 ssprintf(buf, "%s", _("This is a map-mod, it can change your playing experience!"));
3800 displayRoomSystemMessage(buf);
3801 break;
3802
3803 case MULTIOP_MAP_RANDOM:
3804 ssprintf(buf, "%s", _("This is a random map, it can vary your playing experience!"));
3805 displayRoomSystemMessage(buf);
3806 break;
3807
3808 case MULTIOP_STRUCTLIMITS:
3809 changeTitleUI(std::make_shared<WzMultiLimitTitleUI>(std::dynamic_pointer_cast<WzMultiplayerOptionsTitleUI>(wzTitleUICurrent)));
3810 break;
3811
3812 case MULTIOP_PNAME:
3813 sstrcpy(sPlayer, widgGetString(psWScreen, MULTIOP_PNAME));
3814
3815 // chop to 15 chars..
3816 while (strlen(sPlayer) > 15) // clip name.
3817 {
3818 sPlayer[strlen(sPlayer) - 1] = '\0';
3819 }
3820 removeWildcards(sPlayer);
3821 // update string.
3822 widgSetString(psWScreen, MULTIOP_PNAME, sPlayer);
3823 printConsoleNameChange(NetPlay.players[selectedPlayer].name, sPlayer);
3824
3825 NETchangePlayerName(selectedPlayer, (char *)sPlayer); // update if joined.
3826 loadMultiStats((char *)sPlayer, &playerStats);
3827 setMultiStats(selectedPlayer, playerStats, false);
3828 setMultiStats(selectedPlayer, playerStats, true);
3829 lookupRatingAsync(selectedPlayer);
3830 netPlayersUpdated = true;
3831
3832 if (NetPlay.isHost && NetPlay.bComms)
3833 {
3834 sendOptions();
3835 NETsetLobbyOptField(NetPlay.players[selectedPlayer].name, NET_LOBBY_OPT_FIELD::HOSTNAME);
3836 NETregisterServer(WZ_SERVER_UPDATE);
3837 }
3838
3839 break;
3840
3841 case MULTIOP_PNAME_ICON:
3842 widgDelete(psWScreen, MULTIOP_PLAYERS);
3843 widgDelete(psWScreen, FRONTEND_SIDETEXT2); // del text too,
3844
3845 addMultiRequest(MultiPlayersPath, ".sta2", MULTIOP_PNAME, 0);
3846 break;
3847
3848 case MULTIOP_HOST:
3849 debug(LOG_NET, "MULTIOP_HOST enabled");
3850 sstrcpy(game.name, widgGetString(psWScreen, MULTIOP_GNAME));
3851 sstrcpy(sPlayer, widgGetString(psWScreen, MULTIOP_PNAME));
3852
3853 resetVoteData();
3854 resetDataHash();
3855
3856 startHost();
3857 break;
3858 case MULTIOP_RANDOM:
3859 randomizeOptions();
3860 break;
3861
3862 case MULTIOP_CHATEDIT:
3863
3864 // don't send empty lines to other players in the lobby
3865 if (!strcmp(widgGetString(psWScreen, MULTIOP_CHATEDIT), ""))
3866 {
3867 break;
3868 }
3869
3870 sendRoomChatMessage(widgGetString(psWScreen, MULTIOP_CHATEDIT));
3871 widgSetString(psWScreen, MULTIOP_CHATEDIT, "");
3872 break;
3873
3874 case CON_CANCEL:
3875 pie_LoadBackDrop(SCREEN_RANDOMBDROP);
3876 setHostLaunch(HostLaunch::Normal); // Dont load the autohost file on subsequent hosts
3877 performedFirstStart = false; // Reset everything
3878 if (!challengeActive)
3879 {
3880 if (NetPlay.bComms && ingame.side == InGameSide::MULTIPLAYER_CLIENT && !NetPlay.isHost)
3881 {
3882 // remove a potential "allow" vote if we gracefully leave
3883 sendVoteData(0);
3884 }
3885 NETGameLocked(false); // reset status on a cancel
3886 stopJoining(parent);
3887 }
3888 else
3889 {
3890 NETclose();
3891 ingame.localJoiningInProgress = false;
3892 changeTitleMode(SINGLE);
3893 addChallenges();
3894 }
3895 break;
3896 case MULTIOP_MAP_PREVIEW:
3897 loadMapPreview(true);
3898 break;
3899 default:
3900 break;
3901 }
3902
3903 STATIC_ASSERT(MULTIOP_TEAMS_START + MAX_PLAYERS - 1 <= MULTIOP_TEAMS_END);
3904 if (id >= MULTIOP_TEAMS_START && id <= MULTIOP_TEAMS_START + MAX_PLAYERS - 1 && !locked.teams) // Clicked on a team chooser
3905 {
3906 int clickedMenuID = id - MULTIOP_TEAMS_START;
3907
3908 //make sure team chooser is not up before adding new one for another player
3909 if (canChooseTeamFor(clickedMenuID) && positionChooserUp < 0)
3910 {
3911 openTeamChooser(clickedMenuID);
3912 }
3913 }
3914
3915 // 'ready' button
3916 if (id >= MULTIOP_READY_START && id <= MULTIOP_READY_END) // clicked on a player
3917 {
3918 UBYTE player = (UBYTE)(id - MULTIOP_READY_START);
3919
3920 if (player == selectedPlayer && positionChooserUp < 0)
3921 {
3922 // Lock the "ready" button (until the request is processed)
3923 widgSetButtonState(psWScreen, id, WBUT_LOCK);
3924
3925 SendReadyRequest(selectedPlayer, !NetPlay.players[player].ready);
3926
3927 // if hosting try to start the game if everyone is ready
3928 if (NetPlay.isHost && multiplayPlayersReady())
3929 {
3930 startMultiplayerGame();
3931 // reset flag in case people dropped/quit on join screen
3932 NETsetPlayerConnectionStatus(CONNECTIONSTATUS_NORMAL, NET_ALL_PLAYERS);
3933 }
3934 }
3935
3936 if (NetPlay.isHost && !alliancesSetTeamsBeforeGame(game.alliance))
3937 {
3938 if (mouseDown(MOUSE_RMB) && player != NetPlay.hostPlayer) // both buttons....
3939 {
3940 std::string msg = astringf(_("The host has kicked %s from the game!"), getPlayerName(player));
3941 sendRoomSystemMessage(msg.c_str());
3942 kickPlayer(player, _("The host has kicked you from the game."), ERROR_KICKED);
3943 resetReadyStatus(true); //reset and send notification to all clients
3944 }
3945 }
3946 }
3947
3948 if (id >= MULTIOP_COLOUR_START && id <= MULTIOP_COLOUR_END && (id - MULTIOP_COLOUR_START == selectedPlayer || NetPlay.isHost))
3949 {
3950 if (positionChooserUp < 0) // not choosing something else already
3951 {
3952 openColourChooser(id - MULTIOP_COLOUR_START);
3953 }
3954 }
3955
3956 // clicked on a player
3957 STATIC_ASSERT(MULTIOP_PLAYER_START + MAX_PLAYERS - 1 <= MULTIOP_PLAYER_END);
3958 if (id >= MULTIOP_PLAYER_START && id <= MULTIOP_PLAYER_START + MAX_PLAYERS - 1
3959 && (id - MULTIOP_PLAYER_START == selectedPlayer || NetPlay.isHost
3960 || (positionChooserUp >= 0 && !isHumanPlayer(id - MULTIOP_PLAYER_START))))
3961 {
3962 int player = id - MULTIOP_PLAYER_START;
3963 if ((player == selectedPlayer || (NetPlay.players[player].allocated && NetPlay.isHost))
3964 && !locked.position
3965 && positionChooserUp < 0)
3966 {
3967 openPositionChooser(player);
3968 }
3969 else if (positionChooserUp == player)
3970 {
3971 closePositionChooser(); // changed his mind
3972 addPlayerBox(true);
3973 }
3974 else if (positionChooserUp >= 0)
3975 {
3976 // Switch player
3977 resetReadyStatus(false); // will reset only locally if not a host
3978 SendPositionRequest(positionChooserUp, NetPlay.players[player].position);
3979 closePositionChooser();
3980 addPlayerBox(true);
3981 }
3982 else if (!NetPlay.players[player].allocated && !locked.ai && NetPlay.isHost
3983 && positionChooserUp < 0)
3984 {
3985 if (widgGetButtonKey_DEPRECATED(psWScreen) == WKEY_SECONDARY)
3986 {
3987 // Right clicking distributes selected AI's type and difficulty to all other AIs
3988 for (int i = 0; i < MAX_PLAYERS; ++i)
3989 {
3990 // Don't change open/closed slots or humans
3991 if (NetPlay.players[i].ai >= 0 && i != player && !isHumanPlayer(i))
3992 {
3993 NetPlay.players[i].ai = NetPlay.players[player].ai;
3994 NetPlay.players[i].difficulty = NetPlay.players[player].difficulty;
3995 sstrcpy(NetPlay.players[i].name, getAIName(player));
3996 NETBroadcastPlayerInfo(i);
3997 }
3998 }
3999 addPlayerBox(true);
4000 resetReadyStatus(false);
4001 }
4002 else
4003 {
4004 openAiChooser(player);
4005 }
4006 }
4007 }
4008
4009 if (id >= MULTIOP_DIFFICULTY_INIT_START && id <= MULTIOP_DIFFICULTY_INIT_END
4010 && !locked.difficulty && NetPlay.isHost && positionChooserUp < 0)
4011 {
4012 openDifficultyChooser(id - MULTIOP_DIFFICULTY_INIT_START);
4013 addPlayerBox(true);
4014 }
4015
4016 // clicked on faction chooser button
4017 if (id >= MULTIOP_FACTION_START && id <= MULTIOP_FACTION_END && (id - MULTIOP_FACTION_START == selectedPlayer || NetPlay.isHost))
4018 {
4019 if (positionChooserUp < 0) // not choosing something else already
4020 {
4021 openFactionChooser(id - MULTIOP_FACTION_START);
4022 }
4023 }
4024 }
4025
4026 /* Start a multiplayer or skirmish game */
startMultiplayerGame()4027 void startMultiplayerGame()
4028 {
4029 ASSERT_HOST_ONLY(return);
4030
4031 decideWRF(); // set up swrf & game.map
4032 bMultiPlayer = true;
4033 bMultiMessages = true;
4034 NETsetPlayerConnectionStatus(CONNECTIONSTATUS_NORMAL, NET_ALL_PLAYERS); // reset disconnect conditions
4035 initLoadingScreen(true);
4036 if (NetPlay.isHost)
4037 {
4038 // This sets the limits to whatever the defaults are for the limiter screen
4039 // If host sets limits, then we do not need to do the following routine.
4040 if (!bLimiterLoaded)
4041 {
4042 debug(LOG_NET, "limiter was NOT activated, setting defaults");
4043
4044 if (!resLoad("wrf/limiter_data.wrf", 503))
4045 {
4046 debug(LOG_INFO, "Unable to load limiter_data.");
4047 }
4048 }
4049 else
4050 {
4051 debug(LOG_NET, "limiter was activated");
4052 }
4053
4054 resetDataHash(); // need to reset it, since host's data has changed.
4055 createLimitSet();
4056 debug(LOG_NET, "sending our options to all clients");
4057 sendOptions();
4058 NEThaltJoining(); // stop new players entering.
4059 ingame.TimeEveryoneIsInGame = 0;
4060 ingame.isAllPlayersDataOK = false;
4061 memset(&ingame.DataIntegrity, 0x0, sizeof(ingame.DataIntegrity)); //clear all player's array
4062 SendFireUp(); //bcast a fireup message
4063 }
4064
4065 debug(LOG_NET, "title mode STARTGAME is set--Starting Game!");
4066 changeTitleMode(STARTGAME);
4067
4068 if (NetPlay.isHost)
4069 {
4070 sendRoomSystemMessage(_("Host is Starting Game"));
4071 }
4072 }
4073
4074 // ////////////////////////////////////////////////////////////////////////////
4075 // Net message handling
4076
frontendMultiMessages(bool running)4077 void WzMultiplayerOptionsTitleUI::frontendMultiMessages(bool running)
4078 {
4079 NETQUEUE queue;
4080 uint8_t type;
4081 bool ignoredMessage = false;
4082
4083 while (NETrecvNet(&queue, &type))
4084 {
4085 // Copy the message to the global one used by the new NET API
4086 switch (type)
4087 {
4088 case NET_FILE_REQUESTED:
4089 recvMapFileRequested(queue);
4090 break;
4091
4092 case NET_FILE_PAYLOAD:
4093 {
4094 bool done = recvMapFileData(queue);
4095 if (running)
4096 {
4097 ((MultibuttonWidget *)widgGetFromID(psWScreen, MULTIOP_MAP_PREVIEW))->enable(done); // turn preview button on or off
4098 }
4099 break;
4100 }
4101
4102 case NET_FILE_CANCELLED:
4103 {
4104 ASSERT_HOST_ONLY(break);
4105
4106 Sha256 hash;
4107 hash.setZero();
4108
4109 NETbeginDecode(queue, NET_FILE_CANCELLED);
4110 NETbin(hash.bytes, hash.Bytes);
4111 NETend();
4112
4113 debug(LOG_WARNING, "Received file cancel request from player %u, they weren't expecting the file.", queue.index);
4114 auto &wzFiles = NetPlay.players[queue.index].wzFiles;
4115 wzFiles.erase(std::remove_if(wzFiles.begin(), wzFiles.end(), [&](WZFile const &file) { return file.hash == hash; }), wzFiles.end());
4116 }
4117 break;
4118
4119 case NET_OPTIONS: // incoming options file.
4120 recvOptions(queue);
4121 bInActualHostedLobby = true;
4122 ingame.localOptionsReceived = true;
4123
4124 if (std::dynamic_pointer_cast<WzMultiplayerOptionsTitleUI>(wzTitleUICurrent))
4125 {
4126 addGameOptions();
4127 disableMultiButs();
4128 addChatBox();
4129 }
4130 break;
4131
4132 case GAME_ALLIANCE:
4133 recvAlliance(queue, false);
4134 break;
4135
4136 case NET_COLOURREQUEST:
4137 if (multiplayIsStartingGame())
4138 {
4139 ignoredMessage = true;
4140 break;
4141 }
4142 recvColourRequest(queue);
4143 break;
4144
4145 case NET_FACTIONREQUEST:
4146 if (multiplayIsStartingGame())
4147 {
4148 ignoredMessage = true;
4149 break;
4150 }
4151 recvFactionRequest(queue);
4152 break;
4153
4154 case NET_POSITIONREQUEST:
4155 if (multiplayIsStartingGame())
4156 {
4157 ignoredMessage = true;
4158 break;
4159 }
4160 recvPositionRequest(queue);
4161 break;
4162
4163 case NET_TEAMREQUEST:
4164 if (multiplayIsStartingGame())
4165 {
4166 ignoredMessage = true;
4167 break;
4168 }
4169 recvTeamRequest(queue);
4170 break;
4171
4172 case NET_READY_REQUEST:
4173 if (multiplayIsStartingGame())
4174 {
4175 ignoredMessage = true;
4176 break;
4177 }
4178 recvReadyRequest(queue);
4179
4180 // If hosting and game not yet started, try to start the game if everyone is ready.
4181 if (NetPlay.isHost && multiplayPlayersReady())
4182 {
4183 startMultiplayerGame();
4184 }
4185 break;
4186
4187 case NET_PING: // diagnostic ping msg.
4188 recvPing(queue);
4189 break;
4190
4191 case NET_PLAYER_DROPPED: // remote player got disconnected
4192 {
4193 uint32_t player_id = MAX_PLAYERS;
4194
4195 resetReadyStatus(false);
4196
4197 NETbeginDecode(queue, NET_PLAYER_DROPPED);
4198 {
4199 NETuint32_t(&player_id);
4200 }
4201 NETend();
4202
4203 if (player_id >= MAX_PLAYERS)
4204 {
4205 debug(LOG_INFO, "** player %u has dropped - huh?", player_id);
4206 break;
4207 }
4208
4209 if (whosResponsible(player_id) != queue.index && queue.index != NET_HOST_ONLY)
4210 {
4211 HandleBadParam("NET_PLAYER_DROPPED given incorrect params.", player_id, queue.index);
4212 break;
4213 }
4214
4215 debug(LOG_INFO, "** player %u has dropped!", player_id);
4216
4217 MultiPlayerLeave(player_id); // get rid of their stuff
4218 NET_InitPlayer(player_id, false); // sets index player's array to false
4219 NETsetPlayerConnectionStatus(CONNECTIONSTATUS_PLAYER_DROPPED, player_id);
4220 playerVotes[player_id] = 0;
4221 ActivityManager::instance().updateMultiplayGameData(game, ingame, NETGameIsLocked());
4222 if (player_id == NetPlay.hostPlayer || player_id == selectedPlayer) // if host quits or we quit, abort out
4223 {
4224 stopJoining(parent);
4225 }
4226 break;
4227 }
4228 case NET_PLAYERRESPONDING: // remote player is now playing.
4229 {
4230 uint32_t player_id;
4231
4232 resetReadyStatus(false);
4233
4234 NETbeginDecode(queue, NET_PLAYERRESPONDING);
4235 // the player that has just responded
4236 NETuint32_t(&player_id);
4237 NETend();
4238
4239 ingame.JoiningInProgress[player_id] = false;
4240 ingame.DataIntegrity[player_id] = false;
4241 break;
4242 }
4243 case NET_FIREUP: // campaign game started.. can fire the whole shebang up...
4244 cancelOrDismissNotificationsWithTag(VOTE_TAG); // don't need vote notifications anymore
4245 if (NET_HOST_ONLY != queue.index)
4246 {
4247 HandleBadParam("NET_FIREUP given incorrect params.", 255, queue.index);
4248 break;
4249 }
4250 debug(LOG_NET, "NET_FIREUP was received ...");
4251 if (ingame.localOptionsReceived)
4252 {
4253 uint32_t randomSeed = 0;
4254 NETbeginDecode(queue, NET_FIREUP);
4255 NETuint32_t(&randomSeed);
4256 NETend();
4257
4258 gameSRand(randomSeed); // Set the seed for the synchronised random number generator, using the seed given by the host.
4259
4260 debug(LOG_NET, "& local Options Received (MP game)");
4261 ingame.TimeEveryoneIsInGame = 0; // reset time
4262 resetDataHash();
4263 decideWRF();
4264
4265 bMultiPlayer = true;
4266 bMultiMessages = true;
4267 changeTitleMode(STARTGAME);
4268
4269 // Start the game before processing more messages.
4270 NETpop(queue);
4271 return;
4272 }
4273 ASSERT(false, "NET_FIREUP was received, but !ingame.localOptionsReceived.");
4274 break;
4275
4276 case NET_KICK: // player is forcing someone to leave
4277 {
4278 uint32_t player_id;
4279 char reason[MAX_KICK_REASON];
4280 LOBBY_ERROR_TYPES KICK_TYPE = ERROR_NOERROR;
4281
4282 NETbeginDecode(queue, NET_KICK);
4283 NETuint32_t(&player_id);
4284 NETstring(reason, MAX_KICK_REASON);
4285 NETenum(&KICK_TYPE);
4286 NETend();
4287
4288 if (player_id >= MAX_PLAYERS)
4289 {
4290 debug(LOG_ERROR, "NET_KICK message with invalid player_id: (%" PRIu32")", player_id);
4291 break;
4292 }
4293
4294 playerVotes[player_id] = 0;
4295
4296 if (player_id == NET_HOST_ONLY)
4297 {
4298 char buf[250] = {'\0'};
4299
4300 ssprintf(buf, "*Player %d (%s : %s) tried to kick %u", (int) queue.index, NetPlay.players[queue.index].name, NetPlay.players[queue.index].IPtextAddress, player_id);
4301 NETlogEntry(buf, SYNC_FLAG, 0);
4302 debug(LOG_ERROR, "%s", buf);
4303 if (NetPlay.isHost)
4304 {
4305 NETplayerKicked((unsigned int) queue.index);
4306 }
4307 break;
4308 }
4309
4310 if (selectedPlayer == player_id) // we've been told to leave.
4311 {
4312 setLobbyError(KICK_TYPE);
4313 stopJoining(std::make_shared<WzMsgBoxTitleUI>(WzString(_("You have been kicked: ")) + reason, parent));
4314 debug(LOG_INFO, "You have been kicked, because %s ", reason);
4315 displayKickReasonPopup(reason);
4316 ActivityManager::instance().wasKickedByPlayer(NetPlay.players[queue.index], KICK_TYPE, reason);
4317 }
4318 else
4319 {
4320 NETplayerKicked(player_id);
4321 }
4322 break;
4323 }
4324 case NET_HOST_DROPPED:
4325 NETbeginDecode(queue, NET_HOST_DROPPED);
4326 NETend();
4327 stopJoining(std::make_shared<WzMsgBoxTitleUI>(WzString(_("No connection to host.")), parent));
4328 debug(LOG_NET, "The host has quit!");
4329 setLobbyError(ERROR_HOSTDROPPED);
4330 break;
4331
4332 case NET_TEXTMSG: // Chat message
4333 if (ingame.localOptionsReceived)
4334 {
4335 NetworkTextMessage message;
4336 if (message.receive(queue)) {
4337 displayRoomMessage(buildMessage(message.sender, message.text));
4338 audio_PlayTrack(FE_AUDIO_MESSAGEEND);
4339 }
4340 }
4341 break;
4342
4343 case NET_VOTE:
4344 if (NetPlay.isHost && ingame.localOptionsReceived)
4345 {
4346 recvVote(queue);
4347 }
4348 break;
4349
4350 case NET_VOTE_REQUEST:
4351 if (!NetPlay.isHost)
4352 {
4353 setupVoteChoice();
4354 }
4355 break;
4356
4357 default:
4358 ignoredMessage = true;
4359 break;
4360 }
4361
4362 if (ignoredMessage)
4363 {
4364 debug(LOG_ERROR, "Didn't handle %s message!", messageTypeToString(type));
4365 }
4366
4367 NETpop(queue);
4368 }
4369 }
4370
run()4371 TITLECODE WzMultiplayerOptionsTitleUI::run()
4372 {
4373 static UDWORD lastrefresh = 0;
4374 PLAYERSTATS playerStats;
4375
4376 frontendMultiMessages(true);
4377 if (NetPlay.isHost)
4378 {
4379 // send it for each player that needs it
4380 sendMap();
4381 }
4382
4383 // update boxes?
4384 if (netPlayersUpdated || (NetPlay.isHost && mouseDown(MOUSE_LMB) && gameTime - lastrefresh > 500))
4385 {
4386 netPlayersUpdated = false;
4387 lastrefresh = gameTime;
4388 if (!multiRequestUp && (NetPlay.isHost || ingame.localJoiningInProgress))
4389 {
4390 addPlayerBox(true); // update the player box.
4391 loadMapPreview(false);
4392 }
4393 }
4394
4395
4396 // update scores and pings if far enough into the game
4397 if (ingame.localOptionsReceived && ingame.localJoiningInProgress)
4398 {
4399 sendScoreCheck();
4400 sendPing();
4401 }
4402
4403 // if we don't have the focus, then autoclick in the chatbox.
4404 if (psWScreen->psFocus.expired())
4405 {
4406 W_CONTEXT context = W_CONTEXT::ZeroContext();
4407 context.mx = mouseX();
4408 context.my = mouseY();
4409
4410 W_EDITBOX* pChatEdit = dynamic_cast<W_EDITBOX*>(widgGetFromID(psWScreen, MULTIOP_CHATEDIT));
4411 if (pChatEdit)
4412 {
4413 pChatEdit->simulateClick(&context, true);
4414 }
4415 }
4416
4417 // chat box handling
4418 if (widgGetFromID(psWScreen, MULTIOP_CHATBOX))
4419 {
4420 while (getNumberConsoleMessages() > getConsoleLineInfo())
4421 {
4422 removeTopConsoleMessage();
4423 }
4424 updateConsoleMessages(); // run the chatbox
4425 }
4426
4427 // widget handling
4428
4429 /* Map or player selection is open */
4430 if (multiRequestUp)
4431 {
4432 WidgetTriggers const &triggers = widgRunScreen(psRScreen);
4433 unsigned id = triggers.empty() ? 0 : triggers.front().widget->id; // Just use first click here, since the next click could be on another menu.
4434
4435 LEVEL_DATASET *mapData = nullptr;
4436 bool isHoverPreview = false;
4437 WzString sTemp;
4438 if (runMultiRequester(id, &id, &sTemp, &mapData, &isHoverPreview))
4439 {
4440 switch (id)
4441 {
4442 case MULTIOP_PNAME:
4443 sstrcpy(sPlayer, sTemp.toUtf8().c_str());
4444 widgSetString(psWScreen, MULTIOP_PNAME, sTemp.toUtf8().c_str());
4445
4446 removeWildcards((char *)sPlayer);
4447
4448 printConsoleNameChange(NetPlay.players[selectedPlayer].name, sPlayer);
4449
4450 NETchangePlayerName(selectedPlayer, (char *)sPlayer);
4451 loadMultiStats((char *)sPlayer, &playerStats);
4452 setMultiStats(selectedPlayer, playerStats, false);
4453 setMultiStats(selectedPlayer, playerStats, true);
4454 lookupRatingAsync(selectedPlayer);
4455 netPlayersUpdated = true;
4456 if (NetPlay.isHost && NetPlay.bComms)
4457 {
4458 sendOptions();
4459 NETsetLobbyOptField(NetPlay.players[selectedPlayer].name, NET_LOBBY_OPT_FIELD::HOSTNAME);
4460 NETregisterServer(WZ_SERVER_UPDATE);
4461 }
4462 break;
4463 case MULTIOP_MAP:
4464 {
4465 if (isHoverPreview)
4466 {
4467 char oldGameMap[128];
4468
4469 sstrcpy(oldGameMap, game.map);
4470 Sha256 oldGameHash = game.hash;
4471 uint8_t oldMaxPlayers = game.maxPlayers;
4472 bool oldGameIsMapMod = game.isMapMod;
4473 bool oldGameIsRandom = game.isRandom;
4474
4475 sstrcpy(game.map, mapData->pName);
4476 game.hash = levGetFileHash(mapData);
4477 game.maxPlayers = mapData->players;
4478 game.isMapMod = CheckForMod(mapData->realFileName);
4479 game.isRandom = CheckForRandom(mapData->realFileName, mapData->apDataFiles[0]);
4480 loadMapPreview(false);
4481
4482 /* Change game info to match the previous selection if hover preview was displayed */
4483 sstrcpy(game.map, oldGameMap);
4484 game.hash = oldGameHash;
4485 game.maxPlayers = oldMaxPlayers;
4486 game.isMapMod = oldGameIsMapMod;
4487 game.isRandom = oldGameIsRandom;
4488 break;
4489 }
4490
4491 if (NetPlay.bComms && NetPlay.isHost)
4492 {
4493 uint8_t numHumans = NET_numHumanPlayers();
4494 if (numHumans > mapData->players)
4495 {
4496 displayRoomSystemMessage(_("Cannot change to a map with too few slots for all players."));
4497 break;
4498 }
4499 if (mapData->players < game.maxPlayers)
4500 {
4501 displayRoomSystemMessage(_("Cannot change to a map with fewer slots."));
4502 break;
4503 }
4504 if (!canChangeMapOrRandomize())
4505 {
4506 break;
4507 }
4508 }
4509
4510 uint8_t oldMaxPlayers = game.maxPlayers;
4511
4512 sstrcpy(game.map, mapData->pName);
4513 game.hash = levGetFileHash(mapData);
4514 game.maxPlayers = mapData->players;
4515 game.isMapMod = CheckForMod(mapData->realFileName);
4516 game.isRandom = CheckForRandom(mapData->realFileName, mapData->apDataFiles[0]);
4517 loadMapPreview(true);
4518 loadMapChallengeAndPlayerSettings();
4519
4520 WzString name = formatGameName(game.map);
4521 widgSetString(psWScreen, MULTIOP_MAP + 1, name.toUtf8().c_str()); //What a horrible, horrible way to do this! FIX ME! (See addBlueForm)
4522
4523 //Reset player slots if it's a smaller map.
4524 if (NetPlay.isHost && NetPlay.bComms && oldMaxPlayers > game.maxPlayers)
4525 {
4526 resetPlayerPositions();
4527 repositionHumanSlots();
4528
4529 const std::vector<uint8_t>& humans = NET_getHumanPlayers();
4530 size_t playerInc = 0;
4531
4532 for (uint8_t slotInc = 0; slotInc < game.maxPlayers && playerInc < humans.size(); ++slotInc)
4533 {
4534 changePosition(humans[playerInc], slotInc);
4535 ++playerInc;
4536 }
4537 }
4538
4539 addGameOptions();
4540
4541 if (NetPlay.isHost && NetPlay.bComms)
4542 {
4543 sendOptions();
4544 NETsetLobbyOptField(game.map, NET_LOBBY_OPT_FIELD::MAPNAME);
4545 NETregisterServer(WZ_SERVER_UPDATE);
4546 }
4547 }
4548 break;
4549 default:
4550 loadMapPreview(false); // Restore the preview of the old map.
4551 break;
4552 }
4553 if (!isHoverPreview)
4554 {
4555 addPlayerBox(ingame.side != InGameSide::HOST_OR_SINGLEPLAYER);
4556 }
4557 }
4558 }
4559 /* Map/Player selection (multi-requester) is closed */
4560 else
4561 {
4562 if (hideTime != 0)
4563 {
4564 // we abort the 'hidetime' on press of a mouse button.
4565 if (gameTime - hideTime < MAP_PREVIEW_DISPLAY_TIME && !mousePressed(MOUSE_LMB) && !mousePressed(MOUSE_RMB))
4566 {
4567 return TITLECODE_CONTINUE;
4568 }
4569 inputLoseFocus(); // remove the mousepress from the input stream.
4570 hideTime = 0;
4571 }
4572 else
4573 {
4574 WidgetTriggers const &triggers = widgRunScreen(psWScreen);
4575 unsigned id = triggers.empty() ? 0 : triggers.front().widget->id; // Just use first click here, since the next click could be on another menu.
4576
4577 if (!triggers.empty() && (!multiplayIsStartingGame() || id == CON_CANCEL))
4578 {
4579 processMultiopWidgets(id);
4580 }
4581 }
4582 }
4583
4584 widgDisplayScreen(psWScreen); // show the widgets currently running
4585
4586 if (multiRequestUp)
4587 {
4588 widgDisplayScreen(psRScreen); // show the Requester running
4589 }
4590
4591 if (CancelPressed())
4592 {
4593 processMultiopWidgets(CON_CANCEL); // "Press" the cancel button to clean up net connections and stuff.
4594 }
4595 if (!NetPlay.isHostAlive && ingame.side == InGameSide::MULTIPLAYER_CLIENT)
4596 {
4597 cancelOrDismissNotificationsWithTag(VOTE_TAG);
4598 changeTitleUI(std::make_shared<WzMsgBoxTitleUI>(WzString(_("The host has quit.")), parent));
4599 pie_LoadBackDrop(SCREEN_RANDOMBDROP);
4600 }
4601
4602 return TITLECODE_CONTINUE;
4603 }
4604
WzMultiplayerOptionsTitleUI(std::shared_ptr<WzTitleUI> parent)4605 WzMultiplayerOptionsTitleUI::WzMultiplayerOptionsTitleUI(std::shared_ptr<WzTitleUI> parent)
4606 : parent(parent)
4607 , inlineChooserUp(-1)
4608 , aiChooserUp(-1)
4609 , difficultyChooserUp(-1)
4610 , positionChooserUp(-1)
4611 {
4612 }
4613
~WzMultiplayerOptionsTitleUI()4614 WzMultiplayerOptionsTitleUI::~WzMultiplayerOptionsTitleUI()
4615 {
4616 widgRemoveOverlayScreen(psInlineChooserOverlayScreen);
4617 bInActualHostedLobby = false;
4618 }
4619
screenSizeDidChange(unsigned int oldWidth,unsigned int oldHeight,unsigned int newWidth,unsigned int newHeight)4620 void WzMultiplayerOptionsTitleUI::screenSizeDidChange(unsigned int oldWidth, unsigned int oldHeight, unsigned int newWidth, unsigned int newHeight)
4621 {
4622 // NOTE: To properly support resizing the inline overlay screen based on underlying screen layer recalculations
4623 // frontendScreenSizeDidChange() should be called after intScreenSizeDidChange() in gameScreenSizeDidChange()
4624 if (psInlineChooserOverlayScreen == nullptr) return;
4625 psInlineChooserOverlayScreen->screenSizeDidChange(oldWidth, oldHeight, newWidth, newHeight);
4626 }
4627
printHostHelpMessagesToConsole()4628 static void printHostHelpMessagesToConsole()
4629 {
4630 char buf[512] = { '\0' };
4631 if (NetPlay.bComms)
4632 {
4633 if (NetPlay.isUPNP)
4634 {
4635 if (NetPlay.isUPNP_CONFIGURED)
4636 {
4637 ssprintf(buf, "%s", _("UPnP has been enabled."));
4638 }
4639 else
4640 {
4641 if (NetPlay.isUPNP_ERROR)
4642 {
4643 ssprintf(buf, "%s", _("UPnP detection failed. You must manually configure router yourself."));
4644 }
4645 else
4646 {
4647 ssprintf(buf, "%s", _("UPnP detection is in progress..."));
4648 }
4649 }
4650 displayRoomNotifyMessage(buf);
4651 }
4652 else
4653 {
4654 ssprintf(buf, "%s", _("UPnP detection disabled by user. Autoconfig of port 2100 will not happen."));
4655 displayRoomNotifyMessage(buf);
4656 }
4657 }
4658 if (challengeActive)
4659 {
4660 ssprintf(buf, "%s", _("Hit the ready box to begin your challenge!"));
4661 }
4662 else if (!NetPlay.isHost)
4663 {
4664 ssprintf(buf, "%s", _("Press the start hosting button to begin hosting a game."));
4665 }
4666 displayRoomNotifyMessage(buf);
4667 }
4668
calcBackdropLayoutForMultiplayerOptionsTitleUI(WIDGET * psWidget,unsigned int,unsigned int,unsigned int newScreenWidth,unsigned int newScreenHeight)4669 void calcBackdropLayoutForMultiplayerOptionsTitleUI(WIDGET *psWidget, unsigned int, unsigned int, unsigned int newScreenWidth, unsigned int newScreenHeight)
4670 {
4671 auto height = newScreenHeight - 80;
4672 CLIP(height, HIDDEN_FRONTEND_HEIGHT, HIDDEN_FRONTEND_WIDTH);
4673
4674 psWidget->setGeometry(
4675 ((newScreenWidth - HIDDEN_FRONTEND_WIDTH) / 2),
4676 ((newScreenHeight - height) / 2),
4677 HIDDEN_FRONTEND_WIDTH - 1,
4678 height
4679 );
4680 }
4681
start()4682 void WzMultiplayerOptionsTitleUI::start()
4683 {
4684 const bool bReenter = performedFirstStart;
4685 performedFirstStart = true;
4686 netPlayersUpdated = true;
4687
4688 addBackdrop()->setCalcLayout(calcBackdropLayoutForMultiplayerOptionsTitleUI);
4689 addTopForm(true);
4690
4691 if (getLobbyError() != ERROR_INVALID)
4692 {
4693 setLobbyError(ERROR_NOERROR);
4694 }
4695
4696 /* Entering the first time */
4697 if (!bReenter)
4698 {
4699 initKnownPlayers();
4700 resetPlayerConfiguration(true);
4701 memset(&locked, 0, sizeof(locked));
4702 loadMapChallengeAndPlayerSettings(true);
4703 game.isMapMod = false;
4704 game.isRandom = false;
4705 game.mapHasScavengers = true; // FIXME, should default to false
4706
4707 inlineChooserUp = -1;
4708 aiChooserUp = -1;
4709 difficultyChooserUp = -1;
4710 positionChooserUp = -1;
4711
4712 // Initialize the inline chooser overlay screen
4713 psInlineChooserOverlayScreen = W_SCREEN::make();
4714 auto newRootFrm = W_FULLSCREENOVERLAY_CLICKFORM::make(MULTIOP_INLINE_OVERLAY_ROOT_FRM);
4715 std::weak_ptr<W_SCREEN> psWeakInlineOverlayScreen(psInlineChooserOverlayScreen);
4716 WzMultiplayerOptionsTitleUI *psTitleUI = this;
4717 newRootFrm->onClickedFunc = [psWeakInlineOverlayScreen, psTitleUI]() {
4718 if (auto psOverlayScreen = psWeakInlineOverlayScreen.lock())
4719 {
4720 widgRemoveOverlayScreen(psOverlayScreen);
4721 }
4722 psTitleUI->closeAllChoosers();
4723 psTitleUI->addPlayerBox(true);
4724 };
4725 newRootFrm->onCancelPressed = newRootFrm->onClickedFunc;
4726 psInlineChooserOverlayScreen->psForm->attach(newRootFrm);
4727
4728 ingame.localOptionsReceived = false;
4729
4730 PLAYERSTATS nullStats;
4731 loadMultiStats((char*)sPlayer, &nullStats);
4732 lookupRatingAsync(selectedPlayer);
4733
4734 /* Entering the first time with challenge, immediately start the host */
4735 if (challengeActive && !startHost())
4736 {
4737 debug(LOG_ERROR, "Failed to host the challenge.");
4738 return;
4739 }
4740 }
4741
4742 loadMapPreview(false);
4743
4744 /* Re-entering or entering without a challenge */
4745 if (bReenter || !challengeActive)
4746 {
4747 addPlayerBox(false);
4748 addGameOptions();
4749 addChatBox(bReenter);
4750
4751 if (ingame.side == InGameSide::HOST_OR_SINGLEPLAYER)
4752 {
4753 printHostHelpMessagesToConsole();
4754 }
4755 }
4756
4757 /* Reset structure limits if we are entering the first time or if we have a challenge */
4758 if (!bReenter || challengeActive)
4759 {
4760 resetLimits();
4761 updateStructureDisabledFlags();
4762 updateLimitIcons();
4763 }
4764
4765 if (autogame_enabled() || getHostLaunch() == HostLaunch::Autohost)
4766 {
4767 if (!ingame.localJoiningInProgress)
4768 {
4769 processMultiopWidgets(MULTIOP_HOST);
4770 }
4771 SendReadyRequest(selectedPlayer, true);
4772 if (getHostLaunch() == HostLaunch::Skirmish)
4773 {
4774 startMultiplayerGame();
4775 // reset flag in case people dropped/quit on join screen
4776 NETsetPlayerConnectionStatus(CONNECTIONSTATUS_NORMAL, NET_ALL_PLAYERS);
4777 }
4778 }
4779 }
4780
4781 /////////////////////////////////////////////////////////////////////////////////////////
4782 /////////////////////////////////////////////////////////////////////////////////////////
4783 // Drawing functions
4784
displayChatEdit(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)4785 void displayChatEdit(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
4786 {
4787 int x = xOffset + psWidget->x();
4788 int y = yOffset + psWidget->y();
4789
4790 // draws the line at the bottom of the multiplayer join dialog separating the chat
4791 // box from the input box
4792 iV_Line(x, y, x + psWidget->width(), y, WZCOL_MENU_SEPARATOR);
4793 }
4794
4795 // ////////////////////////////////////////////////////////////////////////////
4796
displayTeamChooser(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)4797 void displayTeamChooser(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
4798 {
4799 int x = xOffset + psWidget->x();
4800 int y = yOffset + psWidget->y();
4801 UDWORD i = psWidget->UserData;
4802
4803 ASSERT_OR_RETURN(, i < MAX_PLAYERS && NetPlay.players[i].team >= 0 && NetPlay.players[i].team < MAX_PLAYERS, "Team index out of bounds");
4804
4805 drawBlueBox(x, y, psWidget->width(), psWidget->height());
4806
4807 if (NetPlay.players[i].difficulty != AIDifficulty::DISABLED)
4808 {
4809 iV_DrawImage(FrontImages, IMAGE_TEAM0 + NetPlay.players[i].team, x + 2, y + 8);
4810 }
4811 }
4812
displayPosition(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)4813 void displayPosition(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
4814 {
4815 // Any widget using displayPosition must have its pUserData initialized to a (DisplayPositionCache*)
4816 assert(psWidget->pUserData != nullptr);
4817 DisplayPositionCache& cache = *static_cast<DisplayPositionCache *>(psWidget->pUserData);
4818
4819 const int x = xOffset + psWidget->x();
4820 const int y = yOffset + psWidget->y();
4821 const int i = psWidget->UserData;
4822 char text[80];
4823
4824 drawBlueBox(x, y, psWidget->width(), psWidget->height());
4825 ssprintf(text, _("Click to take player slot %d"), NetPlay.players[i].position);
4826 cache.wzPositionText.setText(text, font_regular);
4827 cache.wzPositionText.render(x + 10, y + 22, WZCOL_FORM_TEXT);
4828 }
4829
displayAi(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)4830 static void displayAi(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
4831 {
4832 // Any widget using displayAi must have its pUserData initialized to a (DisplayAICache*)
4833 assert(psWidget->pUserData != nullptr);
4834 DisplayAICache& cache = *static_cast<DisplayAICache *>(psWidget->pUserData);
4835
4836 const int x = xOffset + psWidget->x();
4837 const int y = yOffset + psWidget->y();
4838 const int j = psWidget->UserData;
4839 const char *commsText[] = { N_("Open"), N_("Closed") };
4840
4841 drawBlueBox(x, y, psWidget->width(), psWidget->height());
4842 cache.wzText.setText((j >= 0) ? aidata[j].name : gettext(commsText[j + 2]), font_regular);
4843 cache.wzText.render(x + 10, y + 22, WZCOL_FORM_TEXT);
4844 }
4845
difficultyIcon(int difficulty)4846 static int difficultyIcon(int difficulty)
4847 {
4848 switch (difficulty)
4849 {
4850 case 0: return IMAGE_EASY;
4851 case 1: return IMAGE_MEDIUM;
4852 case 2: return IMAGE_HARD;
4853 case 3: return IMAGE_INSANE;
4854 default: return IMAGE_NO; /// what??
4855 }
4856 }
4857
factionIcon(FactionID faction)4858 static int factionIcon(FactionID faction)
4859 {
4860 switch (faction)
4861 {
4862 case 0: return IMAGE_FACTION_NORMAL;
4863 case 1: return IMAGE_FACTION_NEXUS;
4864 case 2: return IMAGE_FACTION_COLLECTIVE;
4865 default: return IMAGE_NO; /// what??
4866 }
4867 }
4868
displayDifficulty(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)4869 static void displayDifficulty(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
4870 {
4871 // Any widget using displayDifficulty must have its pUserData initialized to a (DisplayDifficultyCache*)
4872 assert(psWidget->pUserData != nullptr);
4873 DisplayDifficultyCache& cache = *static_cast<DisplayDifficultyCache *>(psWidget->pUserData);
4874
4875 const int x = xOffset + psWidget->x();
4876 const int y = yOffset + psWidget->y();
4877 const int j = psWidget->UserData;
4878
4879 ASSERT_OR_RETURN(, j < ARRAY_SIZE(difficultyList), "Bad difficulty found: %d", j);
4880 drawBlueBox(x, y, psWidget->width(), psWidget->height());
4881 iV_DrawImage(FrontImages, difficultyIcon(j), x + 5, y + 5);
4882 cache.wzDifficultyText.setText(gettext(difficultyList[j]), font_regular);
4883 cache.wzDifficultyText.render(x + 42, y + 22, WZCOL_FORM_TEXT);
4884 }
4885
isKnownPlayer(std::map<std::string,EcKey::Key> const & knownPlayers,std::string const & name,EcKey const & key)4886 static bool isKnownPlayer(std::map<std::string, EcKey::Key> const &knownPlayers, std::string const &name, EcKey const &key)
4887 {
4888 if (key.empty())
4889 {
4890 return false;
4891 }
4892 std::map<std::string, EcKey::Key>::const_iterator i = knownPlayers.find(name);
4893 return i != knownPlayers.end() && key.toBytes(EcKey::Public) == i->second;
4894 }
4895
4896 // ////////////////////////////////////////////////////////////////////////////
displayPlayer(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)4897 void displayPlayer(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
4898 {
4899 // Any widget using displayPlayer must have its pUserData initialized to a (DisplayPlayerCache*)
4900 assert(psWidget->pUserData != nullptr);
4901 DisplayPlayerCache& cache = *static_cast<DisplayPlayerCache *>(psWidget->pUserData);
4902
4903 int const x = xOffset + psWidget->x();
4904 int const y = yOffset + psWidget->y();
4905 unsigned const j = psWidget->UserData;
4906
4907 const int nameX = 32;
4908
4909 unsigned downloadProgress = NETgetDownloadProgress(j);
4910
4911 drawBlueBox(x, y, psWidget->width(), psWidget->height());
4912 if (downloadProgress != 100)
4913 {
4914 char progressString[MAX_STR_LENGTH];
4915 ssprintf(progressString, j != selectedPlayer ? _("Sending Map: %u%% ") : _("Map: %u%% downloaded"), downloadProgress);
4916 cache.wzMainText.setText(progressString, font_regular);
4917 cache.wzMainText.render(x + 5, y + 22, WZCOL_FORM_TEXT);
4918 return;
4919 }
4920 else if (ingame.localOptionsReceived && NetPlay.players[j].allocated) // only draw if real player!
4921 {
4922 std::string name = NetPlay.players[j].name;
4923
4924 drawBlueBox(x, y, psWidget->width(), psWidget->height());
4925
4926 std::map<std::string, EcKey::Key> serverPlayers; // TODO Fill this with players known to the server (needs implementing on the server, too). Currently useless.
4927
4928 PIELIGHT colour;
4929 if (ingame.PingTimes[j] >= PING_LIMIT)
4930 {
4931 colour = WZCOL_FORM_PLAYER_NOPING;
4932 }
4933 else if (isKnownPlayer(serverPlayers, name, getMultiStats(j).identity))
4934 {
4935 colour = WZCOL_FORM_PLAYER_KNOWN_BY_SERVER;
4936 }
4937 else if (isLocallyKnownPlayer(name, getMultiStats(j).identity))
4938 {
4939 colour = WZCOL_FORM_PLAYER_KNOWN;
4940 }
4941 else
4942 {
4943 colour = WZCOL_FORM_PLAYER_UNKNOWN;
4944 }
4945
4946 // name
4947 if (cache.fullMainText != name)
4948 {
4949 if ((int)iV_GetTextWidth(name.c_str(), font_regular) > psWidget->width() - nameX)
4950 {
4951 while (!name.empty() && (int)iV_GetTextWidth((name + "...").c_str(), font_regular) > psWidget->width() - nameX)
4952 {
4953 name.resize(name.size() - 1); // Clip name.
4954 }
4955 name += "...";
4956 }
4957 cache.wzMainText.setText(name, font_regular);
4958 cache.fullMainText = name;
4959 }
4960 std::string subText;
4961 if (j == NET_HOST_ONLY && NetPlay.bComms)
4962 {
4963 subText += _("HOST");
4964 }
4965 if (NetPlay.bComms && j != selectedPlayer)
4966 {
4967 char buf[250] = {'\0'};
4968
4969 // show "actual" ping time
4970 ssprintf(buf, "%s%s: ", subText.empty() ? "" : ", ", _("Ping"));
4971 subText += buf;
4972 if (ingame.PingTimes[j] < PING_LIMIT)
4973 {
4974 ssprintf(buf, "%03d", ingame.PingTimes[j]);
4975 }
4976 else
4977 {
4978 ssprintf(buf, "%s", "∞"); // Player has ping of somewhat questionable quality.
4979 }
4980 subText += buf;
4981 }
4982
4983 PLAYERSTATS stat = getMultiStats(j);
4984 auto ar = stat.autorating;
4985 if (!ar.valid)
4986 {
4987 ar.dummy = stat.played < 5;
4988 // star 1 total droid kills
4989 ar.star[0] = stat.totalKills > 600? 1 : stat.totalKills > 300? 2 : stat.totalKills > 150? 3 : 0;
4990
4991 // star 2 games played
4992 ar.star[1] = stat.played > 200? 1 : stat.played > 100? 2 : stat.played > 50? 3 : 0;
4993
4994 // star 3 games won.
4995 ar.star[2] = stat.wins > 80? 1 : stat.wins > 40? 2 : stat.wins > 10? 3 : 0;
4996
4997 // medals.
4998 ar.medal = stat.wins >= 24 && stat.wins > 8 * stat.losses? 1 : stat.wins >= 12 && stat.wins > 4 * stat.losses? 2 : stat.wins >= 6 && stat.wins > 2 * stat.losses? 3 : 0;
4999
5000 ar.level = 0;
5001 ar.autohoster = false;
5002 ar.elo.clear();
5003 }
5004
5005 int H = 5;
5006 cache.wzMainText.render(x + nameX, y + 22 - H*!subText.empty() - H*ar.valid, colour);
5007 if (!subText.empty())
5008 {
5009 cache.wzSubText.setText(subText, font_small);
5010 cache.wzSubText.render(x + nameX, y + 28 - H*!ar.elo.empty(), WZCOL_TEXT_MEDIUM);
5011 }
5012
5013 if (ar.autohoster)
5014 {
5015 iV_DrawImage(FrontImages, IMAGE_PLAYER_PC, x, y + 11);
5016 }
5017 else if (ar.dummy)
5018 {
5019 iV_DrawImage(FrontImages, IMAGE_MEDAL_DUMMY, x + 4, y + 13);
5020 }
5021 else
5022 {
5023 constexpr int starImgs[4] = {0, IMAGE_MULTIRANK1, IMAGE_MULTIRANK2, IMAGE_MULTIRANK3};
5024 if (1 <= ar.star[0] && ar.star[0] < ARRAY_SIZE(starImgs))
5025 {
5026 iV_DrawImage(FrontImages, starImgs[ar.star[0]], x + 4, y + 3);
5027 }
5028 if (1 <= ar.star[1] && ar.star[1] < ARRAY_SIZE(starImgs))
5029 {
5030 iV_DrawImage(FrontImages, starImgs[ar.star[1]], x + 4, y + 13);
5031 }
5032 if (1 <= ar.star[2] && ar.star[2] < ARRAY_SIZE(starImgs))
5033 {
5034 iV_DrawImage(FrontImages, starImgs[ar.star[2]], x + 4, y + 23);
5035 }
5036 constexpr int medalImgs[4] = {0, IMAGE_MEDAL_GOLD, IMAGE_MEDAL_SILVER, IMAGE_MEDAL_BRONZE};
5037 if (1 <= ar.medal && ar.medal < ARRAY_SIZE(medalImgs))
5038 {
5039 iV_DrawImage(FrontImages, medalImgs[ar.medal], x + 16 - 2*(ar.level != 0), y + 11);
5040 }
5041 }
5042 constexpr int levelImgs[9] = {0, IMAGE_LEV_0, IMAGE_LEV_1, IMAGE_LEV_2, IMAGE_LEV_3, IMAGE_LEV_4, IMAGE_LEV_5, IMAGE_LEV_6, IMAGE_LEV_7};
5043 if (1 <= ar.level && ar.level < ARRAY_SIZE(levelImgs))
5044 {
5045 iV_DrawImage(IntImages, levelImgs[ar.star[2]], x + 24, y + 15);
5046 }
5047
5048 if (!ar.elo.empty())
5049 {
5050 cache.wzEloText.setText(ar.elo, font_small);
5051 cache.wzEloText.render(x + nameX, y + 28 + H*!subText.empty(), WZCOL_TEXT_BRIGHT);
5052 }
5053
5054 NetPlay.players[j].difficulty = AIDifficulty::HUMAN;
5055 }
5056 else // AI
5057 {
5058 char aitext[80];
5059
5060 if (NetPlay.players[j].ai >= 0)
5061 {
5062 iV_DrawImage(FrontImages, IMAGE_PLAYER_PC, x, y + 11);
5063 }
5064
5065 // challenges may use custom AIs that are not in aidata and set to 127
5066 if (!challengeActive)
5067 {
5068 ASSERT_OR_RETURN(, NetPlay.players[j].ai < (int)aidata.size(), "Uh-oh, AI index out of bounds");
5069 }
5070
5071 switch (NetPlay.players[j].ai)
5072 {
5073 case AI_OPEN: sstrcpy(aitext, _("Open")); break;
5074 case AI_CLOSED: sstrcpy(aitext, _("Closed")); break;
5075 default: sstrcpy(aitext, NetPlay.players[j].name ); break;
5076 }
5077 cache.wzMainText.setText(aitext, font_regular);
5078 cache.wzMainText.render(x + nameX, y + 22, WZCOL_FORM_TEXT);
5079 }
5080 }
5081
displayColour(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)5082 void displayColour(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
5083 {
5084 const int x = xOffset + psWidget->x();
5085 const int y = yOffset + psWidget->y();
5086 const int j = psWidget->UserData;
5087
5088 drawBlueBox(x, y, psWidget->width(), psWidget->height());
5089 if (NetPlay.players[j].wzFiles.empty() && NetPlay.players[j].difficulty != AIDifficulty::DISABLED)
5090 {
5091 int player = getPlayerColour(j);
5092 STATIC_ASSERT(MAX_PLAYERS <= 16);
5093 iV_DrawImageTc(FrontImages, IMAGE_PLAYERN, IMAGE_PLAYERN_TC, x + 7, y + 9, pal_GetTeamColour(player));
5094 }
5095 }
5096
displayFaction(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)5097 void displayFaction(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
5098 {
5099 const int x = xOffset + psWidget->x();
5100 const int y = yOffset + psWidget->y();
5101 const int j = psWidget->UserData;
5102
5103 drawBlueBox(x, y, psWidget->width(), psWidget->height());
5104 if (NetPlay.players[j].wzFiles.empty() && NetPlay.players[j].difficulty != AIDifficulty::DISABLED)
5105 {
5106 FactionID faction = NetPlay.players[j].faction;
5107 iV_DrawImage(FrontImages, factionIcon(faction), x + 5, y + 8);
5108 }
5109 }
5110
5111 // ////////////////////////////////////////////////////////////////////////////
5112 // Display blue box
intDisplayFeBox(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)5113 void intDisplayFeBox(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
5114 {
5115 int x = xOffset + psWidget->x();
5116 int y = yOffset + psWidget->y();
5117 int w = psWidget->width();
5118 int h = psWidget->height();
5119
5120 drawBlueBox(x, y, w, h);
5121 }
5122
5123 // ////////////////////////////////////////////////////////////////////////////
5124 // Display edit box
displayMultiEditBox(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)5125 void displayMultiEditBox(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
5126 {
5127 int x = xOffset + psWidget->x();
5128 int y = yOffset + psWidget->y();
5129
5130 drawBlueBox(x, y, psWidget->width(), psWidget->height());
5131
5132 if (((W_EDITBOX *)psWidget)->state & WEDBS_DISABLE) // disabled
5133 {
5134 PIELIGHT colour;
5135
5136 colour.byte.r = FILLRED;
5137 colour.byte.b = FILLBLUE;
5138 colour.byte.g = FILLGREEN;
5139 colour.byte.a = FILLTRANS;
5140 pie_UniTransBoxFill(x, y, x + psWidget->width() + psWidget->height(), y + psWidget->height(), colour);
5141 }
5142 }
5143
mpwidgetGetFrontHighlightImage(Image image)5144 Image mpwidgetGetFrontHighlightImage(Image image)
5145 {
5146 if (image.isNull())
5147 {
5148 return Image();
5149 }
5150 switch (image.width())
5151 {
5152 case 30: return Image(FrontImages, IMAGE_HI34);
5153 case 60: return Image(FrontImages, IMAGE_HI64);
5154 case 19: return Image(FrontImages, IMAGE_HI23);
5155 case 27: return Image(FrontImages, IMAGE_HI31);
5156 case 35: return Image(FrontImages, IMAGE_HI39);
5157 case 37: return Image(FrontImages, IMAGE_HI41);
5158 case 56: return Image(FrontImages, IMAGE_HI56);
5159 }
5160 return Image();
5161 }
5162
display(int xOffset,int yOffset)5163 void WzMultiButton::display(int xOffset, int yOffset)
5164 {
5165 int x0 = xOffset + x();
5166 int y0 = yOffset + y();
5167 Image hiToUse(nullptr, 0);
5168
5169 // FIXME: This seems to be a way to conserve space, so you can use a
5170 // transparent icon with these edit boxes.
5171 // hack for multieditbox
5172 if (imNormal.id == IMAGE_EDIT_MAP || imNormal.id == IMAGE_EDIT_GAME || imNormal.id == IMAGE_EDIT_PLAYER
5173 || imNormal.id == IMAGE_LOCK_BLUE || imNormal.id == IMAGE_UNLOCK_BLUE)
5174 {
5175 drawBlueBox(x0 - 2, y0 - 2, height(), height()); // box on end.
5176 }
5177
5178 // evaluate auto-frame
5179 bool highlight = (getState() & WBUT_HIGHLIGHT) != 0;
5180
5181 // evaluate auto-frame
5182 if (doHighlight == 1 && highlight)
5183 {
5184 hiToUse = mpwidgetGetFrontHighlightImage(imNormal);
5185 }
5186
5187 bool down = (getState() & (WBUT_DOWN | WBUT_LOCK | WBUT_CLICKLOCK)) != 0;
5188 bool grey = (getState() & WBUT_DISABLE) != 0;
5189
5190 Image toDraw[3];
5191 int numToDraw = 0;
5192
5193 // now display
5194 toDraw[numToDraw++] = imNormal;
5195
5196 // hilights etc..
5197 if (down)
5198 {
5199 toDraw[numToDraw++] = imDown;
5200 }
5201 if (highlight && !grey && hiToUse.images != nullptr)
5202 {
5203 toDraw[numToDraw++] = hiToUse;
5204 }
5205
5206 for (int n = 0; n < numToDraw; ++n)
5207 {
5208 Image tcImage(toDraw[n].images, toDraw[n].id + 1);
5209 if (tc == MAX_PLAYERS)
5210 {
5211 iV_DrawImage(toDraw[n], x0, y0);
5212 }
5213 else if (tc == MAX_PLAYERS + 1)
5214 {
5215 const int scale = 4000;
5216 int f = realTime % scale;
5217 PIELIGHT mix;
5218 mix.byte.r = 128 + iSinR(65536 * f / scale + 65536 * 0 / 3, 127);
5219 mix.byte.g = 128 + iSinR(65536 * f / scale + 65536 * 1 / 3, 127);
5220 mix.byte.b = 128 + iSinR(65536 * f / scale + 65536 * 2 / 3, 127);
5221 mix.byte.a = 255;
5222 iV_DrawImageTc(toDraw[n], tcImage, x0, y0, mix);
5223 }
5224 else
5225 {
5226 iV_DrawImageTc(toDraw[n], tcImage, x0, y0, pal_GetTeamColour(tc));
5227 }
5228 }
5229
5230 if (grey)
5231 {
5232 // disabled, render something over it!
5233 iV_TransBoxFill(x0, y0, x0 + width(), y0 + height());
5234 }
5235 }
5236
5237 /////////////////////////////////////////////////////////////////////////////////////////
5238 /////////////////////////////////////////////////////////////////////////////////////////
5239 // common widgets
5240
addMultiEditBox(UDWORD formid,UDWORD id,UDWORD x,UDWORD y,char const * tip,char const * tipres,UDWORD icon,UDWORD iconhi,UDWORD iconid)5241 static bool addMultiEditBox(UDWORD formid, UDWORD id, UDWORD x, UDWORD y, char const *tip, char const *tipres, UDWORD icon, UDWORD iconhi, UDWORD iconid)
5242 {
5243 W_EDBINIT sEdInit; // editbox
5244 sEdInit.formID = formid;
5245 sEdInit.id = id;
5246 sEdInit.x = (short)x;
5247 sEdInit.y = (short)y;
5248 sEdInit.width = MULTIOP_EDITBOXW;
5249 sEdInit.height = MULTIOP_EDITBOXH;
5250 sEdInit.pText = tipres;
5251 sEdInit.pBoxDisplay = displayMultiEditBox;
5252 if (!widgAddEditBox(psWScreen, &sEdInit))
5253 {
5254 return false;
5255 }
5256
5257 addMultiBut(psWScreen, MULTIOP_OPTIONS, iconid, x + MULTIOP_EDITBOXW + 2, y + 2, MULTIOP_EDITBOXH, MULTIOP_EDITBOXH, tip, icon, iconhi, iconhi);
5258 return true;
5259 }
5260
5261 /////////////////////////////////////////////////////////////////////////////////////////
addMultiBut(WIDGET & parent,UDWORD id,UDWORD x,UDWORD y,UDWORD width,UDWORD height,const char * tipres,UDWORD norm,UDWORD down,UDWORD hi,unsigned tc)5262 std::shared_ptr<W_BUTTON> addMultiBut(WIDGET &parent, UDWORD id, UDWORD x, UDWORD y, UDWORD width, UDWORD height, const char *tipres, UDWORD norm, UDWORD down, UDWORD hi, unsigned tc)
5263 {
5264 std::shared_ptr<WzMultiButton> button;
5265 auto existingWidget = widgFormGetFromID(parent.shared_from_this(), id);
5266 if (existingWidget)
5267 {
5268 button = std::dynamic_pointer_cast<WzMultiButton>(existingWidget);
5269 }
5270 if (button == nullptr)
5271 {
5272 button = std::make_shared<WzMultiButton>();
5273 parent.attach(button);
5274 button->id = id;
5275 }
5276 button->setGeometry(x, y, width, height);
5277 button->setTip((tipres != nullptr) ? std::string(tipres) : std::string());
5278 button->imNormal = Image(FrontImages, norm);
5279 button->imDown = Image(FrontImages, down);
5280 button->doHighlight = hi;
5281 button->tc = tc;
5282 return button;
5283 }
5284
addMultiBut(const std::shared_ptr<W_SCREEN> & screen,UDWORD formid,UDWORD id,UDWORD x,UDWORD y,UDWORD width,UDWORD height,const char * tipres,UDWORD norm,UDWORD down,UDWORD hi,unsigned tc)5285 std::shared_ptr<W_BUTTON> addMultiBut(const std::shared_ptr<W_SCREEN> &screen, UDWORD formid, UDWORD id, UDWORD x, UDWORD y, UDWORD width, UDWORD height, const char *tipres, UDWORD norm, UDWORD down, UDWORD hi, unsigned tc)
5286 {
5287 return addMultiBut(*widgGetFromID(screen, formid), id, x, y, width, height, tipres, norm, down, hi, tc);
5288 }
5289
5290 /* Returns true if the multiplayer game can start (i.e. all players are ready) */
multiplayPlayersReady()5291 static bool multiplayPlayersReady()
5292 {
5293 bool bReady = true;
5294 size_t numReadyPlayers = 0;
5295
5296 for (unsigned int player = 0; player < game.maxPlayers; player++)
5297 {
5298 // check if this human player is ready, ignore AIs
5299 if (NetPlay.players[player].allocated)
5300 {
5301 if ((!NetPlay.players[player].ready || ingame.PingTimes[player] >= PING_LIMIT))
5302 {
5303 bReady = false;
5304 }
5305 else
5306 {
5307 numReadyPlayers++;
5308 }
5309 }
5310 else if (NetPlay.players[player].ai >= 0)
5311 {
5312 numReadyPlayers++;
5313 }
5314 }
5315
5316 return bReady && numReadyPlayers > 0;
5317 }
5318
multiplayIsStartingGame()5319 static bool multiplayIsStartingGame()
5320 {
5321 return bInActualHostedLobby && multiplayPlayersReady();
5322 }
5323
sendRoomSystemMessage(char const * text)5324 void sendRoomSystemMessage(char const *text)
5325 {
5326 NetworkTextMessage message(SYSTEM_MESSAGE, text);
5327 displayRoomSystemMessage(text);
5328 message.enqueue(NETbroadcastQueue());
5329 }
5330
sendRoomChatMessage(char const * text)5331 static void sendRoomChatMessage(char const *text)
5332 {
5333 NetworkTextMessage message(selectedPlayer, text);
5334 displayRoomMessage(RoomMessage::player(selectedPlayer, text));
5335 message.enqueue(NETbroadcastQueue());
5336 }
5337