forked from Mirrors/openclonk
391 lines
15 KiB
C++
391 lines
15 KiB
C++
/*
|
|
* OpenClonk, http://www.openclonk.org
|
|
*
|
|
* Copyright (c) 2007-2009, RedWolf Design GmbH, http://www.clonk.de/
|
|
* Copyright (c) 2010-2016, The OpenClonk Team and contributors
|
|
*
|
|
* Distributed under the terms of the ISC license; see accompanying file
|
|
* "COPYING" for details.
|
|
*
|
|
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
|
|
* See accompanying file "TRADEMARK" for details.
|
|
*
|
|
* To redistribute this file separately, substitute the full license texts
|
|
* for the above references.
|
|
*/
|
|
// player info attribute conflict resolving
|
|
// e.g., changing colors if two players have the same
|
|
// "There must be some easier way to do it"(tm)
|
|
|
|
#include "C4Include.h"
|
|
#include "control/C4PlayerInfo.h"
|
|
#include "game/C4Game.h"
|
|
#include "control/C4Teams.h"
|
|
#include "lib/StdColors.h"
|
|
#include "lib/C4Random.h"
|
|
|
|
// number of times trying new player colors
|
|
const int32_t C4MaxPlayerColorChangeTries = 100;
|
|
const int32_t C4MaxPlayerNameChangeTries = 100;
|
|
|
|
// *** Helpers
|
|
|
|
DWORD GenerateRandomPlayerColor(int32_t iTry) // generate a random player color for the iTry'th try
|
|
{
|
|
// generate a random one biased towards max channel luminance
|
|
// (for greater color difference and less gray-ish colors)
|
|
return C4RGB(std::min<int>(SafeRandom(302), 256), std::min<int>(SafeRandom(302), 256), std::min<int>(SafeRandom(302), 256));
|
|
}
|
|
|
|
bool IsColorConflict(DWORD dwClr1, DWORD dwClr2) // return whether dwClr1 and dwClr2 are closely together
|
|
{
|
|
// NEW COLOR CONFLICT METHOD: u'v'-distance
|
|
int R1 = 0xff & (dwClr1 >> 16);
|
|
int G1 = 0xff & (dwClr1 >> 8);
|
|
int B1 = 0xff & (dwClr1 );
|
|
int R2 = 0xff & (dwClr2 >> 16);
|
|
int G2 = 0xff & (dwClr2 >> 8);
|
|
int B2 = 0xff & (dwClr2 );
|
|
double r1=0,g1=0,b1=0,r2=0,g2=0,b2=0,x1=0,y1=0,Y1=0,x2=0,y2=0,Y2=0,u1=0,v1=0,u2=0,v2=0;
|
|
RGB2rgb(R1, G1, B1, &r1, &g1, &b1);
|
|
RGB2rgb(R2, G2, B2, &r2, &g2, &b2);
|
|
rgb2xyY(r1, g1, b1, &x1, &y1, &Y1);
|
|
rgb2xyY(r2, g2, b2, &x2, &y2, &Y2);
|
|
xy2upvp(x1, y1, &u1, &v1);
|
|
xy2upvp(x2, y2, &u2, &v2);
|
|
double Y = (Y1+Y2)/2.0;
|
|
double clrdiff = sqrt((u2-u1)*(u2-u1) + (v2-v1)*(v2-v1)) * Y*Y * 150;
|
|
double lumdiff = (Abs<double>(Y2-Y1) / std::max<double>(Y*Y*5, 0.5)) / 0.10;
|
|
return clrdiff + lumdiff < 1.0;
|
|
}
|
|
|
|
|
|
// conflict resolver
|
|
class C4PlayerInfoListAttributeConflictResolver
|
|
{
|
|
|
|
private:
|
|
// info packets that need to be checked yet
|
|
// info packets sorted by check priority, i.e. last ones checked first
|
|
C4ClientPlayerInfos **ppCheckInfos;
|
|
int32_t iCheckInfoCount;
|
|
|
|
// lists to be checked against
|
|
const C4PlayerInfoList &rPriCheckList, &rSecCheckList;
|
|
C4ClientPlayerInfos *pSecPacket;
|
|
|
|
// current attribute being checked
|
|
C4PlayerInfo::Attribute eAttr;
|
|
|
|
// handling of current packet to be adjusted
|
|
C4ClientPlayerInfos *pResolvePacket;
|
|
bool fAnyChange;
|
|
|
|
// handling of current info to be adjusted
|
|
C4PlayerInfo *pResolveInfo;
|
|
bool fCurrentConflict, fOriginalConflict, fAlternateConflict;
|
|
C4ClientPlayerInfos *pLowPrioOriginalConflictPacket, *pLowPrioAlternateConflictPacket;
|
|
|
|
public:
|
|
C4PlayerInfoListAttributeConflictResolver(C4PlayerInfoList &rPriCheckList, const C4PlayerInfoList &rSecCheckList, C4ClientPlayerInfos *pSecPacket);
|
|
~C4PlayerInfoListAttributeConflictResolver();
|
|
|
|
private:
|
|
void ReaddInfoForCheck(C4ClientPlayerInfos *pCheckAdd); // temp readd info to be checked, because another packet reset its attribute with higher priority
|
|
int32_t GetAttributePriorityDifference(const C4PlayerInfo *pInfo1, const C4ClientPlayerInfos *pPck1, const C4PlayerInfo *pInfo2, const C4ClientPlayerInfos *pPck2);
|
|
bool IsAttributeConflict(const C4PlayerInfo *pInfo1, const C4PlayerInfo *pInfo2, C4PlayerInfo::AttributeLevel eLevel);
|
|
void MarkConflicts(C4ClientPlayerInfos &rCheckPacket, bool fTestOriginal);
|
|
void MarkConflicts(const C4PlayerInfoList &rCheckList, bool fTestOriginal);
|
|
void ResolveInInfo();
|
|
void ResolveInPacket();
|
|
|
|
public:
|
|
void Resolve(); // action go!
|
|
};
|
|
|
|
// resolve function calling resolver
|
|
void C4PlayerInfoList::ResolvePlayerAttributeConflicts(C4ClientPlayerInfos *pSecPacket)
|
|
{
|
|
C4PlayerInfoListAttributeConflictResolver r(*this, Game.RestorePlayerInfos, pSecPacket);
|
|
r.Resolve();
|
|
}
|
|
|
|
// implementation of conflict resolver
|
|
C4PlayerInfoListAttributeConflictResolver::C4PlayerInfoListAttributeConflictResolver(C4PlayerInfoList &rPriCheckList, const C4PlayerInfoList &rSecCheckList, C4ClientPlayerInfos *pSecPacket)
|
|
: ppCheckInfos(NULL), iCheckInfoCount(0), rPriCheckList(rPriCheckList), rSecCheckList(rSecCheckList), pSecPacket(pSecPacket)
|
|
{
|
|
// prepare check array
|
|
int32_t iMaxCheckCount = rPriCheckList.GetInfoCount() + !!pSecPacket;
|
|
if (!iMaxCheckCount) return;
|
|
ppCheckInfos = new C4ClientPlayerInfos *[iMaxCheckCount];
|
|
// resolve all primary packets in reverse order, so late clients have lower priority
|
|
for (int32_t i=0; i<rPriCheckList.GetInfoCount(); ++i)
|
|
{
|
|
C4ClientPlayerInfos *pInfos = rPriCheckList.GetIndexedInfo(i);
|
|
if (pInfos != pSecPacket) // skip packet if it was explicitly passed for update
|
|
ppCheckInfos[iCheckInfoCount++] = pInfos;
|
|
else
|
|
{
|
|
// if the additional packet is in the list already, it needn't be a check packed (only resolve packet)
|
|
this->pSecPacket = NULL;
|
|
}
|
|
}
|
|
// must check sec packet first
|
|
if (pSecPacket) ppCheckInfos[iCheckInfoCount++] = pSecPacket;
|
|
}
|
|
|
|
C4PlayerInfoListAttributeConflictResolver::~C4PlayerInfoListAttributeConflictResolver()
|
|
{
|
|
delete [] ppCheckInfos;
|
|
}
|
|
|
|
void C4PlayerInfoListAttributeConflictResolver::ReaddInfoForCheck(C4ClientPlayerInfos *pCheckAdd)
|
|
{
|
|
// do not add twice<
|
|
for (int32_t i=0; i<iCheckInfoCount; ++i) if (ppCheckInfos[i] == pCheckAdd) return;
|
|
// readd it - must have been in there before, so array is large enough
|
|
// and it must have been at the head of the list
|
|
ppCheckInfos[iCheckInfoCount++] = pCheckAdd;
|
|
}
|
|
|
|
int32_t C4PlayerInfoListAttributeConflictResolver::GetAttributePriorityDifference(const C4PlayerInfo *pInfo1, const C4ClientPlayerInfos *pPck1, const C4PlayerInfo *pInfo2, const C4ClientPlayerInfos *pPck2)
|
|
{
|
|
// return info1.prio - info2.prio
|
|
// highest priority if joined already and attributes may not be changed
|
|
if (pInfo1->IsJoined())
|
|
return pInfo2->IsJoined() ? 0 : +1;
|
|
if (pInfo2->IsJoined())
|
|
return -1;
|
|
// Computer players lower priority than user players
|
|
int32_t iTypeDiff = int(pInfo2->GetType()) - int(pInfo1->GetType());
|
|
if (iTypeDiff) return iTypeDiff;
|
|
// All unjoined: Priority by score
|
|
int32_t iScoreDiff = pInfo1->getLeagueScore() - pInfo2->getLeagueScore();
|
|
if (iScoreDiff) return iScoreDiff;
|
|
// No scores known. Developers higher than others
|
|
if (pPck1->IsDeveloperPacket())
|
|
return pPck2->IsDeveloperPacket() ? 0 : +1;
|
|
if (pPck2->IsDeveloperPacket())
|
|
return -1;
|
|
// equal priority
|
|
return 0;
|
|
}
|
|
|
|
bool C4PlayerInfoListAttributeConflictResolver::IsAttributeConflict(const C4PlayerInfo *pInfo1, const C4PlayerInfo *pInfo2, C4PlayerInfo::AttributeLevel eLevel)
|
|
{
|
|
// check for conflict of colors and names
|
|
if (eAttr == C4PlayerInfo::PLRATT_Color)
|
|
{
|
|
uint32_t dwClr1 = pInfo1->GetColor(), dwClr2 = 0;
|
|
switch (eLevel)
|
|
{
|
|
case C4PlayerInfo::PLRAL_Current: dwClr2 = pInfo2->GetColor(); break;
|
|
case C4PlayerInfo::PLRAL_Original: dwClr2 = pInfo2->GetOriginalColor(); break;
|
|
case C4PlayerInfo::PLRAL_Alternate: dwClr2 = pInfo2->GetAlternateColor(); break;
|
|
}
|
|
return IsColorConflict(dwClr1, dwClr2);
|
|
}
|
|
else if (eAttr == C4PlayerInfo::PLRATT_Name)
|
|
{
|
|
const char *szName1 = pInfo1->GetName(), *szName2 = "";
|
|
switch (eLevel)
|
|
{
|
|
case C4PlayerInfo::PLRAL_Current: szName2 = pInfo2->GetName(); break;
|
|
case C4PlayerInfo::PLRAL_Original: szName2 = pInfo2->GetOriginalName(); break;
|
|
default: return SEqualNoCase(szName1, szName2);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void C4PlayerInfoListAttributeConflictResolver::MarkConflicts(C4ClientPlayerInfos &rCheckPacket, bool fTestOriginal)
|
|
{
|
|
C4PlayerInfo *pCheckAgainstInfo;
|
|
// check current and original attribute against all player infos
|
|
for (int32_t j=0; (pCheckAgainstInfo = rCheckPacket.GetPlayerInfo(j)); ++j)
|
|
{
|
|
if (pCheckAgainstInfo->IsUsingAttribute(eAttr)) if (!pResolveInfo->GetID() || pResolveInfo->GetID() != pCheckAgainstInfo->GetID()) if (pResolveInfo != pCheckAgainstInfo)
|
|
{
|
|
// current conflict is marked only if the checked packet has same of lower priority than the one compared to
|
|
// if the priority is higher, the attribute shall be changed in the other, low priority info instead!
|
|
bool fHasHigherPrio = (GetAttributePriorityDifference(pResolveInfo, pResolvePacket, pCheckAgainstInfo, &rCheckPacket) > 0);
|
|
if (!fHasHigherPrio)
|
|
if (IsAttributeConflict(pCheckAgainstInfo, pResolveInfo, C4PlayerInfo::PLRAL_Current))
|
|
fCurrentConflict = true;
|
|
if (fTestOriginal)
|
|
{
|
|
if (IsAttributeConflict(pCheckAgainstInfo, pResolveInfo, C4PlayerInfo::PLRAL_Original))
|
|
{
|
|
if (fHasHigherPrio && !fOriginalConflict && !pLowPrioOriginalConflictPacket)
|
|
{
|
|
// original attribute is taken by a low prio packet - do not mark an original conflict, but remember the packet
|
|
// that's blocking it
|
|
pLowPrioOriginalConflictPacket = &rCheckPacket;
|
|
}
|
|
else
|
|
{
|
|
// original attribute is taken by either one higher/equal priority by packet, or by two low prio packets
|
|
// in this case, don't revert to original
|
|
pLowPrioOriginalConflictPacket = NULL;
|
|
fOriginalConflict = true;
|
|
}
|
|
}
|
|
if (IsAttributeConflict(pCheckAgainstInfo, pResolveInfo, C4PlayerInfo::PLRAL_Alternate))
|
|
{
|
|
if (fHasHigherPrio && !fAlternateConflict && !pLowPrioAlternateConflictPacket)
|
|
pLowPrioAlternateConflictPacket = &rCheckPacket;
|
|
else
|
|
fAlternateConflict = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void C4PlayerInfoListAttributeConflictResolver::MarkConflicts(const C4PlayerInfoList &rCheckList, bool fTestOriginal)
|
|
{
|
|
// mark in all infos of given list...
|
|
for (int32_t i=0; i<rCheckList.GetInfoCount(); ++i)
|
|
MarkConflicts(*rCheckList.GetIndexedInfo(i), fTestOriginal);
|
|
}
|
|
|
|
void C4PlayerInfoListAttributeConflictResolver::ResolveInInfo()
|
|
{
|
|
// trial-loop for assignment of new player colors/names
|
|
int32_t iTries = 0;
|
|
// original/alternate conflict evaluated once only
|
|
fOriginalConflict = false;
|
|
fAlternateConflict = (eAttr == C4PlayerInfo::PLRATT_Name) || !pResolveInfo->GetAlternateColor(); // mark as conflict if there is no alternate color/name
|
|
for (;;)
|
|
{
|
|
// check against all other player infos, and given info, too (may be redundant)
|
|
fCurrentConflict = false;
|
|
pLowPrioOriginalConflictPacket = pLowPrioAlternateConflictPacket = NULL;
|
|
MarkConflicts(rPriCheckList, !iTries);
|
|
// check secondary list, too. But only for colors, not for names, because secondary list is Restore list
|
|
// and colors are retained in restore while names are always taken from new joins
|
|
if (eAttr != C4PlayerInfo::PLRATT_Name) MarkConflicts(rSecCheckList, !iTries);
|
|
// and mark conflicts in additional packet that' sbeen passed
|
|
if (pSecPacket) MarkConflicts(*pSecPacket, !iTries);
|
|
// color conflict resolving
|
|
if (eAttr == C4PlayerInfo::PLRATT_Color)
|
|
{
|
|
// original color free but not used?
|
|
if (!iTries)
|
|
{
|
|
if (pResolveInfo->GetColor() != pResolveInfo->GetOriginalColor())
|
|
{
|
|
if (!fOriginalConflict)
|
|
{
|
|
// revert to original color!
|
|
pResolveInfo->SetColor(pResolveInfo->GetOriginalColor());
|
|
// in case a lower priority packet was blocking the attribute, re-check that packet
|
|
// note that the may readd the current resolve packet, but the conflict will occur with the
|
|
// lower priority packet in the next loop
|
|
if (pLowPrioOriginalConflictPacket) ReaddInfoForCheck(pLowPrioOriginalConflictPacket);
|
|
// done with this player (breaking the trial-loop)
|
|
break;
|
|
}
|
|
// neither original nor alternate color used but alternate color free?
|
|
else if (pResolveInfo->GetColor() != pResolveInfo->GetAlternateColor() && !fAlternateConflict)
|
|
{
|
|
// revert to alternate
|
|
pResolveInfo->SetColor(pResolveInfo->GetAlternateColor());
|
|
if (pLowPrioAlternateConflictPacket) ReaddInfoForCheck(pLowPrioAlternateConflictPacket);
|
|
// done with this player (breaking the trial-loop)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// conflict found?
|
|
if (!fCurrentConflict)
|
|
// done with this player, then - break the trial-loop
|
|
break;
|
|
// try to get a new, unused player color
|
|
uint32_t dwNewClr;
|
|
if (++iTries > C4MaxPlayerColorChangeTries)
|
|
{
|
|
LogF(LoadResStr("IDS_PRC_NOREPLPLRCLR"), pResolveInfo->GetName() ? pResolveInfo->GetName() : "<NONAME>");
|
|
// since there's a conflict anyway, change to original
|
|
pResolveInfo->SetColor(pResolveInfo->GetOriginalColor());
|
|
break;
|
|
}
|
|
else
|
|
dwNewClr = GenerateRandomPlayerColor(iTries);
|
|
pResolveInfo->SetColor(dwNewClr);
|
|
}
|
|
else // if (eAttr == PLRATT_Name)
|
|
{
|
|
// name conflict resolving
|
|
// original name free but not used?
|
|
if (!SEqualNoCase(pResolveInfo->GetName(), pResolveInfo->GetOriginalName()))
|
|
if (!fOriginalConflict)
|
|
{
|
|
// revert to original name!
|
|
pResolveInfo->SetForcedName(NULL);
|
|
if (pLowPrioOriginalConflictPacket) ReaddInfoForCheck(pLowPrioOriginalConflictPacket);
|
|
// done with this player (breaking the trial-loop)
|
|
break;
|
|
}
|
|
// conflict found?
|
|
if (!fCurrentConflict)
|
|
// done with this player, then - break the trial-loop
|
|
break;
|
|
// generate new name by appending an index
|
|
if (++iTries > C4MaxPlayerNameChangeTries) break;
|
|
pResolveInfo->SetForcedName(FormatString("%s (%d)", pResolveInfo->GetOriginalName(), iTries+1).getData());
|
|
}
|
|
}
|
|
}
|
|
|
|
void C4PlayerInfoListAttributeConflictResolver::ResolveInPacket()
|
|
{
|
|
// check all player infos
|
|
fAnyChange = false;
|
|
int32_t iCheck = 0;
|
|
while ((pResolveInfo = pResolvePacket->GetPlayerInfo(iCheck++)))
|
|
{
|
|
// not already joined? Joined player must not change their attributes!
|
|
if (pResolveInfo->HasJoined()) continue;
|
|
DWORD dwPrevColor = pResolveInfo->GetColor();
|
|
StdStrBuf sPrevForcedName; sPrevForcedName.Copy(pResolveInfo->GetForcedName());
|
|
// check attributes: Name and color
|
|
for (eAttr = C4PlayerInfo::PLRATT_Color; eAttr != C4PlayerInfo::PLRATT_Last; eAttr = (C4PlayerInfo::Attribute) (eAttr+1))
|
|
{
|
|
if (eAttr == C4PlayerInfo::PLRATT_Color)
|
|
{
|
|
// no color change in savegame associations
|
|
if (pResolveInfo->GetAssociatedSavegamePlayerID()) continue;
|
|
// or forced team colors
|
|
if (Game.Teams.IsTeamColors() && Game.Teams.GetTeamByID(pResolveInfo->GetTeam())) continue;
|
|
}
|
|
else if (eAttr == C4PlayerInfo::PLRATT_Name)
|
|
{
|
|
// no name change if a league name is used
|
|
if (pResolveInfo->getLeagueAccount() && *pResolveInfo->getLeagueAccount()) continue;
|
|
}
|
|
// not if attributes are otherwise fixed (e.g., for script players)
|
|
if (pResolveInfo->IsAttributesFixed()) continue;
|
|
// resolve in this info
|
|
ResolveInInfo();
|
|
}
|
|
// mark change for return value if anything was changed
|
|
if (pResolveInfo->GetColor() != dwPrevColor || (pResolveInfo->GetForcedName() != sPrevForcedName))
|
|
fAnyChange = true;
|
|
// next player info check
|
|
}
|
|
// mark update if anything was changed
|
|
if (fAnyChange) pResolvePacket->SetUpdated();
|
|
}
|
|
|
|
void C4PlayerInfoListAttributeConflictResolver::Resolve()
|
|
{
|
|
// resolve in all packets in list until list is empty
|
|
// resolving in reverse order, because highest priority first in the list
|
|
while (iCheckInfoCount)
|
|
{
|
|
pResolvePacket = ppCheckInfos[--iCheckInfoCount];
|
|
ResolveInPacket();
|
|
}
|
|
}
|