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(&currentVote);
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>(&paragraphInit);
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