1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2008, 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// Round result information to be displayed in game over dialog
19
20#include <C4Include.h>
21#include <C4RoundResults.h>
22
23#include <C4Player.h>
24#include <C4GraphicsSystem.h>
25#include <C4Game.h>
26#include <C4Object.h>
27#include <C4Wrappers.h>
28
29// C4RoundResultsPlayer
30
31void C4RoundResultsPlayer::CompileFunc(StdCompiler *pComp)
32{
33 // remember to adjust operator = and == when adding values here!
34 pComp->Value(rStruct: mkNamingAdapt(rValue&: id, szName: "ID", rDefault: 0));
35 pComp->Value(rStruct: mkNamingAdapt(rValue&: iTotalPlayingTime, szName: "TotalPlayingTime", rDefault: 0u));
36 pComp->Value(rStruct: mkNamingAdapt(rValue&: iScoreOld, szName: "SettlementScoreOld", rDefault: -1));
37 pComp->Value(rStruct: mkNamingAdapt(rValue&: iScoreNew, szName: "SettlementScoreNew", rDefault: -1));
38 pComp->Value(rStruct: mkNamingAdapt(rValue&: iLeagueScoreNew, szName: "Score", rDefault: -1)); // name used in league reply!
39 pComp->Value(rStruct: mkNamingAdapt(rValue&: iLeagueScoreGain, szName: "GameScore", rDefault: -1)); // name used in league reply!
40 pComp->Value(rStruct: mkNamingAdapt(rValue&: iLeagueRankNew, szName: "Rank", rDefault: 0)); // name used in league reply!
41 pComp->Value(rStruct: mkNamingAdapt(rValue&: iLeagueRankSymbolNew, szName: "RankSymbol", rDefault: 0)); // name used in league reply!
42 pComp->Value(rStruct: mkNamingAdapt(rValue&: sLeagueProgressData, szName: "LeagueProgressData", rDefault: StdStrBuf()));
43 StdEnumEntry<LeagueStatus> LeagueStatusEntries[] =
44 {
45 { .Name: "", .Val: RRPLS_Unknown },
46 { .Name: "Lost", .Val: RRPLS_Lost },
47 { .Name: "Won", .Val: RRPLS_Won },
48 };
49 pComp->Value(rStruct: mkNamingAdapt(rValue: mkEnumAdaptT<uint8_t>(rVal&: eLeagueStatus, pNames: LeagueStatusEntries), szName: "Status", rDefault: RRPLS_Unknown)); // name used in league reply!
50}
51
52void C4RoundResultsPlayer::EvaluatePlayer(C4Player *pPlr)
53{
54 assert(pPlr);
55 // set fields by player
56 iTotalPlayingTime = pPlr->TotalPlayingTime;
57 if (pPlr->Evaluated)
58 {
59 iScoreNew = pPlr->Score;
60 iScoreOld = iScoreNew - pPlr->LastRound.FinalScore;
61 }
62 else
63 {
64 // player not evaluated (e.g., removed by disconnect): Old score known only
65 iScoreOld = pPlr->Score;
66 }
67 // load icon from player
68 fctBigIcon.Clear();
69 if (pPlr->BigIcon.Surface)
70 {
71 fctBigIcon.Create(iWdt: pPlr->BigIcon.Wdt, iHgt: pPlr->BigIcon.Hgt);
72 pPlr->BigIcon.Draw(cgo&: fctBigIcon);
73 }
74 // progress data by player
75 C4PlayerInfo *pInfo = pPlr->GetInfo();
76 if (pInfo)
77 {
78 sLeagueProgressData.Copy(pnData: pInfo->GetLeagueProgressData());
79 }
80}
81
82void C4RoundResultsPlayer::EvaluateLeague(C4RoundResultsPlayer *pLeaguePlayerInfo)
83{
84 assert(pLeaguePlayerInfo);
85
86 // copy league info
87 iLeagueScoreNew = pLeaguePlayerInfo->iLeagueScoreNew;
88 iLeagueScoreGain = pLeaguePlayerInfo->iLeagueScoreGain;
89 iLeagueRankNew = pLeaguePlayerInfo->iLeagueRankNew;
90 iLeagueRankSymbolNew = pLeaguePlayerInfo->iLeagueRankSymbolNew;
91 sLeagueProgressData = pLeaguePlayerInfo->sLeagueProgressData;
92}
93
94void C4RoundResultsPlayer::AddCustomEvaluationString(const char *szCustomString)
95{
96 if (sCustomEvaluationStrings.getLength()) sCustomEvaluationStrings.Append(pnData: " ");
97 sCustomEvaluationStrings.Append(pnData: szCustomString);
98}
99
100bool C4RoundResultsPlayer::operator==(const C4RoundResultsPlayer &cmp) const
101{
102 // cmp all xcept icon
103 if (id != cmp.id) return false;
104 if (iTotalPlayingTime != cmp.iTotalPlayingTime) return false;
105 if (iScoreOld != cmp.iScoreOld) return false;
106 if (iScoreNew != cmp.iScoreNew) return false;
107 if (sCustomEvaluationStrings != cmp.sCustomEvaluationStrings) return false;
108 if (iLeagueScoreNew != cmp.iLeagueScoreNew) return false;
109 if (iLeagueScoreGain != cmp.iLeagueScoreGain) return false;
110 if (iLeagueRankNew != cmp.iLeagueRankNew) return false;
111 if (iLeagueRankSymbolNew != cmp.iLeagueRankSymbolNew) return false;
112 if (eLeagueStatus != cmp.eLeagueStatus) return false;
113 return true;
114}
115
116C4RoundResultsPlayer &C4RoundResultsPlayer::operator=(const C4RoundResultsPlayer &cpy)
117{
118 if (this == &cpy) return *this;
119 // assign all xcept icon
120 id = cpy.id;
121 iTotalPlayingTime = cpy.iTotalPlayingTime;
122 iScoreOld = cpy.iScoreOld;
123 iScoreNew = cpy.iScoreNew;
124 sCustomEvaluationStrings = cpy.sCustomEvaluationStrings;
125 iLeagueScoreNew = cpy.iLeagueScoreNew;
126 iLeagueScoreGain = cpy.iLeagueScoreGain;
127 iLeagueRankNew = cpy.iLeagueRankNew;
128 iLeagueRankSymbolNew = cpy.iLeagueRankSymbolNew;
129 sLeagueProgressData = cpy.sLeagueProgressData;
130 eLeagueStatus = cpy.eLeagueStatus;
131 return *this;
132}
133
134// C4RoundResultsPlayers
135
136void C4RoundResultsPlayers::Clear()
137{
138 while (iPlayerCount) delete ppPlayers[--iPlayerCount];
139 delete[] ppPlayers;
140 ppPlayers = nullptr;
141 iPlayerCapacity = 0;
142}
143
144void C4RoundResultsPlayers::CompileFunc(StdCompiler *pComp)
145{
146 bool fCompiler = pComp->isCompiler();
147 if (fCompiler) Clear();
148 int32_t iTemp = iPlayerCount;
149 pComp->Value(rStruct: mkNamingCountAdapt<int32_t>(iCount&: iTemp, szName: "Player"));
150 if (iTemp < 0 || iTemp > C4MaxPlayer)
151 {
152 pComp->excCorrupt(message: "player count out of range"); return;
153 }
154 // Grow list, if necessary
155 if (fCompiler && iTemp > iPlayerCapacity)
156 {
157 GrowList(iByVal: iTemp - iPlayerCapacity);
158 iPlayerCount = iTemp;
159 std::fill_n(first: ppPlayers, n: iPlayerCount, value: nullptr);
160 }
161 // Compile
162 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdaptMapS(array: ppPlayers, size: iPlayerCount, map: mkPtrAdaptNoNull<C4RoundResultsPlayer>), szName: "Player"));
163 // Force specialization
164 mkPtrAdaptNoNull<C4RoundResultsPlayer>(rpObj&: *ppPlayers);
165}
166
167C4RoundResultsPlayer *C4RoundResultsPlayers::GetByIndex(int32_t idx) const
168{
169 if (idx >= 0 && idx < iPlayerCount)
170 return ppPlayers[idx];
171 else
172 return nullptr;
173}
174
175C4RoundResultsPlayer *C4RoundResultsPlayers::GetByID(int32_t id) const
176{
177 for (int32_t idx = 0; idx < iPlayerCount; ++idx)
178 if (ppPlayers[idx]->GetID() == id)
179 return ppPlayers[idx];
180 return nullptr;
181}
182
183void C4RoundResultsPlayers::GrowList(size_t iByVal)
184{
185 // create new list (out of mem: simply returns here; info list remains in a valid state)
186 C4RoundResultsPlayer **ppNew = new C4RoundResultsPlayer *[iPlayerCapacity += iByVal];
187 // move existing
188 if (ppPlayers)
189 {
190 memcpy(dest: ppNew, src: ppPlayers, n: iPlayerCount * sizeof(C4RoundResultsPlayer *));
191 }
192 delete[] ppPlayers;
193 // assign new
194 ppPlayers = ppNew;
195}
196
197void C4RoundResultsPlayers::Add(C4RoundResultsPlayer *pNewPlayer)
198{
199 assert(pNewPlayer);
200 if (iPlayerCount == iPlayerCapacity) GrowList(iByVal: 4);
201 ppPlayers[iPlayerCount++] = pNewPlayer;
202}
203
204C4RoundResultsPlayer *C4RoundResultsPlayers::GetCreateByID(int32_t id)
205{
206 assert(id);
207 // find existing
208 C4RoundResultsPlayer *pPlr = GetByID(id);
209 // not found: Add new
210 if (!pPlr)
211 {
212 pPlr = new C4RoundResultsPlayer();
213 pPlr->SetID(id);
214 Add(pNewPlayer: pPlr);
215 }
216 return pPlr;
217}
218
219bool C4RoundResultsPlayers::operator==(const C4RoundResultsPlayers &cmp) const
220{
221 if (iPlayerCount != cmp.iPlayerCount) return false;
222 for (int32_t i = 0; i < iPlayerCount; ++i)
223 if (!(*ppPlayers[i] == *cmp.ppPlayers[i]))
224 return false;
225 // equal
226 return true;
227}
228
229C4RoundResultsPlayers &C4RoundResultsPlayers::operator=(const C4RoundResultsPlayers &cpy)
230{
231 Clear();
232 C4RoundResultsPlayer *pPlr; int32_t i = 0;
233 while (pPlr = cpy.GetByIndex(idx: i++))
234 Add(pNewPlayer: new C4RoundResultsPlayer(*pPlr));
235 return *this;
236}
237
238// C4RoundResults
239
240void C4RoundResults::Init()
241{
242 if (Game.C4S.Game.IsMelee())
243 fHideSettlementScore = true;
244 else fHideSettlementScore = false;
245}
246
247void C4RoundResults::Clear()
248{
249 Players.Clear();
250 Goals.Clear();
251 iPlayingTime = 0;
252 sCustomEvaluationStrings.Clear();
253 iLeaguePerformance = 0;
254 sNetResult.Clear();
255 eNetResult = NR_None;
256 fHideSettlementScore = false;
257}
258
259void C4RoundResults::CompileFunc(StdCompiler *pComp)
260{
261 bool fCompiler = pComp->isCompiler();
262 if (fCompiler) Clear();
263 pComp->Value(rStruct: mkNamingAdapt(rValue&: Goals, szName: "Goals", rDefault: C4IDList()));
264 pComp->Value(rStruct: mkNamingAdapt(rValue&: iPlayingTime, szName: "PlayingTime", rDefault: 0u));
265 pComp->Value(rStruct: mkNamingAdapt(rValue&: fHideSettlementScore, szName: "HideSettlementScore", rDefault: !!Game.C4S.Game.IsMelee()));
266 pComp->Value(rStruct: mkNamingAdapt(rValue&: sCustomEvaluationStrings, szName: "CustomEvaluationStrings", rDefault: StdStrBuf()));
267 pComp->Value(rStruct: mkNamingAdapt(rValue&: iLeaguePerformance, szName: "LeaguePerformance", rDefault: 0));
268 pComp->Value(rStruct: mkNamingAdapt(rValue&: Players, szName: "PlayerInfos", rDefault: C4RoundResultsPlayers()));
269 pComp->Value(rStruct: mkNamingAdapt(rValue&: sNetResult, szName: "NetResult", rDefault: StdStrBuf()));
270 StdEnumEntry<NetResult> NetResultEntries[] =
271 {
272 { .Name: "", .Val: NR_None },
273 { .Name: "LeagueOK", .Val: NR_LeagueOK },
274 { .Name: "LeagueError", .Val: NR_LeagueError },
275 { .Name: "NetError", .Val: NR_NetError },
276 };
277 pComp->Value(rStruct: mkNamingAdapt(rValue: mkEnumAdaptT<uint8_t>(rVal&: eNetResult, pNames: NetResultEntries), szName: "NetResult", rDefault: NR_None));
278}
279
280void C4RoundResults::EvaluateGoals(C4IDList &GoalList, C4IDList &FulfilledGoalList, int32_t iPlayerNumber)
281{
282 // clear prev
283 GoalList.Clear(); FulfilledGoalList.Clear();
284 // Items
285 bool fRivalvry = !!Game.ObjectCount(id: C4Id(str: "RVLR"));
286 int32_t cnt; C4ID idGoal;
287 for (cnt = 0; idGoal = Game.Objects.GetListID(dwCategory: C4D_Goal, Index: cnt); cnt++)
288 {
289 // determine if the goal is fulfilled - do the calls even if the menu is not to be opened to ensure synchronization
290 bool fFulfilled = false;
291 C4Object *pObj;
292 if (pObj = Game.Objects.Find(id: idGoal))
293 {
294 if (fRivalvry)
295 {
296 fFulfilled = static_cast<bool>(pObj->Call(PSF_IsFulfilledforPlr, pPars: {C4VInt(iVal: iPlayerNumber)}));
297 }
298 else
299 fFulfilled = static_cast<bool>(pObj->Call(PSF_IsFulfilled));
300 }
301 GoalList.SetIDCount(id: idGoal, count: cnt, addNewID: true);
302 if (fFulfilled) FulfilledGoalList.SetIDCount(id: idGoal, count: 1, addNewID: true);
303 }
304}
305
306void C4RoundResults::EvaluateGame()
307{
308 // set game data
309 C4Player *pFirstLocalPlayer = Game.Players.GetLocalByIndex(iIndex: 0);
310 int32_t iFirstLocalPlayer = pFirstLocalPlayer ? pFirstLocalPlayer->Number : NO_OWNER;
311 EvaluateGoals(GoalList&: Goals, FulfilledGoalList&: FulfilledGoals, iPlayerNumber: iFirstLocalPlayer);
312 iPlayingTime = Game.Time;
313}
314
315void C4RoundResults::EvaluateNetwork(C4RoundResults::NetResult eNetResult, const char *szResultMsg)
316{
317 // take result only if there was no previous result (the previous one is usually more specific)
318 if (!HasNetResult())
319 {
320 this->eNetResult = eNetResult;
321 if (szResultMsg) sNetResult.Copy(pnData: szResultMsg); else sNetResult.Clear();
322 }
323}
324
325void C4RoundResults::EvaluateLeague(const char *szResultMsg, bool fSuccess, const C4RoundResultsPlayers &rLeagueInfo)
326{
327 // League evaluation imples network evaluation
328 Game.RoundResults.EvaluateNetwork(eNetResult: fSuccess ? C4RoundResults::NR_LeagueOK : C4RoundResults::NR_LeagueError, szResultMsg);
329 // Evaluation called by league: Sets new league scores and ranks
330 C4RoundResultsPlayer *pPlr, *pOwnPlr; int32_t i = 0;
331 while (pPlr = rLeagueInfo.GetByIndex(idx: i++))
332 {
333 pOwnPlr = Players.GetCreateByID(id: pPlr->GetID());
334 pOwnPlr->EvaluateLeague(pLeaguePlayerInfo: pPlr);
335 }
336}
337
338void C4RoundResults::EvaluatePlayer(C4Player *pPlr)
339{
340 // Evaluation called by player when it's evaluated
341 assert(pPlr);
342 C4RoundResultsPlayer *pOwnPlr = Players.GetCreateByID(id: pPlr->ID);
343 pOwnPlr->EvaluatePlayer(pPlr);
344}
345
346void C4RoundResults::AddCustomEvaluationString(const char *szCustomString, int32_t idPlayer)
347{
348 // Set custom string to be shown in game over dialog
349 // idPlayer==0 for global strings
350 if (!idPlayer)
351 {
352 if (sCustomEvaluationStrings.getLength()) sCustomEvaluationStrings.AppendChar(cChar: '|');
353 sCustomEvaluationStrings.Append(pnData: szCustomString);
354 }
355 else
356 {
357 C4RoundResultsPlayer *pOwnPlr = Players.GetCreateByID(id: idPlayer);
358 pOwnPlr->AddCustomEvaluationString(szCustomString);
359 }
360}
361
362void C4RoundResults::HideSettlementScore(bool fHide)
363{
364 fHideSettlementScore = fHide;
365}
366
367bool C4RoundResults::SettlementScoreIsHidden()
368{
369 return fHideSettlementScore;
370}
371
372void C4RoundResults::SetLeaguePerformance(int32_t iNewPerf, int32_t idPlayer)
373{
374 // Store to be sent later. idPlayer == 0 means global performance.
375 if (!idPlayer)
376 {
377 iLeaguePerformance = iNewPerf;
378 }
379 else
380 {
381 C4RoundResultsPlayer *pOwnPlr = Players.GetCreateByID(id: idPlayer);
382 pOwnPlr->SetLeaguePerformance(iNewPerf);
383 }
384}
385
386int32_t C4RoundResults::GetLeaguePerformance(int32_t idPlayer) const
387{
388 if (!idPlayer)
389 return iLeaguePerformance;
390 else if (C4RoundResultsPlayer *pPlr = Players.GetByID(id: idPlayer))
391 return pPlr->GetLeaguePerformance();
392 return 0;
393}
394
395bool C4RoundResults::Load(C4Group &hGroup, const char *szFilename)
396{
397 // clear previous
398 Clear();
399 // load file contents
400 StdStrBuf Buf;
401 if (!hGroup.LoadEntryString(szEntryName: szFilename, Buf)) return false;
402 // compile
403 if (!CompileFromBuf_LogWarn<StdCompilerINIRead>(TargetStruct: mkNamingAdapt(rValue&: *this, szName: "RoundResults"), SrcBuf: Buf, szName: szFilename)) return false;
404 // done, success
405 return true;
406}
407
408bool C4RoundResults::Save(C4Group &hGroup, const char *szFilename)
409{
410 // remove previous entry from group
411 hGroup.DeleteEntry(szFilename);
412 // decompile
413 try
414 {
415 const std::string buf{DecompileToBuf<StdCompilerINIWrite>(SrcStruct: mkNamingAdapt(rValue&: *this, szName: "RoundResults"))};
416 // save it, if not empty
417 if (!buf.empty())
418 {
419 StdStrBuf copy{buf.c_str(), buf.size()};
420 if (!hGroup.Add(szName: szFilename, pBuffer&: copy, fChild: false, fHoldBuffer: true))
421 return false;
422 }
423 }
424 catch (const StdCompiler::Exception &)
425 {
426 return false;
427 }
428 // done, success
429 return true;
430}
431
432// C4PacketLeagueRoundResults
433
434void C4PacketLeagueRoundResults::CompileFunc(StdCompiler *pComp)
435{
436 pComp->Value(rStruct: mkNamingAdapt(rValue&: fSuccess, szName: "Success", rDefault: false));
437 pComp->Value(rStruct: mkNamingAdapt(rValue&: sResultsString, szName: "ResultString", rDefault: StdStrBuf()));
438 pComp->Value(rStruct&: Players);
439}
440