1#include "playermapping.h"
2
3#include <base/net.h>
4
5#include <engine/shared/config.h>
6
7#include <game/server/entities/character.h>
8#include <game/server/gamecontext.h>
9#include <game/server/gamecontroller.h>
10#include <game/server/player.h>
11
12void CPlayerMapping::Init(CGameContext *pGameServer)
13{
14 m_pGameServer = pGameServer;
15 m_pConfig = m_pGameServer->Config();
16 m_pServer = m_pGameServer->Server();
17 std::fill(first: std::begin(arr&: m_aTeamSizes), last: std::end(arr&: m_aTeamSizes), value: 0);
18
19 for(int i = 0; i < MAX_CLIENTS; i++)
20 m_aMap[i].Init(ClientId: i, pPlayerMapping: this);
21}
22
23void CPlayerMapping::Tick()
24{
25 UpdatePlayerMap(ClientId: -1);
26
27 // Translate StrongWeakId to clamp it to 64 players
28 bool NeedsLegacyMapping = false;
29 for(int i = 0; i < MAX_CLIENTS; i++)
30 {
31 if(GameServer()->m_apPlayers[i] && GameServer()->GetClientVersion(ClientId: i) < VERSION_DDNET_128_PLAYERS)
32 {
33 NeedsLegacyMapping = true;
34 break;
35 }
36 }
37 if(!NeedsLegacyMapping)
38 return; // or continue past this block — nothing to do on modern-only servers
39
40 // Walk the character list ONCE per tick, not once per legacy client
41 int aCharacterIds[MAX_CLIENTS];
42 int NumCharacters = 0;
43 for(CCharacter *pChar = (CCharacter *)GameServer()->m_World.FindFirst(Type: CGameWorld::ENTTYPE_CHARACTER); pChar; pChar = (CCharacter *)pChar->TypeNext())
44 {
45 aCharacterIds[NumCharacters++] = pChar->GetPlayer()->GetCid();
46 }
47
48 for(int i = 0; i < MAX_CLIENTS; i++)
49 {
50 CPlayer *pPlayer = GameServer()->m_apPlayers[i];
51 if(!pPlayer || GameServer()->GetClientVersion(ClientId: i) >= VERSION_DDNET_128_PLAYERS)
52 continue;
53
54 int StrongWeakId = 0;
55 for(int c = 0; c < NumCharacters; c++)
56 {
57 int Id = aCharacterIds[c];
58 if(Server()->Translate(Target&: Id, ClientId: i))
59 pPlayer->m_aStrongWeakId[Id] = StrongWeakId++;
60 }
61 }
62}
63
64void CPlayerMapping::CPlayerMap::Init(int ClientId, CPlayerMapping *pPlayerMapping)
65{
66 m_ClientId = ClientId;
67 m_pPlayerMapping = pPlayerMapping;
68 m_pMap = m_pPlayerMapping->Server()->GetIdMap(ClientId: m_ClientId);
69 m_pReverseMap = m_pPlayerMapping->Server()->GetReverseIdMap(ClientId: m_ClientId);
70 m_ResortReserved = false;
71 std::fill(first: std::begin(arr&: m_aReserved), last: std::end(arr&: m_aReserved), value: false);
72 m_NumPages = 0;
73 m_TotalOverhang = 0;
74 m_NumReserved = 0;
75 m_DoSeeOthersByVote = false;
76 ResetSeeOthers();
77}
78
79CPlayer *CPlayerMapping::CPlayerMap::Player() const
80{
81 return m_pPlayerMapping->GameServer()->m_apPlayers[m_ClientId];
82}
83
84void CPlayerMapping::CPlayerMap::InitPlayer(bool Timeout)
85{
86 std::fill(first: std::begin(arr&: m_aReserved), last: std::end(arr&: m_aReserved), value: false);
87
88 int NextFreeId = 0;
89 const NETADDR *pOwnAddr = m_pPlayerMapping->Server()->ClientAddr(ClientId: m_ClientId);
90 for(bool Finished = false; !Finished;)
91 {
92 Finished = true;
93 for(int i = 0; i < MAX_CLIENTS; i++)
94 {
95 if(!m_pPlayerMapping->GameServer()->m_apPlayers[i])
96 continue;
97
98 const NETADDR *pAddr = m_pPlayerMapping->Server()->ClientAddr(ClientId: i);
99 if(net_addr_comp_noport(a: pOwnAddr, b: pAddr) == 0)
100 {
101 // For 0.7 timeout: Rejoin has to check ourselves because it's the id of the old connection that we want to skip
102 // Do not access our own reverse map on initial initialization, as it's only initialized below
103 if((i != m_ClientId || Timeout) && m_pPlayerMapping->m_aMap[i].m_pReverseMap[i] == NextFreeId)
104 {
105 NextFreeId++;
106 Finished = false;
107 }
108 }
109 }
110 }
111
112 // make sure no outdated data is stored, so we can start and insert new values
113 // after a timeout remove all players from the previous map in correct order (important for 0.7 net msgs...)
114 if(Timeout)
115 {
116 m_UpdateTeamsState = true; // to get back all teams
117 for(int i = 0; i < LEGACY_MAX_CLIENTS; i++)
118 Remove(MapId: i);
119 }
120
121 // Clear map, for 0.7 timeouts do this after we got our id back
122 for(int i = 0; i < LEGACY_MAX_CLIENTS; i++)
123 m_pMap[i] = -1;
124 for(int i = 0; i < MAX_CLIENTS; i++)
125 m_pReverseMap[i] = -1;
126
127 m_NumReserved = 2;
128 m_pMap[LEGACY_MAX_CLIENTS - 1] = -1; // player with empty name to say chat msgs
129 m_pMap[m_pPlayerMapping->SeeOthersId()] = -1; // see others in spec menu
130 m_TotalOverhang = 0;
131
132 if(m_pPlayerMapping->Server()->IsSixup(ClientId: m_ClientId))
133 {
134 protocol7::CNetMsg_Sv_ClientInfo FakeInfo;
135 FakeInfo.m_ClientId = LEGACY_MAX_CLIENTS - 1;
136 FakeInfo.m_Local = 0;
137 FakeInfo.m_Team = TEAM_BLUE;
138 FakeInfo.m_pName = " ";
139 FakeInfo.m_pClan = "";
140 FakeInfo.m_Country = -1;
141 FakeInfo.m_Silent = 1;
142 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
143 {
144 FakeInfo.m_apSkinPartNames[p] = "standard";
145 FakeInfo.m_aUseCustomColors[p] = 0;
146 FakeInfo.m_aSkinPartColors[p] = 0;
147 }
148 m_pPlayerMapping->Server()->SendPackMsg(pMsg: &FakeInfo, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD | MSGFLAG_NOTRANSLATE, ClientId: m_ClientId);
149 // see others
150 UpdateSeeOthers();
151 }
152
153 // Breaks with more than 64 tees from the same ip
154 if(NextFreeId < MapSize())
155 {
156 m_aReserved[m_ClientId] = true;
157 Add(MapId: NextFreeId, ClientId: m_ClientId);
158 }
159
160 for(int i = 0; i < MAX_CLIENTS; i++)
161 {
162 if(!m_pPlayerMapping->GameServer()->m_apPlayers[i] || i == m_ClientId)
163 continue;
164
165 const NETADDR *pAddr = m_pPlayerMapping->Server()->ClientAddr(ClientId: i);
166 if(net_addr_comp_noport(a: pOwnAddr, b: pAddr) != 0)
167 continue;
168
169 // update us with other same ip player infos
170 if(m_pPlayerMapping->m_aMap[i].m_pReverseMap[i] < MapSize())
171 {
172 m_aReserved[i] = true;
173 Add(MapId: m_pPlayerMapping->m_aMap[i].m_pReverseMap[i], ClientId: i);
174 }
175
176 // update other same ip players with our info
177 if(NextFreeId < m_pPlayerMapping->m_aMap[i].MapSize())
178 {
179 m_pPlayerMapping->m_aMap[i].m_aReserved[m_ClientId] = true;
180 m_pPlayerMapping->m_aMap[i].Add(MapId: NextFreeId, ClientId: m_ClientId);
181 }
182 }
183}
184
185void CPlayerMapping::CPlayerMap::Add(int MapId, int ClientId)
186{
187 dbg_assert(Player(), "invalid player map insertion: player does not exist");
188 if(MapId == -1 || ClientId == -1 || m_pReverseMap[ClientId] == MapId)
189 return;
190
191 Remove(MapId: m_pReverseMap[ClientId]);
192
193 int OldClientId = Remove(MapId);
194 if((OldClientId == -1 && m_pPlayerMapping->GameServer()->GetDDRaceTeam(ClientId) > 0) || (OldClientId != -1 && m_pPlayerMapping->GameServer()->GetDDRaceTeam(ClientId: OldClientId) != m_pPlayerMapping->GameServer()->GetDDRaceTeam(ClientId)))
195 m_UpdateTeamsState = true;
196
197 if(m_aReserved[ClientId])
198 m_ResortReserved = true;
199
200 m_pMap[MapId] = ClientId;
201 m_pReverseMap[ClientId] = MapId;
202 Player()->SendConnect(FakeId: MapId, ClientId);
203}
204
205int CPlayerMapping::CPlayerMap::Remove(int MapId)
206{
207 dbg_assert(Player(), "invalid player map removal: player does not exist");
208 if(MapId == -1)
209 return -1;
210
211 int ClientId = m_pMap[MapId];
212 if(ClientId != -1)
213 {
214 if(m_pPlayerMapping->GameServer()->GetDDRaceTeam(ClientId) > 0)
215 m_UpdateTeamsState = true;
216
217 if(m_aReserved[ClientId])
218 m_ResortReserved = true;
219
220 Player()->SendDisconnect(FakeId: MapId);
221 m_pReverseMap[ClientId] = -1;
222 m_pMap[MapId] = -1;
223 }
224 return ClientId;
225}
226
227void CPlayerMapping::CPlayerMap::Update()
228{
229 if(!m_pPlayerMapping->Server()->ClientIngame(ClientId: m_ClientId) || !Player())
230 return;
231 if(m_pPlayerMapping->GameServer()->GetClientVersion(ClientId: m_ClientId) >= VERSION_DDNET_128_PLAYERS)
232 return;
233
234 if(m_DoSeeOthersByVote)
235 {
236 CCharacter *pChr = m_pPlayerMapping->GameServer()->GetPlayerChar(ClientId: m_ClientId);
237 if(pChr && !pChr->IsIdle())
238 {
239 ResetSeeOthers();
240 m_DoSeeOthersByVote = false;
241 }
242 }
243
244 bool ResortReserved = m_ResortReserved;
245 m_ResortReserved = false;
246
247 for(int i = 0; i < MAX_CLIENTS; i++)
248 {
249 if(i == m_ClientId)
250 continue;
251
252 CPlayer *pPlayer = m_pPlayerMapping->GameServer()->m_apPlayers[i];
253
254 if(!m_pPlayerMapping->Server()->ClientIngame(ClientId: i) || !pPlayer)
255 {
256 Remove(MapId: m_pReverseMap[i]);
257 m_aReserved[i] = false;
258 continue;
259 }
260
261 // If a team (not 0) has more than 10 players, do not reserve their slots because it can get messy quickly if a few huge teams form.
262 // To keep teams state the same on main and dummy big teams do not get highlighted at all.
263 int DDTeam = m_pPlayerMapping->GameServer()->GetDDRaceTeam(ClientId: i);
264 bool ReserveTeamSlots = m_pPlayerMapping->ReserveTeamSlots(DDTeam);
265
266 if(m_aReserved[i])
267 {
268 const NETADDR *pOwnAddr = m_pPlayerMapping->Server()->ClientAddr(ClientId: m_ClientId);
269 const NETADDR *pAddr = m_pPlayerMapping->Server()->ClientAddr(ClientId: i);
270 if(net_addr_comp_noport(a: pOwnAddr, b: pAddr) != 0)
271 {
272 if(ResortReserved || !ReserveTeamSlots) // condition to unset reserved slot
273 {
274 m_aReserved[i] = false;
275
276 // reset our team to 0 when we are in a big team for example
277 if(DDTeam != TEAM_FLOCK)
278 m_UpdateTeamsState = true;
279 }
280 }
281 continue;
282 }
283 else if(ResortReserved)
284 continue;
285
286 int Insert = -1;
287 if(DDTeam != TEAM_FLOCK && ReserveTeamSlots)
288 {
289 for(int j = 0; j < MapSize() - m_NumSeeOthers; j++)
290 {
291 int CId = m_pMap[j];
292 if(CId == -1 || !m_aReserved[CId])
293 {
294 Insert = j;
295 m_aReserved[i] = true;
296 break;
297 }
298 }
299 }
300 else if(m_pReverseMap[i] != -1)
301 {
302 Insert = m_pReverseMap[i];
303 }
304 else
305 {
306 for(int j = 0; j < MapSize() - m_NumSeeOthers; j++)
307 if(m_pMap[j] == -1)
308 {
309 Insert = j;
310 break;
311 }
312 }
313
314 if(Insert != -1)
315 {
316 Add(MapId: Insert, ClientId: i);
317 }
318 else if(pPlayer->GetCharacter() && !pPlayer->GetCharacter()->NetworkClipped(SnappingClient: m_ClientId))
319 {
320 InsertNextEmpty(ClientId: i);
321 }
322 }
323
324 if(m_UpdateTeamsState)
325 {
326 m_pPlayerMapping->GameServer()->m_pController->Teams().SendTeamsState(ClientId: m_ClientId);
327 m_UpdateTeamsState = false;
328 }
329}
330
331void CPlayerMapping::CPlayerMap::InsertNextEmpty(int ClientId)
332{
333 if(ClientId == -1 || m_pReverseMap[ClientId] != -1)
334 return;
335
336 for(int i = 0; i < MapSize() - m_NumSeeOthers; i++)
337 {
338 int MappedClientId = m_pMap[i];
339 if(MappedClientId != -1 && m_aReserved[MappedClientId])
340 continue;
341
342 if(MappedClientId == -1 || (!m_pPlayerMapping->GameServer()->GetPlayerChar(ClientId: MappedClientId) || m_pPlayerMapping->GameServer()->GetPlayerChar(ClientId: MappedClientId)->NetworkClipped(SnappingClient: m_ClientId)))
343 {
344 Add(MapId: i, ClientId);
345 break;
346 }
347 }
348}
349
350bool CPlayerMapping::ReserveTeamSlots(int DDTeam) const
351{
352 return !g_Config.m_SvSoloServer && DDTeam != TEAM_FLOCK && m_aTeamSizes[DDTeam] <= ms_MaxTeamSizePlayerMap;
353}
354
355int CPlayerMapping::SeeOthersId() const
356{
357 return LEGACY_MAX_CLIENTS - 2;
358}
359
360bool CPlayerMapping::DoSeeOthers(int ClientId, int SelectedId, bool DoByVote)
361{
362 if(GameServer()->GetClientVersion(ClientId) >= VERSION_DDNET_128_PLAYERS)
363 return false;
364 if(SelectedId == SeeOthersId())
365 {
366 if(DoByVote)
367 {
368 m_aMap[ClientId].m_DoSeeOthersByVote = true;
369 }
370 m_aMap[ClientId].DoSeeOthers();
371 return true;
372 }
373 return false;
374}
375
376void CPlayerMapping::ResetSeeOthers(int ClientId)
377{
378 m_aMap[ClientId].ResetSeeOthers();
379}
380
381int CPlayerMapping::TotalOverhang(int ClientId) const
382{
383 return m_aMap[ClientId].m_TotalOverhang;
384}
385
386void CPlayerMapping::UpdatePlayerMap(int ClientId)
387{
388 if(ClientId == -1)
389 {
390 bool Update = Server()->Tick() % Config()->m_SvMapUpdateRate == 0;
391 int ClientCount = Server()->ClientCount();
392
393 if(Update)
394 {
395 // Cache team sizes to avoid more loops
396 std::fill(first: std::begin(arr&: m_aTeamSizes), last: std::end(arr&: m_aTeamSizes), value: 0);
397 for(int i = 0; i < MAX_CLIENTS; i++)
398 {
399 CPlayer *pPlayer = GameServer()->m_apPlayers[i];
400 if(!pPlayer)
401 continue;
402 int DDTeam = GameServer()->GetDDRaceTeam(ClientId: i);
403 m_aTeamSizes[DDTeam]++;
404 }
405 }
406
407 for(auto &Map : m_aMap)
408 {
409 if(!Map.Player())
410 continue;
411
412 // Calculate overhang every tick, not only when the map updates
413 int Overhang = std::max(a: 0, b: ClientCount - Map.MapSize());
414 if(Overhang != Map.m_TotalOverhang)
415 {
416 Map.m_TotalOverhang = Overhang;
417 Map.m_NumPages = std::max(a: 1, b: (Overhang + ms_MaxNumSeeOthers - 1) / ms_MaxNumSeeOthers);
418 if(Map.m_TotalOverhang <= 0 && Map.m_SeeOthersPage != -1)
419 Map.ResetSeeOthers();
420
421 Map.UpdateSeeOthers();
422 Map.m_UpdateTeamsState = true;
423 }
424
425 if(Update)
426 {
427 Map.Update();
428 }
429 }
430 }
431 else
432 {
433 m_aMap[ClientId].Update();
434 }
435}
436
437CPlayerMapping::ESeeOthersInd CPlayerMapping::SeeOthersInd(int ClientId, int MapId) const
438{
439 if(m_aMap[ClientId].m_TotalOverhang && MapId == SeeOthersId())
440 return ESeeOthersInd::BUTTON;
441 if(m_aMap[ClientId].m_NumSeeOthers && MapId >= m_aMap[ClientId].MapSize() - m_aMap[ClientId].m_NumSeeOthers && MapId < m_aMap[ClientId].MapSize())
442 return ESeeOthersInd::PLAYER;
443 return ESeeOthersInd::NONE;
444}
445
446const char *CPlayerMapping::SeeOthersName(int ClientId)
447{
448 int Page = m_aMap[ClientId].m_SeeOthersPage + 1;
449 if(m_aMap[ClientId].m_NumPages > 1 && Page == m_aMap[ClientId].m_NumPages)
450 {
451 str_format(buffer: m_aSeeOthersName, buffer_size: sizeof(m_aSeeOthersName), format: "⋅ %d/%d | Close", Page, Page);
452 }
453 else if(m_aMap[ClientId].m_SeeOthersPage != -1)
454 {
455 if(m_aMap[ClientId].m_TotalOverhang > ms_MaxNumSeeOthers)
456 str_format(buffer: m_aSeeOthersName, buffer_size: sizeof(m_aSeeOthersName), format: "⋅ %d/%d", Page, m_aMap[ClientId].m_NumPages);
457 else
458 str_copy(dst&: m_aSeeOthersName, src: "⋅ Close");
459 }
460 else
461 {
462 str_format(buffer: m_aSeeOthersName, buffer_size: sizeof(m_aSeeOthersName), format: "⋅ %d others", m_aMap[ClientId].m_TotalOverhang);
463 }
464 return m_aSeeOthersName;
465}
466
467void CPlayerMapping::CPlayerMap::CycleSeeOthers()
468{
469 if(m_TotalOverhang <= 0)
470 return;
471
472 for(int i = 0; i < LEGACY_MAX_CLIENTS; i++)
473 if(m_pMap[i] != -1)
474 m_aWasSeeOthers[m_pMap[i]] = true;
475
476 int Size = std::min(a: m_TotalOverhang, b: ms_MaxNumSeeOthers);
477 int Added = 0;
478 int MapId = MapSize() - 1;
479 for(int i = 0; i < MAX_CLIENTS; i++)
480 {
481 if(!m_pPlayerMapping->GameServer()->m_apPlayers[i] || m_aWasSeeOthers[i])
482 continue;
483
484 Add(MapId, ClientId: i);
485 m_aWasSeeOthers[i] = true;
486 Added++;
487 MapId--;
488
489 if(Added >= Size)
490 break;
491 }
492
493 m_NumSeeOthers = Added;
494}
495
496void CPlayerMapping::CPlayerMap::DoSeeOthers()
497{
498 if(m_TotalOverhang <= 0)
499 return;
500
501 // -1 (none) to 1 then 2, or even more if ms_MaxNumSeeOthers is lowered (currently 34 so there are only two pages at most)
502 m_SeeOthersPage++;
503 UpdateSeeOthers();
504
505 CycleSeeOthers();
506
507 // aggressively trigger reset now
508 if(m_NumSeeOthers == 0)
509 {
510 // Reset these for the next cycle so we can get the fresh page we had before
511 for(bool &WasSeeOthers : m_aWasSeeOthers)
512 WasSeeOthers = false;
513 CycleSeeOthers();
514 ResetSeeOthers();
515 }
516
517 // instantly update so we dont have to wait for the map to be executed
518 m_UpdateTeamsState = true;
519 Update();
520}
521
522void CPlayerMapping::CPlayerMap::ResetSeeOthers()
523{
524 m_SeeOthersPage = -1;
525 m_NumSeeOthers = 0;
526 for(bool &WasSeeOthers : m_aWasSeeOthers)
527 WasSeeOthers = false;
528 m_UpdateTeamsState = true;
529 UpdateSeeOthers();
530}
531
532void CPlayerMapping::CPlayerMap::UpdateSeeOthers() const
533{
534 if(!m_pPlayerMapping->Server()->IsSixup(ClientId: m_ClientId))
535 return;
536
537 int SeeOthersId = m_pPlayerMapping->SeeOthersId();
538 protocol7::CNetMsg_Sv_ClientDrop ClientDropMsg;
539 ClientDropMsg.m_ClientId = SeeOthersId;
540 ClientDropMsg.m_pReason = "";
541 ClientDropMsg.m_Silent = 1;
542
543 protocol7::CNetMsg_Sv_ClientInfo NewClientInfoMsg;
544 NewClientInfoMsg.m_ClientId = SeeOthersId;
545 NewClientInfoMsg.m_Local = 0;
546 NewClientInfoMsg.m_Team = TEAM_BLUE;
547 NewClientInfoMsg.m_pName = m_pPlayerMapping->SeeOthersName(ClientId: m_ClientId);
548 NewClientInfoMsg.m_pClan = "";
549 NewClientInfoMsg.m_Country = -1;
550 NewClientInfoMsg.m_Silent = 1;
551 for(int p = 0; p < protocol7::NUM_SKINPARTS; p++)
552 {
553 bool Colored = p == protocol7::SKINPART_BODY || p == protocol7::SKINPART_FEET;
554 NewClientInfoMsg.m_apSkinPartNames[p] = "standard";
555 NewClientInfoMsg.m_aUseCustomColors[p] = (int)Colored;
556 NewClientInfoMsg.m_aSkinPartColors[p] = Colored ? 5963600 : 0;
557 }
558
559 m_pPlayerMapping->Server()->SendPackMsg(pMsg: &ClientDropMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD | MSGFLAG_NOTRANSLATE, ClientId: m_ClientId);
560 m_pPlayerMapping->Server()->SendPackMsg(pMsg: &NewClientInfoMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD | MSGFLAG_NOTRANSLATE, ClientId: m_ClientId);
561}
562