1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2005-2009, RedWolf Design GmbH, http://www.clonk.de/
5  * Copyright (c) 2009-2016, The OpenClonk Team and contributors
6  *
7  * Distributed under the terms of the ISC license; see accompanying file
8  * "COPYING" for details.
9  *
10  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11  * See accompanying file "TRADEMARK" for details.
12  *
13  * To redistribute this file separately, substitute the full license texts
14  * for the above references.
15  */
16 // player team management for teamwork melees
17 
18 #include "C4Include.h"
19 #include "control/C4Teams.h"
20 
21 #include "c4group/C4Components.h"
22 #include "control/C4GameControl.h"
23 #include "lib/C4Random.h"
24 #include "player/C4Player.h"
25 #include "player/C4PlayerList.h"
26 
27 // ---------------------------------------------------------------
28 // C4Team
29 
C4Team(const C4Team & rCopy)30 C4Team::C4Team(const C4Team &rCopy)
31 		: piPlayers(new int32_t[rCopy.GetPlayerCount()]),
32 		iPlayerCount(rCopy.GetPlayerCount()),
33 		iPlayerCapacity(rCopy.GetPlayerCount()),
34 		iID(rCopy.GetID()), iPlrStartIndex(rCopy.iPlrStartIndex), dwClr(rCopy.dwClr),
35 		sIconSpec(rCopy.GetIconSpec()), iMaxPlayer(rCopy.iMaxPlayer)
36 {
37 	// copy name
38 	SCopy(rCopy.GetName(), Name, C4MaxName);
39 	// copy players
40 	for (int32_t i = 0; i < iPlayerCount; i++)
41 		piPlayers[i] = rCopy.GetIndexedPlayer(i);
42 }
43 
Clear()44 void C4Team::Clear()
45 {
46 	delete [] piPlayers; piPlayers = nullptr;
47 	iPlayerCount = iPlayerCapacity = iMaxPlayer = 0;
48 	iID = 0; *Name=0;
49 	sIconSpec.Clear();
50 }
51 
AddPlayer(C4PlayerInfo & rInfo,bool fAdjustPlayer)52 void C4Team::AddPlayer(C4PlayerInfo &rInfo, bool fAdjustPlayer)
53 {
54 	// must not happen!
55 	assert(rInfo.GetID());
56 	if (!rInfo.GetID()) return;
57 	// add player; grow vector if necessary
58 	if (iPlayerCount >= iPlayerCapacity)
59 	{
60 		int32_t *piNewPlayers = new int32_t[iPlayerCapacity = (iPlayerCount+4)&~3];
61 		if (iPlayerCount) memcpy(piNewPlayers, piPlayers, iPlayerCount*sizeof(int32_t));
62 		delete [] piPlayers; piPlayers = piNewPlayers;
63 	}
64 	// store new player
65 	piPlayers[iPlayerCount++] = rInfo.GetID();
66 	if (!fAdjustPlayer) return;
67 	// set values in info
68 	rInfo.SetTeam(GetID());
69 	if (Game.Teams.IsTeamColors()) rInfo.SetColor(GetColor());
70 	// and in actual player, if it is joined already
71 	if (rInfo.IsJoined())
72 	{
73 		C4Player *pJoinedPlr = ::Players.GetByInfoID(rInfo.GetID());
74 		assert(pJoinedPlr || (rInfo.GetType() == C4PT_Script));
75 		if (pJoinedPlr)
76 		{
77 			pJoinedPlr->Team = GetID();
78 			if (Game.Teams.IsTeamColors()) pJoinedPlr->SetPlayerColor(GetColor());
79 		}
80 	}
81 }
82 
RemoveIndexedPlayer(int32_t iIndex)83 void C4Team::RemoveIndexedPlayer(int32_t iIndex)
84 {
85 	// safety
86 	assert(Inside<int32_t>(iIndex, 0, iPlayerCount-1));
87 	if (!Inside<int32_t>(iIndex, 0, iPlayerCount-1)) return;
88 	// move other players done
89 	for (int32_t i = iIndex+1; i < iPlayerCount; ++i)
90 		piPlayers[i-1] = piPlayers[i];
91 	--iPlayerCount;
92 }
93 
RemovePlayerByID(int32_t iID)94 void C4Team::RemovePlayerByID(int32_t iID)
95 {
96 	// get index
97 	int32_t i;
98 	for (i=0; i < iPlayerCount; ++i)
99 		if (piPlayers[i] == iID) break;
100 	if (i == iPlayerCount) { assert(false); return; } // ID not found
101 	// remove it
102 	RemoveIndexedPlayer(i);
103 }
104 
IsPlayerIDInTeam(int32_t iID)105 bool C4Team::IsPlayerIDInTeam(int32_t iID)
106 {
107 	int32_t i=iPlayerCount, *piPlr = piPlayers;
108 	while (i--) if (*piPlr++ == iID) return true;
109 	return false;
110 }
111 
GetFirstUnjoinedPlayerID() const112 int32_t C4Team::GetFirstUnjoinedPlayerID() const
113 {
114 	// search for a player that does not have the join-flag set
115 	int32_t i=iPlayerCount, idPlr, *piPlr = piPlayers;
116 	C4PlayerInfo *pInfo;
117 	while (i--)
118 		if ((pInfo = Game.PlayerInfos.GetPlayerInfoByID(idPlr = *piPlr++)))
119 			if (!pInfo->HasJoinIssued())
120 				return idPlr;
121 	// none found
122 	return 0;
123 }
124 
CompileFunc(StdCompiler * pComp)125 void C4Team::CompileFunc(StdCompiler *pComp)
126 {
127 	if (pComp->isDeserializer()) Clear();
128 	pComp->Value(mkNamingAdapt(iID,                  "id",             0));
129 	pComp->Value(mkNamingAdapt(mkStringAdaptMA(Name), "Name",          ""));
130 	pComp->Value(mkNamingAdapt(iPlrStartIndex,       "PlrStartIndex",  0));
131 	pComp->Value(mkNamingAdapt(iPlayerCount,         "PlayerCount", 0));
132 	if (pComp->isDeserializer()) { delete [] piPlayers; piPlayers = new int32_t [iPlayerCapacity = iPlayerCount]; ZeroMem(piPlayers, sizeof(*piPlayers) * iPlayerCount); }
133 	pComp->Value(mkNamingAdapt(mkArrayAdapt(piPlayers, iPlayerCount, -1), "Players"));
134 	pComp->Value(mkNamingAdapt(dwClr,                "Color",        0u));
135 	pComp->Value(mkNamingAdapt(mkParAdapt(sIconSpec, StdCompiler::RCT_All),
136 	                                                 "IconSpec",     StdCopyStrBuf()));
137 	pComp->Value(mkNamingAdapt(iMaxPlayer,           "MaxPlayer",    0));
138 }
139 
RecheckPlayers()140 void C4Team::RecheckPlayers()
141 {
142 	// check all players within the team
143 	for (int32_t i=0; i<iPlayerCount; ++i)
144 	{
145 		bool fIsValid = false; int32_t id; C4PlayerInfo *pInfo;
146 		if ((id = piPlayers[i]))
147 			if ((pInfo = Game.PlayerInfos.GetPlayerInfoByID(id)))
148 				if (pInfo->GetTeam() == GetID())
149 					if (pInfo->IsUsingTeam())
150 						fIsValid = true;
151 		// removal will decrease iPlayerCount, which will abort the loop earlier
152 		if (!fIsValid) RemoveIndexedPlayer(i--);
153 	}
154 	// now check for any new players in the team
155 	int32_t id = 0; C4PlayerInfo *pInfo;
156 	while ((pInfo = Game.PlayerInfos.GetNextPlayerInfoByID(id)))
157 	{
158 		id = pInfo->GetID();
159 		if (pInfo->GetTeam() == GetID())
160 			if (pInfo->IsUsingTeam())
161 				if (!IsPlayerIDInTeam(id))
162 					AddPlayer(*pInfo, false);
163 	}
164 }
165 
166 DWORD GenerateRandomPlayerColor(int32_t iTry); // C4PlayerInfo.cpp
167 bool IsColorConflict(DWORD dwClr1, DWORD dwClr2); // C4PlayerInfo.cpp
168 
RecheckColor(C4TeamList & rForList)169 void C4Team::RecheckColor(C4TeamList &rForList)
170 {
171 	// number of times trying new player colors
172 	const int32_t C4MaxTeamColorChangeTries = 100;
173 	if (!dwClr)
174 	{
175 		const int defTeamColorCount = 10;
176 		DWORD defTeamColorRGB[defTeamColorCount] = { 0xF40000, 0x00C800, 0xFCF41C, 0x2020FF, // red, green, yellow, blue,
177 		    0xC48444, 0xFFFFFF, 0x848484, 0xFF00EF, // brown, white, grey, pink,
178 		    0x00FFFF, 0x784830
179 		                                           }; // cyan, dk brown
180 		// no color assigned yet: Generate by team ID
181 		if (iID >=1 && iID <=defTeamColorCount)
182 		{
183 			// default colors
184 			dwClr = defTeamColorRGB[iID-1] | 0xff000000;
185 		}
186 		else
187 		{
188 			// find a new, unused color
189 			for (int32_t iTry=1; iTry<C4MaxTeamColorChangeTries; ++iTry)
190 			{
191 				dwClr = GenerateRandomPlayerColor(iTry);
192 				int32_t iIdx=0; C4Team *pTeam; bool fOK=true;
193 				while ((pTeam = rForList.GetTeamByIndex(iIdx++)))
194 					if (pTeam != this)
195 						if (IsColorConflict(pTeam->GetColor(), dwClr))
196 						{
197 							fOK=false;
198 							break;
199 						}
200 				// color is fine?
201 				if (fOK) return;
202 				// it's not; try next color
203 			}
204 			// Giving up: Use last generated color
205 		}
206 	}
207 }
208 
GetNameWithParticipants() const209 StdStrBuf C4Team::GetNameWithParticipants() const
210 {
211 	// compose team name like "Team 1 (boni, GhostBear, Clonko)"
212 	// or just "Team 1" for empty team
213 	StdStrBuf sTeamName;
214 	sTeamName.Copy(GetName());
215 	if (GetPlayerCount())
216 	{
217 		sTeamName.Append(" (");
218 		int32_t iTeamPlrCount=0;
219 		for (int32_t j=0; j<GetPlayerCount(); ++j)
220 		{
221 			int32_t iPlr = GetIndexedPlayer(j);
222 			C4PlayerInfo *pPlrInfo;
223 			if (iPlr) if  ((pPlrInfo = Game.PlayerInfos.GetPlayerInfoByID(iPlr)))
224 				{
225 					if (iTeamPlrCount++) sTeamName.Append(", ");
226 					sTeamName.Append(pPlrInfo->GetName());
227 				}
228 		}
229 		sTeamName.AppendChar(')');
230 	}
231 	return sTeamName;
232 }
233 
HasWon() const234 bool C4Team::HasWon() const
235 {
236 	// return true if any member player of the team has won
237 	bool fHasWon = false;
238 	for (int32_t i=0; i<iPlayerCount; ++i)
239 	{
240 		int32_t id; C4PlayerInfo *pInfo;
241 		if ((id = piPlayers[i]))
242 			if ((pInfo = Game.PlayerInfos.GetPlayerInfoByID(id)))
243 				if (pInfo->HasWon())
244 				{
245 					fHasWon = true;
246 					break;
247 				}
248 	}
249 	return fHasWon;
250 }
251 
252 // ---------------------------------------------------------------
253 // C4TeamList
254 
Clear()255 void C4TeamList::Clear()
256 {
257 	// del all teams
258 	ClearTeams();
259 	// del player team vector
260 	delete [] ppList; ppList = nullptr;
261 	iTeamCapacity = 0;
262 	fAllowHostilityChange = true;
263 	fAllowTeamSwitch = false;
264 	fCustom = false;
265 	fActive = true;
266 	fTeamColors = false;
267 	eTeamDist = TEAMDIST_Free;
268 	fAutoGenerateTeams = false;
269 	iMaxScriptPlayers = 0;
270 	sScriptPlayerNames.Clear();
271 }
272 
operator =(const C4TeamList & rCopy)273 C4TeamList &C4TeamList::operator =(const C4TeamList &rCopy)
274 {
275 	Clear();
276 	if ((iTeamCount = iTeamCapacity = rCopy.iTeamCount))
277 		ppList = new C4Team *[iTeamCapacity];
278 	for (int i = 0; i < iTeamCount; i++)
279 		ppList[i] = new C4Team(*rCopy.ppList[i]);
280 	iLastTeamID = rCopy.iLastTeamID;
281 	fAllowHostilityChange = rCopy.fAllowHostilityChange;
282 	fAllowTeamSwitch = rCopy.fAllowTeamSwitch;
283 	fCustom = rCopy.fCustom;
284 	fActive = rCopy.fActive;
285 	eTeamDist = rCopy.eTeamDist;
286 	fTeamColors = rCopy.fTeamColors;
287 	fAutoGenerateTeams = rCopy.fAutoGenerateTeams;
288 	sScriptPlayerNames = rCopy.sScriptPlayerNames;
289 	return *this;
290 }
291 
CanLocalChooseTeam() const292 bool C4TeamList::CanLocalChooseTeam() const
293 {
294 	// only if there are any teams
295 	if (!fActive) return false;
296 	// check by mode
297 	switch (eTeamDist)
298 	{
299 	case TEAMDIST_Free: return true;
300 	case TEAMDIST_Host: return ::Control.isCtrlHost();
301 	case TEAMDIST_None:
302 	case TEAMDIST_Random:
303 	case TEAMDIST_RandomInv:
304 		return false;
305 	default: assert(false); return false;
306 	}
307 }
308 
CanLocalChooseTeam(int32_t idPlayer) const309 bool C4TeamList::CanLocalChooseTeam(int32_t idPlayer) const
310 {
311 	// must be possible at all
312 	if (!CanLocalChooseTeam()) return false;
313 	// there must be space in a target team
314 	// always possible if teams are generated on the fly
315 	if (IsAutoGenerateTeams()) return true;
316 	// also possible if one of the teams that's not the player's is not full
317 	C4Team *pCurrentTeam = nullptr, *pCheck;
318 	if (idPlayer) pCurrentTeam = GetTeamByPlayerID(idPlayer);
319 	int32_t iCheckTeam=0;
320 	while ((pCheck = GetTeamByIndex(iCheckTeam++)))
321 		if (pCheck != pCurrentTeam)
322 			if (!pCheck->IsFull())
323 				break;
324 	return !!pCheck;
325 }
326 
CanLocalSeeTeam() const327 bool C4TeamList::CanLocalSeeTeam() const
328 {
329 	if (!fActive) return false;
330 	// invisible teams aren't revealed before game start
331 	if (eTeamDist != TEAMDIST_RandomInv) return true;
332 	return !!Game.IsRunning;
333 }
334 
AddTeam(C4Team * pNewTeam)335 void C4TeamList::AddTeam(C4Team *pNewTeam)
336 {
337 	// add team; grow vector if necessary
338 	if (iTeamCount >= iTeamCapacity)
339 	{
340 		// grow up to the nearest multiple of 4 elements
341 		// (TODO: Replace the whole thing e.g. with a simple std::vector<C4Team>)
342 		C4Team **ppNewTeams = new C4Team*[(iTeamCapacity = ((iTeamCount+4)&~3))];
343 		if (iTeamCount) memcpy(ppNewTeams, ppList, iTeamCount*sizeof(C4Team *));
344 		delete [] ppList; ppList = ppNewTeams;
345 	}
346 	// store new team
347 	ppList[iTeamCount++] = pNewTeam;
348 	// adjust ID
349 	iLastTeamID = std::max(pNewTeam->iID, iLastTeamID);
350 }
351 
ClearTeams()352 void C4TeamList::ClearTeams()
353 {
354 	// delete all teams
355 	C4Team **ppTeam=ppList;
356 	if (iTeamCount) { while (iTeamCount--) delete *(ppTeam++); iTeamCount = 0; }
357 	iLastTeamID = 0;
358 }
359 
CreateTeam(const char * szName)360 C4Team *C4TeamList::CreateTeam(const char *szName)
361 {
362 	// custom team
363 	C4Team *pNewTeam = new C4Team();
364 	pNewTeam->iID = iLastTeamID + 1;
365 	SCopy(szName, pNewTeam->Name, C4MaxName);
366 	AddTeam(pNewTeam);
367 	pNewTeam->RecheckColor(*this);
368 	return pNewTeam;
369 }
370 
GenerateDefaultTeams(int32_t iUpToID)371 bool C4TeamList::GenerateDefaultTeams(int32_t iUpToID)
372 {
373 	// generate until last team ID matches given
374 	while (iLastTeamID < iUpToID)
375 	{
376 		char TeamName[C4MaxName+1];
377 		sprintf(TeamName, LoadResStr("IDS_MSG_TEAM"), iLastTeamID+1);
378 		if (!CreateTeam(TeamName)) return false;
379 	}
380 	return true;
381 }
382 
GetTeamByID(int32_t iID) const383 C4Team *C4TeamList::GetTeamByID(int32_t iID) const
384 {
385 	C4Team **ppCheck=ppList; int32_t iCnt=iTeamCount;
386 	for (; iCnt--; ++ppCheck) if ((*ppCheck)->GetID() == iID) return *ppCheck;
387 	return nullptr;
388 }
389 
GetGenerateTeamByID(int32_t iID)390 C4Team *C4TeamList::GetGenerateTeamByID(int32_t iID)
391 {
392 	// only if enabled
393 	if (!IsMultiTeams()) return nullptr;
394 	// new team?
395 	if (iID == TEAMID_New) iID = GetLargestTeamID()+1;
396 	// find in list
397 	C4Team *pTeam = GetTeamByID(iID);
398 	if (pTeam) return pTeam;
399 	// not found: Generate
400 	GenerateDefaultTeams(iID);
401 	return GetTeamByID(iID);
402 }
403 
GetTeamByIndex(int32_t iIndex) const404 C4Team *C4TeamList::GetTeamByIndex(int32_t iIndex) const
405 {
406 	// safety
407 	if (!Inside<int32_t>(iIndex, 0, iTeamCount-1)) return nullptr;
408 	// direct list access
409 	return ppList[iIndex];
410 }
411 
GetTeamByName(const char * szName) const412 C4Team *C4TeamList::GetTeamByName(const char *szName) const
413 {
414 	assert(szName);
415 	C4Team **ppCheck=ppList; int32_t iCnt=iTeamCount;
416 	for (; iCnt--; ++ppCheck) if (SEqual((*ppCheck)->GetName(), szName)) return *ppCheck;
417 	return nullptr;
418 }
419 
GetTeamByPlayerID(int32_t iID) const420 C4Team *C4TeamList::GetTeamByPlayerID(int32_t iID) const
421 {
422 	C4Team **ppCheck=ppList; int32_t iCnt=iTeamCount;
423 	for (; iCnt--; ++ppCheck) if ((*ppCheck)->IsPlayerIDInTeam(iID)) return *ppCheck;
424 	return nullptr;
425 }
426 
GetLargestTeamID() const427 int32_t C4TeamList::GetLargestTeamID() const
428 {
429 	int32_t iLargest = 0;
430 	C4Team **ppCheck=ppList; int32_t iCnt=iTeamCount;
431 	for (; iCnt--; ++ppCheck) iLargest = std::max((*ppCheck)->GetID(), iLargest);
432 	return iLargest;
433 }
434 
GetRandomSmallestTeam() const435 C4Team *C4TeamList::GetRandomSmallestTeam() const
436 {
437 	C4Team *pLowestTeam = nullptr; int iLowestTeamCount = 0;
438 	C4Team **ppCheck=ppList; int32_t iCnt=iTeamCount;
439 	for (; iCnt--; ++ppCheck)
440 	{
441 		if ((*ppCheck)->IsFull()) continue; // do not join into full teams
442 		if (!pLowestTeam || pLowestTeam->GetPlayerCount() > (*ppCheck)->GetPlayerCount())
443 		{
444 			pLowestTeam = *ppCheck;
445 			iLowestTeamCount = 1;
446 		}
447 		else if (pLowestTeam->GetPlayerCount() == (*ppCheck)->GetPlayerCount())
448 			if (!UnsyncedRandom(++iLowestTeamCount))
449 				pLowestTeam = *ppCheck;
450 	}
451 	return pLowestTeam;
452 }
453 
IsTeamVisible() const454 bool C4TeamList::IsTeamVisible() const
455 {
456 	// teams invisible during lobby time if random surprise teams
457 	if (eTeamDist == TEAMDIST_RandomInv)
458 		if (::Network.isLobbyActive())
459 			return false;
460 	return true;
461 }
462 
RecheckPlayerInfoTeams(C4PlayerInfo & rNewJoin,bool fByHost)463 bool C4TeamList::RecheckPlayerInfoTeams(C4PlayerInfo &rNewJoin, bool fByHost)
464 {
465 	// only if enabled
466 	assert(IsMultiTeams());
467 	if (!IsMultiTeams()) return false;
468 	// check whether a new team is to be assigned first
469 	C4Team *pCurrentTeam = GetTeamByPlayerID(rNewJoin.GetID());
470 	int32_t idCurrentTeam = pCurrentTeam ? pCurrentTeam->GetID() : 0;
471 	if (rNewJoin.GetTeam())
472 	{
473 		// was that team a change to the current team?
474 		// no change anyway: OK, skip this info
475 		if (idCurrentTeam == rNewJoin.GetTeam()) return true;
476 		// the player had a different team assigned: Check if changes are allowed at all
477 		if (eTeamDist == TEAMDIST_Free || (eTeamDist == TEAMDIST_Host && fByHost))
478 			// also make sure that selecting this team is allowed, e.g. doesn't break the team limit
479 			// this also checks whether the team number is a valid team - but it would accept TEAMID_New, which shouldn't be used in player infos!
480 			if (rNewJoin.GetTeam() != TEAMID_New && IsJoin2TeamAllowed(rNewJoin.GetTeam(), rNewJoin.GetType()))
481 				// okay; accept change
482 				return true;
483 		// Reject change by reassigning the current team
484 		rNewJoin.SetTeam(idCurrentTeam);
485 		// and determine a new team, if none has been assigned yet
486 		if (idCurrentTeam) return true;
487 	}
488 	// new team assignment
489 	// teams are always needed in the lobby, so there's a team preset to change
490 	// for runtime joins, teams are needed if specified by teams.txt or if any teams have been created before (to avoid mixed team-noteam-scenarios)
491 	// but only assign teams in runtime join if the player won't pick it himself
492 	bool fWillHaveLobby = ::Network.isEnabled() && !::Network.Status.isPastLobby() && Game.fLobby;
493 	bool fHasOrWillHaveLobby = ::Network.isLobbyActive() || fWillHaveLobby;
494 	bool fCanPickTeamAtRuntime = !IsRandomTeam() && (rNewJoin.GetType() == C4PT_User) && IsRuntimeJoinTeamChoice();
495 	bool fIsTeamNeeded = IsRuntimeJoinTeamChoice() || GetTeamCount();
496 	if (!fHasOrWillHaveLobby && (!fIsTeamNeeded || fCanPickTeamAtRuntime)) return false;
497 	// get least-used team
498 	C4Team *pAssignTeam=nullptr;
499 	C4Team *pLowestTeam = GetRandomSmallestTeam();
500 	// melee mode
501 	if (IsAutoGenerateTeams() && !IsRandomTeam())
502 	{
503 		// reuse old team only if it's empty
504 		if (pLowestTeam && !pLowestTeam->GetPlayerCount())
505 			pAssignTeam = pLowestTeam;
506 		else
507 		{
508 			// no empty team: generate new
509 			GenerateDefaultTeams(iLastTeamID+1);
510 			pAssignTeam = GetTeamByID(iLastTeamID);
511 		}
512 	}
513 	else
514 	{
515 		if (!pLowestTeam)
516 		{
517 			// not enough teams defined in teamwork mode?
518 			// then create two teams as default
519 			if (!GetTeamByIndex(1))
520 				GenerateDefaultTeams(2);
521 			else
522 				// otherwise, all defined teams are full. This is a scenario error, because MaxPlayer should have been adjusted
523 				return false;
524 			pLowestTeam = GetTeamByIndex(0);
525 		}
526 		pAssignTeam = pLowestTeam;
527 	}
528 	// assign it
529 	if (!pAssignTeam) return false;
530 	pAssignTeam->AddPlayer(rNewJoin, true);
531 	return true;
532 }
533 
IsJoin2TeamAllowed(int32_t idTeam,C4PlayerType plrType)534 bool C4TeamList::IsJoin2TeamAllowed(int32_t idTeam, C4PlayerType plrType)
535 {
536 	// join to new team: Only if new teams can be created
537 	if (idTeam == TEAMID_New) return IsAutoGenerateTeams();
538 	// team number must be valid
539 	C4Team *pTeam = GetTeamByID(idTeam);
540 	if (!pTeam) return false;
541 	// team player count must not exceed the limit, unless it is a script player
542 	return !pTeam->IsFull() || plrType == C4PT_Script;
543 }
544 
CompileFunc(StdCompiler * pComp)545 void C4TeamList::CompileFunc(StdCompiler *pComp)
546 {
547 	// if (pComp->isDeserializer()) Clear(); - do not clear, because this would corrupt  the fCustom-flag
548 	pComp->Value(mkNamingAdapt(fActive,               "Active",               true));
549 	pComp->Value(mkNamingAdapt(fCustom,               "Custom",               true));
550 	pComp->Value(mkNamingAdapt(fAllowHostilityChange, "AllowHostilityChange", false));
551 	pComp->Value(mkNamingAdapt(fAllowTeamSwitch,      "AllowTeamSwitch",      false));
552 	pComp->Value(mkNamingAdapt(fAutoGenerateTeams,    "AutoGenerateTeams",    false));
553 	pComp->Value(mkNamingAdapt(iLastTeamID, "LastTeamID", 0));
554 
555 	StdEnumEntry<TeamDist> TeamDistEntries[] =
556 	{
557 		{ "Free", TEAMDIST_Free },
558 		{ "Host", TEAMDIST_Host },
559 		{ "None", TEAMDIST_None },
560 		{ "Random", TEAMDIST_Random },
561 		{ "RandomInv", TEAMDIST_RandomInv },
562 	};
563 	pComp->Value(mkNamingAdapt(mkEnumAdaptT<uint8_t>(eTeamDist, TeamDistEntries), "TeamDistribution", TEAMDIST_Free));
564 
565 	pComp->Value(mkNamingAdapt(fTeamColors, "TeamColors", false));
566 	pComp->Value(mkNamingAdapt(iMaxScriptPlayers, "MaxScriptPlayers", 0));
567 	pComp->Value(mkNamingAdapt(mkParAdapt(sScriptPlayerNames, StdCompiler::RCT_All), "ScriptPlayerNames", StdStrBuf()));
568 
569 	int32_t iOldTeamCount = iTeamCount;
570 	pComp->Value(mkNamingCountAdapt(iTeamCount,  "Team"));
571 
572 	if (pComp->isDeserializer())
573 	{
574 		while (iOldTeamCount--) delete ppList[iOldTeamCount];
575 		delete [] ppList;
576 		if ((iTeamCapacity = iTeamCount))
577 		{
578 			ppList = new C4Team *[iTeamCapacity];
579 			memset(ppList, 0, sizeof(C4Team *)*iTeamCapacity);
580 		}
581 		else
582 			ppList = nullptr;
583 	}
584 
585 	if (iTeamCount)
586 	{
587 		// Force compiler to spezialize
588 		mkPtrAdaptNoNull(*ppList);
589 		// Save team list, using map-function.
590 		pComp->Value(mkNamingAdapt(
591 		               mkArrayAdaptMap(ppList, iTeamCount, mkPtrAdaptNoNull<C4Team>),
592 		               "Team"));
593 	}
594 
595 	if (pComp->isDeserializer())
596 	{
597 		// adjust last team ID, which may not be set properly for player-generated team files
598 		iLastTeamID = std::max(GetLargestTeamID(), iLastTeamID);
599 		// force automatic generation of teams if none are defined
600 		if (!iTeamCount) fAutoGenerateTeams = true;
601 	}
602 }
603 
Load(C4Group & hGroup,class C4Scenario * pInitDefault,class C4LangStringTable * pLang)604 bool C4TeamList::Load(C4Group &hGroup, class C4Scenario *pInitDefault, class C4LangStringTable *pLang)
605 {
606 	// clear previous
607 	Clear();
608 	// load file contents
609 	StdStrBuf Buf;
610 	if (!hGroup.LoadEntryString(C4CFN_Teams, &Buf))
611 	{
612 		// no teams: Try default init
613 		if (!pInitDefault) return false;
614 		// no teams defined: Activate default melee teams if a melee rule is found
615 		// default: FFA for anything that looks like melee
616 		if ( pInitDefault->Game.IsMelee())
617 		{
618 			fAllowHostilityChange = true;
619 			fActive = true;
620 			fAutoGenerateTeams = true;
621 		}
622 		else
623 		{
624 			// No goals/rules whatsoever: They could be present in the objects.txt, but parsing that would be a bit of
625 			//  overkill
626 			// So just keep the old behaviour here, and disallow teams
627 			fAllowHostilityChange = true;
628 			fActive = false;
629 		}
630 		fCustom = false;
631 	}
632 	else
633 	{
634 		// team definition file may be localized
635 		if (pLang) pLang->ReplaceStrings(Buf);
636 		// compile
637 		if (!CompileFromBuf_LogWarn<StdCompilerINIRead>(mkNamingAdapt(*this, "Teams"), Buf, C4CFN_Teams)) return false;
638 	}
639 	// post-initialization: Generate default team colors
640 	int32_t iTeam=0; C4Team *pTeam;
641 	while ((pTeam = GetTeamByIndex(iTeam++)))
642 		pTeam->RecheckColor(*this);
643 	return true;
644 }
645 
Save(C4Group & hGroup)646 bool C4TeamList::Save(C4Group &hGroup)
647 {
648 	// remove previous entry from group
649 	hGroup.DeleteEntry(C4CFN_Teams);
650 	// decompile
651 	try
652 	{
653 		StdStrBuf Buf = DecompileToBuf<StdCompilerINIWrite>(mkNamingAdapt(*this, "Teams"));
654 		// save it
655 		hGroup.Add(C4CFN_Teams, Buf, false, true);
656 	}
657 	catch (StdCompiler::Exception *)
658 		{ return false; }
659 	// done, success
660 	return true;
661 }
662 
RecheckPlayers()663 void C4TeamList::RecheckPlayers()
664 {
665 	C4Team **ppCheck=ppList; int32_t iCnt=iTeamCount;
666 	for (; iCnt--; ++ppCheck) (*ppCheck)->RecheckPlayers();
667 }
668 
RecheckTeams()669 void C4TeamList::RecheckTeams()
670 {
671 	// automatic team distributions only
672 	if (!IsRandomTeam()) return;
673 	// host decides random teams
674 	if (!::Control.isCtrlHost()) return;
675 	// random teams in auto generate mode? Make sure there are exactly two teams
676 	if (IsAutoGenerateTeams() && GetTeamCount() != 2)
677 		{
678 		ReassignAllTeams();
679 		return;
680 		}
681 	// redistribute players of largest team that has relocatable players left towards smaller teams
682 	for (;;)
683 	{
684 		C4Team *pLowestTeam = GetRandomSmallestTeam();
685 		if (!pLowestTeam) break; // no teams: Nothing to re-distribute.
686 		// get largest team that has relocateable players
687 		C4Team *pLargestTeam = nullptr;
688 		C4Team **ppCheck=ppList; int32_t iCnt=iTeamCount;
689 		for (; iCnt--; ++ppCheck) if (!pLargestTeam || pLargestTeam->GetPlayerCount() > (*ppCheck)->GetPlayerCount())
690 				if ((*ppCheck)->GetFirstUnjoinedPlayerID())
691 					pLargestTeam = *ppCheck;
692 		// no team can redistribute?
693 		if (!pLargestTeam) break;
694 		// redistribution won't help much?
695 		if (pLargestTeam->GetPlayerCount() - pLowestTeam->GetPlayerCount() <= 1) break;
696 		// okay; redistribute one player!
697 		int32_t idRedistPlayer = pLargestTeam->GetFirstUnjoinedPlayerID();
698 		C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(idRedistPlayer);
699 		assert(pInfo);
700 		if (!pInfo) break; // umn...serious problems
701 		pLargestTeam->RemovePlayerByID(idRedistPlayer);
702 		pLowestTeam->AddPlayer(*pInfo, true);
703 		C4ClientPlayerInfos *pClrInfo = Game.PlayerInfos.GetClientInfoByPlayerID(idRedistPlayer);
704 		assert(pClrInfo);
705 		// player info change: mark updated to remote clients get information
706 		if (pClrInfo)
707 		{
708 			pClrInfo->SetUpdated();
709 		}
710 	}
711 }
712 
ReassignAllTeams()713 void C4TeamList::ReassignAllTeams()
714 {
715 	assert(::Control.isCtrlHost());
716 	if (!::Control.isCtrlHost()) return;
717 	// go through all player infos; reset team in them
718 	int32_t idStart = -1; C4PlayerInfo *pNfo;
719 	while ((pNfo = Game.PlayerInfos.GetNextPlayerInfoByID(idStart)))
720 	{
721 		idStart = pNfo->GetID();
722 		if (pNfo->HasJoinIssued()) continue;
723 		pNfo->SetTeam(0);
724 		// mark changed info as updated
725 		C4ClientPlayerInfos *pCltInfo = Game.PlayerInfos.GetClientInfoByPlayerID(idStart);
726 		assert(pCltInfo);
727 		if (pCltInfo)
728 		{
729 			pCltInfo->SetUpdated();
730 		}
731 	}
732 	// clear players from team lists
733 	RecheckPlayers();
734 	// in random autogenerate mode, there must be exactly two teams
735 	if (IsRandomTeam())
736 		if (IsAutoGenerateTeams() && GetTeamCount() != 2)
737 			{
738 			ClearTeams();
739 			GenerateDefaultTeams(2);
740 			}
741 	// reassign them
742 	idStart = -1;
743 	while ((pNfo = Game.PlayerInfos.GetNextPlayerInfoByID(idStart)))
744 	{
745 		idStart = pNfo->GetID();
746 		if (pNfo->HasJoinIssued()) continue;
747 		assert(!pNfo->GetTeam());
748 		RecheckPlayerInfoTeams(*pNfo, true);
749 	}
750 }
751 
GetTeamDistName(TeamDist eTeamDist) const752 StdStrBuf C4TeamList::GetTeamDistName(TeamDist eTeamDist) const
753 {
754 	switch (eTeamDist)
755 	{
756 	case TEAMDIST_Free:      return(StdStrBuf(LoadResStr("IDS_MSG_TEAMDIST_FREE"), true));
757 	case TEAMDIST_Host:      return(StdStrBuf(LoadResStr("IDS_MSG_TEAMDIST_HOST"), true));
758 	case TEAMDIST_None:      return(StdStrBuf(LoadResStr("IDS_MSG_TEAMDIST_NONE"), true));
759 	case TEAMDIST_Random:    return(StdStrBuf(LoadResStr("IDS_MSG_TEAMDIST_RND"), true));
760 	case TEAMDIST_RandomInv: return(StdStrBuf(LoadResStr("IDS_MSG_TEAMDIST_RNDINV"), true));
761 	default: return(FormatString("TEAMDIST_undefined(%d)", (int) eTeamDist));
762 	}
763 }
764 
FillTeamDistOptions(C4GUI::ComboBox_FillCB * pFiller) const765 void C4TeamList::FillTeamDistOptions(C4GUI::ComboBox_FillCB *pFiller) const
766 {
767 	// no teams if disabled
768 	if (!fActive) return;
769 	// team distribution options
770 	pFiller->AddEntry(GetTeamDistName(TEAMDIST_Free).getData(), TEAMDIST_Free);
771 	pFiller->AddEntry(GetTeamDistName(TEAMDIST_Host).getData(), TEAMDIST_Host);
772 	if (IsAutoGenerateTeams()) pFiller->AddEntry(GetTeamDistName(TEAMDIST_None).getData(), TEAMDIST_None); // no teams: only for regular melees
773 	pFiller->AddEntry(GetTeamDistName(TEAMDIST_Random).getData(), TEAMDIST_Random);
774 	pFiller->AddEntry(GetTeamDistName(TEAMDIST_RandomInv).getData(), TEAMDIST_RandomInv);
775 	}
776 
SendSetTeamDist(TeamDist eNewTeamDist)777 void C4TeamList::SendSetTeamDist(TeamDist eNewTeamDist)
778 {
779 	assert(::Control.isCtrlHost());
780 	// set it for all clients
781 	::Control.DoInput(CID_Set, new C4ControlSet(C4CVT_TeamDistribution, eNewTeamDist), CDT_Sync);
782 }
783 
GetTeamDistString() const784 StdStrBuf C4TeamList::GetTeamDistString() const
785 {
786 	// return name of current team distribution setting
787 	return GetTeamDistName(eTeamDist);
788 }
789 
HasTeamDistOptions() const790 bool C4TeamList::HasTeamDistOptions() const
791 {
792 	// team distribution can be changed if teams are enabled
793 	return fActive;
794 }
795 
SetTeamDistribution(TeamDist eToVal)796 void C4TeamList::SetTeamDistribution(TeamDist eToVal)
797 {
798 	if (!Inside(eToVal, TEAMDIST_First, TEAMDIST_Last)) { assert(false); return; }
799 	eTeamDist = eToVal;
800 	// team distribution mode changed: Host may beed to redistribute
801 	if (::Control.isCtrlHost())
802 	{
803 		// if a random team mode was set, reassign all teams so it's really random.
804 		// Also reassign in no-team-mode so enough teams for all players exist
805 		if (IsRandomTeam() || eTeamDist==TEAMDIST_None)
806 			ReassignAllTeams();
807 		else
808 		{
809 			// otherwise, it's sufficient to just reassign any teams that are incorrect for the current mode
810 			RecheckTeams();
811 		}
812 		// send updates to other clients and reset flags
813 		if (::Network.isEnabled())
814 		{
815 			::Network.Players.SendUpdatedPlayers();
816 		}
817 	}
818 }
819 
SendSetTeamColors(bool fEnabled)820 void C4TeamList::SendSetTeamColors(bool fEnabled)
821 {
822 	// set it for all clients
823 	::Control.DoInput(CID_Set, new C4ControlSet(C4CVT_TeamColors, fEnabled), CDT_Sync);
824 }
825 
SetTeamColors(bool fEnabled)826 void C4TeamList::SetTeamColors(bool fEnabled)
827 {
828 	// change only
829 	if (fEnabled == fTeamColors) return;
830 	// reflect change
831 	fTeamColors = fEnabled;
832 	// update colors of all players
833 	if (!::Control.isCtrlHost()) return;
834 	// go through all player infos; reset color in them
835 	Game.PlayerInfos.UpdatePlayerAttributes(); // sets team and savegame colors
836 	if (::Network.isEnabled())
837 	{
838 		// sends color updates to all clients
839 		::Network.Players.SendUpdatedPlayers();
840 	}
841 }
842 
EnforceLeagueRules()843 void C4TeamList::EnforceLeagueRules()
844 {
845 	// enforce some league settings
846 	// allow temp hostility switching; often used e.g. to unstick friendly Clonks, but:
847 	fAllowTeamSwitch = false; // switching teams in league games? Yeah, sure...
848 }
849 
GetForcedTeamSelection(int32_t idForPlayer) const850 int32_t C4TeamList::GetForcedTeamSelection(int32_t idForPlayer) const
851 {
852 	// if there's only one team for the player to join, return that team ID
853 	C4Team *pOKTeam = nullptr, *pCheck;
854 	if (idForPlayer) pOKTeam = GetTeamByPlayerID(idForPlayer); // curent team is always possible, even if full
855 	int32_t iCheckTeam=0;
856 	while ((pCheck = GetTeamByIndex(iCheckTeam++)))
857 		if (!pCheck->IsFull())
858 		{
859 			// this team could be joined
860 			if (pOKTeam && pOKTeam != pCheck)
861 			{
862 				// there already was a team that could be joined
863 				// two alternatives -> team selection is not forced
864 				return 0;
865 			}
866 			pOKTeam = pCheck;
867 		}
868 	// was there a team that could be joined?
869 	if (pOKTeam)
870 	{
871 		// if teams are generated on the fly, there would always be the possibility of creating a new team    }
872 		if (IsAutoGenerateTeams()) return 0;
873 		// otherwise, this team is forced!
874 		return pOKTeam->GetID();
875 	}
876 	// no team could be joined: Teams auto generated?
877 	if (IsAutoGenerateTeams())
878 	{
879 		// then the only possible way is to join a new team
880 		return TEAMID_New;
881 	}
882 	// otherwise, nothing can be done...
883 	return 0;
884 }
885 
GetScriptPlayerName() const886 StdStrBuf C4TeamList::GetScriptPlayerName() const
887 {
888 	// get a name to assign to a new script player. Try to avoid name conflicts
889 	if (!sScriptPlayerNames.getLength()) return StdStrBuf(LoadResStr("IDS_TEXT_COMPUTER")); // default name
890 	// test available script names
891 	int32_t iNameIdx = 0; StdStrBuf sOut;
892 	while (sScriptPlayerNames.GetSection(iNameIdx++, &sOut, '|'))
893 		if (!Game.PlayerInfos.GetActivePlayerInfoByName(sOut.getData()))
894 			return sOut;
895 	// none are available: Return a random name
896 	sScriptPlayerNames.GetSection(UnsyncedRandom(iNameIdx-1), &sOut, '|');
897 	return sOut;
898 }
899 
GetStartupTeamCount(int32_t startup_player_count)900 int32_t C4TeamList::GetStartupTeamCount(int32_t startup_player_count)
901 {
902 	// Count non-empty teams
903 	int32_t i_team = 0; C4Team *team;
904 	int32_t team_count = 0;
905 	while ((team = GetTeamByIndex(i_team++)))
906 	{
907 		if (team->GetPlayerCount() > 0) ++team_count;
908 	}
909 	// No populated teams found? This can happen in non-network mode when no players are assigned
910 	if (!team_count)
911 	{
912 		// Teams have not been selected yet, but the map script may want to have an estimate
913 		// in this case, calculate prospective teams from startup player count
914 		if (IsCustom() && !IsAutoGenerateTeams())
915 		{
916 			// Teams are pre-defined. Assume players will try to distribute broadly on these teams
917 			team_count = std::min<int32_t>(startup_player_count, GetTeamCount());
918 		}
919 		else if (IsRandomTeam())
920 		{
921 			// Randomized teams: Players will be put into two teams.
922 			team_count = std::min<int32_t>(startup_player_count, 2);
923 		}
924 		else
925 		{
926 			// Teams are auto-added -> fallback to player count
927 			team_count = startup_player_count;
928 		}
929 	}
930 
931 	return team_count;
932 }
933