1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2004, Sven2
6 * Copyright (c) 2017-2022, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// NET2 network player management
19// see header for some additional information
20// 2do: Handle client joins after game go but before runtime join (Frame 0)?
21// Those will not receive a player info list right in time
22
23#include "C4Include.h"
24#include "C4Network2Players.h"
25#include "C4PlayerInfo.h"
26#include "C4GameLobby.h"
27#include "C4Game.h"
28#include "C4Control.h"
29#include <C4Log.h>
30
31// *** C4Network2Players
32
33C4Network2Players::C4Network2Players() : rInfoList(Game.Parameters.PlayerInfos)
34{
35 // ctor - init rInfoList-ref to only
36}
37
38void C4Network2Players::Init()
39{
40 // caution: In this call, only local players are joined
41 // remote players may have been added already for runtime joins
42 // not in replay
43 if (Game.C4S.Head.Replay) return;
44 // network only
45 assert(Game.Network.isEnabled());
46 // must init before game is running
47 assert(!Game.IsRunning);
48 // join the local player(s)
49 JoinLocalPlayer(szLocalPlayerFilename: Game.PlayerFilenames, fAdd: false);
50 // host: Rejoin script players from savegame
51 if (Game.Network.isHost())
52 {
53 rInfoList.CreateRestoreInfosForJoinedScriptPlayers(rSavegamePlayers&: Game.RestorePlayerInfos);
54 if (Game.RestartRestoreInfos.What & C4NetworkRestartInfos::ScriptPlayers)
55 {
56 C4ClientPlayerInfos joinInfo = *rInfoList.GetIndexedInfo(iIndex: 0);
57 for (const auto &[name, player] : Game.RestartRestoreInfos.Players)
58 {
59 if (player.type == C4PT_Script)
60 {
61 const auto playerInfo = new C4PlayerInfo;
62 playerInfo->SetAsScriptPlayer(szName: name.c_str(), dwColor: player.color, dwFlags: 0, idExtra: C4ID_None);
63 Game.Teams.GetGenerateTeamByID(iID: player.team);
64 playerInfo->SetTeam(player.team);
65 joinInfo.AddInfo(pAddInfo: playerInfo);
66 }
67 }
68 HandlePlayerInfoUpdRequest(pInfoPacket: &joinInfo, fByHost: true);
69 }
70 }
71}
72
73void C4Network2Players::Clear()
74{
75 // nothing...
76}
77
78bool C4Network2Players::JoinLocalPlayer(const char *szLocalPlayerFilename, bool fAdd)
79{
80 // ignore in replay
81 // shouldn't even come here though
82 assert(!Game.C4S.Head.Replay);
83 if (Game.C4S.Head.Replay) return false;
84 // if observing: don't try
85 if (Game.Clients.getLocal()->isObserver()) return false;
86 // network only
87 assert(Game.Network.isEnabled());
88 // create join info packet
89 C4ClientPlayerInfos JoinInfo(szLocalPlayerFilename, fAdd);
90 // league game: get authentication for players
91 if (Game.Parameters.isLeague())
92 for (int i = 0; i < JoinInfo.GetPlayerCount(); i++)
93 {
94 C4PlayerInfo *pInfo = JoinInfo.GetPlayerInfo(iIndex: i);
95 if (!Game.Network.LeaguePlrAuth(pInfo))
96 {
97 JoinInfo.RemoveIndexedInfo(iAtIndex: i);
98 i--;
99 }
100 }
101 // host or client?
102 if (Game.Network.isHost())
103 {
104 // error joining players? Zero players is OK for initial packet; marks host as observer
105 if (fAdd && !JoinInfo.GetPlayerCount()) return false;
106 // handle it as a direct request
107
108 if (Game.RestartRestoreInfos.What & C4NetworkRestartInfos::PlayerTeams)
109 {
110 C4PlayerInfo *info;
111 for (int i = 0; info = JoinInfo.GetPlayerInfo(iIndex: i); ++i)
112 {
113 if (const auto restoreInfo = Game.RestartRestoreInfos.Players.find(x: info->GetName()); restoreInfo != Game.RestartRestoreInfos.Players.end() && restoreInfo->second.team)
114 {
115 const auto team = restoreInfo->second.team;
116 Game.Teams.GetGenerateTeamByID(iID: team);
117 info->SetTeam(team);
118 }
119 }
120 }
121
122 HandlePlayerInfoUpdRequest(pInfoPacket: &JoinInfo, fByHost: true);
123 }
124 else
125 {
126 // clients request initial joins at host only
127 // create player info for local player joins
128 C4PacketPlayerInfoUpdRequest JoinRequest(JoinInfo);
129 // any players to join? Zero players is OK for initial packet; marks client as observer
130 // it's also necessary to send the empty player info packet, so the host will answer
131 // with infos of all other clients
132 if (fAdd && !JoinRequest.Info.GetPlayerCount()) return false;
133 Game.Network.Clients.SendMsgToHost(rPkt: MkC4NetIOPacket(cStatus: PID_PlayerInfoUpdReq, Pkt: JoinRequest));
134 // request activation
135 if (JoinRequest.Info.GetPlayerCount() && !Game.Clients.getLocal()->isActivated())
136 Game.Network.RequestActivate();
137 }
138 // done, success
139 return true;
140}
141
142void C4Network2Players::RequestPlayerInfoUpdate(const class C4ClientPlayerInfos &rRequest)
143{
144 // network only
145 assert(Game.Network.isEnabled());
146 // host or client?
147 if (Game.Network.isHost())
148 {
149 // host processes directly
150 HandlePlayerInfoUpdRequest(pInfoPacket: &rRequest, fByHost: true);
151 }
152 else
153 {
154 // client sends request to host
155 C4PacketPlayerInfoUpdRequest UpdateRequest(rRequest);
156 Game.Network.Clients.SendMsgToHost(rPkt: MkC4NetIOPacket(cStatus: PID_PlayerInfoUpdReq, Pkt: UpdateRequest));
157 }
158}
159
160void C4Network2Players::HandlePlayerInfoUpdRequest(const class C4ClientPlayerInfos *pInfoPacket, bool fByHost)
161{
162 // network host only
163 assert(Game.Network.isEnabled());
164 assert(Game.Network.isHost());
165 // copy client infos (need to be adjusted)
166 C4ClientPlayerInfos OwnInfoPacket(*pInfoPacket);
167 // safety: check any duplicate, unjoined players first
168 // check those with unassigned IDs only, so update packets won't be rejected by this
169 if (!OwnInfoPacket.IsInitialPacket())
170 {
171 C4ClientPlayerInfos *pExistingClientInfo = rInfoList.GetInfoByClientID(iClientID: OwnInfoPacket.GetClientID());
172 if (pExistingClientInfo)
173 {
174 int iCnt = OwnInfoPacket.GetPlayerCount(); C4PlayerInfo *pPlrInfo;
175 C4Network2Res *pRes;
176 while (iCnt--) if (pPlrInfo = OwnInfoPacket.GetPlayerInfo(iIndex: iCnt))
177 if (!pPlrInfo->GetID()) if (pRes = pPlrInfo->GetRes())
178 if (pExistingClientInfo->GetPlayerInfoByRes(idResID: pRes->getResID()))
179 {
180 // double join: simply deny without message
181 LogNTr(level: spdlog::level::debug, message: "Network: Duplicate player join rejected!");
182 OwnInfoPacket.RemoveIndexedInfo(iAtIndex: iCnt);
183 }
184 }
185 if (!OwnInfoPacket.GetPlayerCount())
186 {
187 // player join request without players: probably all removed because doubled
188 LogNTr(message: "Network: Empty player join request ignored!");
189 return;
190 }
191 }
192 // assign player IDs
193 if (!rInfoList.AssignPlayerIDs(pNewClientInfo: &OwnInfoPacket) && OwnInfoPacket.IsAddPacket())
194 {
195 // no players could be joined in an add request: probably because the maximum player limit has been reached
196 return;
197 }
198 // check doubled savegame player usage
199 UpdateSavegameAssignments(pNewInfo: &OwnInfoPacket);
200 // update teams
201 rInfoList.AssignTeams(pNewClientInfo: &OwnInfoPacket, fByHost);
202 // update any other player colors and names
203 // this may only change colors and names of all unjoined players (which is all players in lobby mode)
204 // any affected players will get an updated-flag
205 rInfoList.UpdatePlayerAttributes(pForInfo: &OwnInfoPacket, fResolveConflicts: true);
206 // league score gains may now be different
207 rInfoList.ResetLeagueProjectedGain(fSetUpdated: true);
208 int32_t iPlrInfo = 0;
209 C4PlayerInfo *pPlrInfo;
210 while (pPlrInfo = OwnInfoPacket.GetPlayerInfo(iIndex: iPlrInfo++)) pPlrInfo->ResetLeagueProjectedGain();
211 if (Game.Parameters.isLeague())
212 {
213 // lobby only
214 if (!Game.Network.isLobbyActive())
215 return;
216 // check league authentication for new players
217 for (int i = 0; i < OwnInfoPacket.GetPlayerCount(); i++)
218 if (!rInfoList.GetPlayerInfoByID(id: OwnInfoPacket.GetPlayerInfo(iIndex: i)->GetID()))
219 {
220 C4PlayerInfo *pInfo = OwnInfoPacket.GetPlayerInfo(iIndex: i);
221 // remove player infos without authentication
222 if (!Game.Network.LeaguePlrAuthCheck(pInfo))
223 {
224 OwnInfoPacket.RemoveIndexedInfo(iAtIndex: i);
225 i--;
226 }
227 else
228 // always reset authentication ID after check - it's not needed anymore
229 pInfo->SetAuthID("");
230 }
231 }
232 // send updates to all other clients and reset update flags
233 SendUpdatedPlayers();
234 // finally, add new player join as direct input
235 // this will add the player infos directly on host side (DirectExec as a subcall),
236 // so future player join request will take the other joined clients into consideration
237 // when assigning player colors, etc.; it will also start resource loading
238 // in running mode, this call will also put the actual player joins into the queue
239 Game.Control.DoInput(eCtrlType: CID_PlrInfo, pPkt: new C4ControlPlayerInfo(OwnInfoPacket), eDelivery: CDT_Direct);
240 // notify lobby of updates
241 C4GameLobby::MainDlg *pLobby = Game.Network.GetLobby();
242 if (pLobby) pLobby->OnPlayersChange();
243}
244
245void C4Network2Players::HandlePlayerInfo(const class C4ClientPlayerInfos &rInfoPacket, bool localOrigin)
246{
247 // network only
248 assert(Game.Network.isEnabled());
249 const auto updatedId = rInfoPacket.GetClientID();
250 // copy client player infos out of packet to be used in local list
251 C4ClientPlayerInfos *pClientInfo = new C4ClientPlayerInfos(rInfoPacket);
252 // add client info to local player info list - eventually deleting pClientInfo and
253 // returning a pointer to the new info structure when multiple player infos are merged
254 // may also replace existing info, if this is an update-call
255 pClientInfo = rInfoList.AddInfo(pNewClientInfo: pClientInfo);
256 // make sure team list reflects teams set in player infos
257 Game.Teams.RecheckPlayers();
258 Game.Teams.RecheckTeams(); // recheck random teams - if a player left, teams may need to be rebalanced
259 // make sure resources are loaded for those players
260 rInfoList.LoadResources();
261 // get associated client - note that pClientInfo might be nullptr for empty packets that got discarded
262 if (pClientInfo)
263 {
264 const C4Client *pClient = Game.Clients.getClientByID(iID: pClientInfo->GetClientID());
265 // host, game running and client active already?
266 if (Game.Network.isHost() && Game.Network.isRunning() && pClient && pClient->isActivated())
267 {
268 // then join the players immediately
269 JoinUnjoinedPlayersInControlQueue(pNewPacket: pClientInfo);
270 }
271 }
272 // adding the player may have invalidated other players (through team settings). Send them.
273 if (localOrigin)
274 {
275 SendUpdatedPlayers();
276 }
277 else
278 {
279 rInfoList.GetInfoByClientID(iClientID: updatedId)->ResetUpdated();
280 }
281 // lobby: update players
282 C4GameLobby::MainDlg *pLobby = Game.Network.GetLobby();
283 if (pLobby) pLobby->OnPlayersChange();
284 // invalidate reference
285 Game.Network.InvalidateReference();
286}
287
288void C4Network2Players::SendUpdatedPlayers()
289{
290 // check all clients for update
291 C4ClientPlayerInfos *pUpdInfo; int i = 0;
292 while (pUpdInfo = rInfoList.GetIndexedInfo(iIndex: i++))
293 if (pUpdInfo->IsUpdated())
294 {
295 pUpdInfo->ResetUpdated();
296 C4ControlPlayerInfo *pkSend = new C4ControlPlayerInfo(*pUpdInfo);
297 // send info to all
298 Game.Control.DoInput(eCtrlType: CID_PlrInfo, pPkt: pkSend, eDelivery: CDT_Direct);
299 }
300}
301
302void C4Network2Players::UpdateSavegameAssignments(C4ClientPlayerInfos *pNewInfo)
303{
304 // safety
305 if (!pNewInfo) return;
306 // check all joins of new info; backwards so they can be deleted
307 C4PlayerInfo *pInfo, *pInfo2, *pSaveInfo; int i = pNewInfo->GetPlayerCount(), j, id;
308 while (i--) if (pInfo = pNewInfo->GetPlayerInfo(iIndex: i))
309 if (id = pInfo->GetAssociatedSavegamePlayerID())
310 {
311 // check for non-existent savegame players
312 if (!(pSaveInfo = Game.RestorePlayerInfos.GetPlayerInfoByID(id)))
313 {
314 pInfo->SetAssociatedSavegamePlayer(id = 0);
315 pNewInfo->SetUpdated();
316 }
317 // check for duplicates (can't really occur...)
318 if (id)
319 {
320 j = i;
321 while (pInfo2 = pNewInfo->GetPlayerInfo(iIndex: ++j))
322 if (pInfo2->GetAssociatedSavegamePlayerID() == id)
323 {
324 // fix it by resetting the savegame info
325 pInfo->SetAssociatedSavegamePlayer(id = 0);
326 pNewInfo->SetUpdated(); break;
327 }
328 }
329 // check against all infos of other clients
330 C4ClientPlayerInfos *pkClientInfo; int k = 0;
331 while ((pkClientInfo = rInfoList.GetIndexedInfo(iIndex: k++)) && id)
332 {
333 // if it's not an add packet, don't check own client twice
334 if (pkClientInfo->GetClientID() == pNewInfo->GetClientID() && !(pNewInfo->IsAddPacket()))
335 continue;
336 // check against all players
337 j = 0;
338 while (pInfo2 = pkClientInfo->GetPlayerInfo(iIndex: j++))
339 if (pInfo2->GetAssociatedSavegamePlayerID() == id)
340 {
341 // fix it by resetting the savegame info
342 pInfo->SetAssociatedSavegamePlayer(id = 0);
343 pNewInfo->SetUpdated(); break;
344 }
345 }
346 // if the player joined just for the savegame assignment, and that failed, delete it
347 if (!id && pInfo->IsJoinForSavegameOnly())
348 pNewInfo->RemoveIndexedInfo(iAtIndex: i);
349 // prev info
350 }
351}
352
353void C4Network2Players::JoinUnjoinedPlayersInControlQueue(C4ClientPlayerInfos *pNewPacket)
354{
355 // only host may join any players to the queue
356 assert(Game.Network.isHost());
357 // check all players
358 int i = 0; C4PlayerInfo *pInfo;
359 while (pInfo = pNewPacket->GetPlayerInfo(iIndex: i++))
360 // not yet joined and no savegame assignment?
361 if (!pInfo->HasJoinIssued()) if (!pInfo->GetAssociatedSavegamePlayerID())
362 {
363 // join will be marked when queue is executed (C4Player::Join)
364 // but better mark join now already to prevent permanent sending overkill
365 pInfo->SetJoinIssued();
366 // do so!
367 C4Network2Res *pPlrRes = pInfo->GetRes();
368 C4Network2Client *pClient = Game.Network.Clients.GetClientByID(iID: pNewPacket->GetClientID());
369 if (!pPlrRes || (!pClient && pNewPacket->GetClientID() != Game.Control.ClientID()))
370 if (pInfo->GetType() != C4PT_Script)
371 {
372 // failure: Non-script players must have a res to join from!
373 const char *szPlrName = pInfo->GetName(); if (!szPlrName) szPlrName = "???";
374 LogNTr(level: spdlog::level::err, fmt: "Network: C4Network2Players::JoinUnjoinedPlayersInControlQueue failed to join player {}!", args&: szPlrName);
375 continue;
376 }
377 if (pPlrRes)
378 {
379 // join with resource
380 Game.Input.Add(eType: CID_JoinPlr,
381 pCtrl: new C4ControlJoinPlayer(pPlrRes->getFile(), pNewPacket->GetClientID(), pInfo->GetID(), pPlrRes->getCore()));
382 }
383 else
384 {
385 // join without resource (script player)
386 Game.Input.Add(eType: CID_JoinPlr,
387 pCtrl: new C4ControlJoinPlayer(nullptr, pNewPacket->GetClientID(), pInfo->GetID()));
388 }
389 }
390}
391
392void C4Network2Players::HandlePacket(char cStatus, const C4PacketBase *pPacket, C4Network2IOConnection *pConn)
393{
394 if (!pConn) return;
395
396 // find associated client
397 C4Network2Client *pClient = Game.Network.Clients.GetClient(pConn);
398 if (!pClient) pClient = Game.Network.Clients.GetClientByID(iID: pConn->getClientID());
399
400#define GETPKT(type, name) \
401 assert(pPacket); \
402 const type &name = static_cast<const type &>(*pPacket);
403
404 // player join request?
405 if (cStatus == PID_PlayerInfoUpdReq)
406 {
407 GETPKT(C4PacketPlayerInfoUpdRequest, pkPlrInfo);
408 // this packet is sent to the host only, and thus cannot have been sent from the host
409 if (!Game.Network.isHost()) return;
410 // handle this packet
411 HandlePlayerInfoUpdRequest(pInfoPacket: &pkPlrInfo.Info, fByHost: false);
412 }
413 else if (cStatus == PID_LeagueRoundResults)
414 {
415 GETPKT(C4PacketLeagueRoundResults, pkLeagueInfo);
416 // accepted from the host only
417 if (!pClient || !pClient->isHost()) return;
418 // process
419 Game.RoundResults.EvaluateLeague(szResultMsg: pkLeagueInfo.sResultsString.getData(), fSuccess: pkLeagueInfo.fSuccess, rLeagueInfo: pkLeagueInfo.Players);
420 }
421
422#undef GETPKT
423}
424
425void C4Network2Players::OnClientPart(C4Client *pPartClient)
426{
427 // lobby could be notified about the removal - but this would be redundant, because
428 // client leave notification is already done directly; this will delete any associated players
429 C4ClientPlayerInfos **ppCltInfo = rInfoList.GetInfoPtrByClientID(iClientID: pPartClient->getID());
430 // abort here if no info is registered - client seems to have had a short life only, anyway...
431 if (!ppCltInfo) return;
432 // remove all unjoined player infos
433 for (int32_t i = 0; i < (*ppCltInfo)->GetPlayerCount();)
434 {
435 C4PlayerInfo *pInfo = (*ppCltInfo)->GetPlayerInfo(iIndex: i);
436 // not joined yet? remove it
437 if (!pInfo->HasJoined())
438 (*ppCltInfo)->RemoveIndexedInfo(iAtIndex: i);
439 else
440 // just ignore, the "removed" flag will be set eventually
441 i++;
442 }
443 // empty? remove
444 if (!(*ppCltInfo)->GetPlayerCount())
445 rInfoList.RemoveInfo(ppRemoveInfo: ppCltInfo);
446 // update team association to left player
447 Game.Teams.RecheckPlayers();
448 // host: update player data according to leaver
449 if (Game.Network.isHost() && Game.Network.isEnabled())
450 {
451 // host: update any player colors and names
452 rInfoList.UpdatePlayerAttributes();
453 // team distribution of remaining unjoined players may change
454 Game.Teams.RecheckTeams();
455 // league score gains may now be different
456 Game.PlayerInfos.ResetLeagueProjectedGain(fSetUpdated: true);
457 // send changes to all clients and reset update flags
458 SendUpdatedPlayers();
459 }
460 // invalidate reference
461 if (Game.Network.isHost())
462 Game.Network.InvalidateReference();
463}
464
465void C4Network2Players::OnStatusGoReached()
466{
467 // host only
468 if (!Game.Network.isHost()) return;
469 // check all player lists
470 int i = 0; C4ClientPlayerInfos *pkInfo;
471 while (pkInfo = rInfoList.GetIndexedInfo(iIndex: i++))
472 // any unsent player joins?
473 if (pkInfo->HasUnjoinedPlayers())
474 {
475 // get client core
476 const C4Client *pClient = Game.Clients.getClientByID(iID: pkInfo->GetClientID());
477 // don't send if client is unknown or not activated yet
478 if (!pClient || !pClient->isActivated()) continue;
479 // send them w/o info packet
480 // info packets are synced during pause mode
481 JoinUnjoinedPlayersInControlQueue(pNewPacket: pkInfo);
482 }
483}
484
485C4ClientPlayerInfos *C4Network2Players::GetLocalPlayerInfoPacket() const
486{
487 // get local client ID
488 int iLocalClientID = Game.Clients.getLocalID();
489 // check all packets for same client ID as local
490 int i = 0; C4ClientPlayerInfos *pkInfo;
491 while (pkInfo = rInfoList.GetIndexedInfo(iIndex: i++))
492 if (pkInfo->GetClientID() == iLocalClientID)
493 // found
494 return pkInfo;
495 // not found
496 return nullptr;
497}
498
499uint32_t C4Network2Players::GetClientChatColor(int idForClient, bool fLobby) const
500{
501 // return color of first joined player; force to white for unknown
502 // deactivated always white
503 const C4Client *pClient = Game.Clients.getClientByID(iID: idForClient);
504 if (pClient && pClient->isActivated())
505 {
506 // get players for activated
507 C4ClientPlayerInfos *pInfoPacket = rInfoList.GetInfoByClientID(iClientID: idForClient);
508 C4PlayerInfo *pPlrInfo;
509 if (pInfoPacket && (pPlrInfo = pInfoPacket->GetPlayerInfo(iIndex: 0, eType: C4PT_User)))
510 if (fLobby)
511 return pPlrInfo->GetLobbyColor();
512 else
513 return pPlrInfo->GetColor();
514 }
515 // default color
516 return 0xffffff;
517}
518