1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2005, Sven2
6 * Copyright (c) 2017-2021, 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// script-controlled InGame dialog to show player infos
19
20#include "C4Include.h"
21#include "C4Scoreboard.h"
22#include "C4Gui.h"
23#include "C4Game.h"
24#include "C4GameOverDlg.h"
25
26#include <utility>
27
28// ************************************************
29// *** C4Scoreboard
30
31void C4Scoreboard::Clear()
32{
33 // del all cells
34 delete[] pEntries;
35 pEntries = nullptr; iRows = iCols = 0;
36 // del dialog
37 iDlgShow = 0;
38 delete pDlg; pDlg = nullptr;
39}
40
41void C4Scoreboard::AddRow(int32_t iInsertBefore)
42{
43 // counts
44 int32_t iNewEntryCount = (iRows + 1) * iCols;
45 if (!iNewEntryCount) { ++iRows; return; }
46 // realloc and copy array
47 Entry *pNewEntries = new Entry[iNewEntryCount];
48 for (int32_t iRow = 0, iFromRow = 0; iFromRow < iRows; ++iRow, ++iFromRow)
49 {
50 if (iFromRow == iInsertBefore) ++iRow;
51 for (int32_t iCol = 0; iCol < iCols; ++iCol)
52 pNewEntries[iRow * iCols + iCol] = std::move(*GetCell(iCol, iRow: iFromRow));
53 }
54 ++iRows;
55 delete[] pEntries; pEntries = pNewEntries;
56}
57
58void C4Scoreboard::AddCol(int32_t iInsertBefore)
59{
60 // counts
61 int32_t iNewEntryCount = iRows * (iCols + 1);
62 if (!iNewEntryCount) { ++iCols; return; }
63 // realloc and copy array
64 Entry *pNewEntries = new Entry[iNewEntryCount];
65 for (int32_t iRow = 0; iRow < iRows; ++iRow)
66 for (int32_t iCol = 0, iFromCol = 0; iFromCol < iCols; ++iCol, ++iFromCol)
67 {
68 if (iFromCol == iInsertBefore) ++iCol;
69 pNewEntries[iRow * (iCols + 1) + iCol] = std::move(*GetCell(iCol: iFromCol, iRow));
70 }
71 ++iCols; // CAUTION: must inc after add, so GetCell won't point into bogus!
72 delete[] pEntries; pEntries = pNewEntries;
73}
74
75void C4Scoreboard::DelRow(int32_t iDelIndex)
76{
77 // counts
78 int32_t iNewEntryCount = (iRows - 1) * iCols;
79 if (!iNewEntryCount) { --iRows; delete[] pEntries; pEntries = nullptr; return; }
80 // realloc and copy array
81 Entry *pNewEntries = new Entry[iNewEntryCount]; Entry *pCpy = pNewEntries;
82 for (int32_t iRow = 0, iFromRow = 0; iRow < (iRows - 1); ++iRow, ++iFromRow)
83 {
84 if (iFromRow == iDelIndex) ++iFromRow;
85 for (int32_t iCol = 0; iCol < iCols; ++iCol)
86 *pCpy++ = std::move(*GetCell(iCol, iRow: iFromRow));
87 }
88 --iRows;
89 delete[] pEntries; pEntries = pNewEntries;
90}
91
92void C4Scoreboard::DelCol(int32_t iDelIndex)
93{
94 // counts
95 int32_t iNewEntryCount = iRows * (iCols - 1);
96 if (!iNewEntryCount) { --iCols; delete[] pEntries; pEntries = nullptr; return; }
97 // realloc and copy array
98 Entry *pNewEntries = new Entry[iNewEntryCount]; Entry *pCpy = pNewEntries;
99 for (int32_t iRow = 0; iRow < iRows; ++iRow)
100 for (int32_t iCol = 0, iFromCol = 0; iCol < (iCols - 1); ++iCol, ++iFromCol)
101 {
102 if (iFromCol == iDelIndex) ++iFromCol;
103 *pCpy++ = std::move(*GetCell(iCol: iFromCol, iRow));
104 }
105 --iCols; // CAUTION: must dec after add, so GetCell won't point into bogus!
106 delete[] pEntries; pEntries = pNewEntries;
107}
108
109void C4Scoreboard::SwapRows(int32_t iRow1, int32_t iRow2)
110{
111 Entry *pXChg1 = pEntries + iRow1 * iCols;
112 Entry *pXChg2 = pEntries + iRow2 * iCols;
113 int32_t i = iCols; while (i--) std::swap(a&: *pXChg1++, b&: *pXChg2++);
114}
115
116int32_t C4Scoreboard::GetColByKey(int32_t iKey) const
117{
118 // safety
119 if (!iRows) return -1;
120 // check all col headers
121 Entry *pCheck = pEntries;
122 for (int32_t i = 0; i < iCols; ++i)
123 if (pCheck++->iVal == iKey) return i;
124 return -1;
125}
126
127int32_t C4Scoreboard::GetRowByKey(int32_t iKey) const
128{
129 // safety
130 if (!iCols) return -1;
131 // check all row headers
132 Entry *pCheck = pEntries;
133 for (int32_t i = 0; i < iRows; ++i, (pCheck += iCols))
134 if (pCheck->iVal == iKey) return i;
135 return -1;
136}
137
138void C4Scoreboard::SetCell(int32_t iColKey, int32_t iRowKey, const char *szValue, int32_t iValue)
139{
140 // ensure primary row/col exists
141 if (!iCols || !iRows)
142 {
143 if (!iCols) AddCol(iInsertBefore: 0);
144 if (!iRows) AddRow(iInsertBefore: 0);
145 GetCell(iCol: 0, iRow: 0)->iVal = TitleKey;
146 }
147 // get row/col; create new if not yet existing
148 int32_t iCol = GetColByKey(iKey: iColKey);
149 if (iCol < 0) { AddCol(iInsertBefore: iCol = iCols); GetCell(iCol, iRow: 0)->iVal = iColKey; }
150 int32_t iRow = GetRowByKey(iKey: iRowKey);
151 if (iRow < 0) { AddRow(iInsertBefore: iRow = iRows); GetCell(iCol: 0, iRow)->iVal = iRowKey; }
152 // now set values
153 Entry *pCell = GetCell(iCol, iRow);
154 pCell->Text.Copy(pnData: szValue);
155 if (iCol && iRow) pCell->iVal = iValue; // do NOT overwrite index keys!
156 // if an empty value was set, prune empty
157 if (!szValue || !*szValue)
158 {
159 // prune empty row (not label row)
160 int32_t i;
161 if (iRow)
162 {
163 for (i = 1; i < iCols; ++i) if (GetCell(iCol: i, iRow)->Text) break;
164 if (i == iCols) DelRow(iDelIndex: iRow);
165 }
166 // prune empty col (not label col)
167 if (iCol)
168 {
169 for (i = 1; i < iRows; ++i) if (GetCell(iCol, iRow: i)->Text) break;
170 if (i == iRows) DelCol(iDelIndex: iCol);
171 }
172 }
173 // recalc row widths in display (else done by clear)
174 InvalidateRows();
175}
176
177const char *C4Scoreboard::GetCellString(int32_t iColKey, int32_t iRowKey)
178{
179 // get row/col
180 int32_t iCol = GetColByKey(iKey: iColKey);
181 int32_t iRow = GetRowByKey(iKey: iRowKey);
182 if (iCol < 0 || iRow < 0) return nullptr;
183 // now get value
184 Entry *pCell = GetCell(iCol, iRow);
185 return pCell->Text.getData();
186}
187
188int32_t C4Scoreboard::GetCellData(int32_t iColKey, int32_t iRowKey)
189{
190 // get row/col
191 int32_t iCol = GetColByKey(iKey: iColKey);
192 int32_t iRow = GetRowByKey(iKey: iRowKey);
193 if (iCol < 0 || iRow < 0) return 0;
194 // now get value
195 Entry *pCell = GetCell(iCol, iRow);
196 return pCell->iVal;
197}
198
199bool C4Scoreboard::SortBy(int32_t iColKey, bool fReverse)
200{
201 // get sort col
202 int32_t iCol = GetColByKey(iKey: iColKey);
203 if (iCol < 0) return false;
204 // sort
205 int32_t iSortDir = fReverse ? -1 : +1;
206 int32_t iSortBegin = 1, iSortEnd = iRows - 1;
207 while (iSortBegin < iSortEnd)
208 {
209 int32_t iNewBorder = iSortBegin; int32_t i;
210 for (i = iSortBegin; i < iSortEnd; ++i)
211 if (GetCell(iCol, iRow: i)->iVal * iSortDir > GetCell(iCol, iRow: i + 1)->iVal * iSortDir)
212 {
213 SwapRows(iRow1: i, iRow2: i + 1);
214 iNewBorder = i;
215 }
216 iSortEnd = iNewBorder;
217 for (i = iSortEnd; i > iSortBegin; --i)
218 if (GetCell(iCol, iRow: i - 1)->iVal * iSortDir > GetCell(iCol, iRow: i)->iVal * iSortDir)
219 {
220 SwapRows(iRow1: i - 1, iRow2: i);
221 iNewBorder = i;
222 }
223 iSortBegin = iNewBorder;
224 }
225 return true;
226}
227
228void C4Scoreboard::InvalidateRows()
229{
230 // recalculate row sizes
231 if (pDlg) pDlg->InvalidateRows();
232}
233
234void C4Scoreboard::DoDlgShow(int32_t iChange, bool fUserToggle)
235{
236 // safety: Only if GUI loaded, and GUI already in exclusive mode
237 if (!C4GUI::IsGUIValid() || Game.pGUI->IsExclusive()) return;
238 // update dlg show
239 iDlgShow += iChange;
240 if (!fUserToggle)
241 // script update: Dlg on off if iDlgShow variable passed zero
242 fUserToggle = (ShouldBeShown() == !pDlg);
243 else
244 // user pressed Tab: Always toggle except if the scoreboard cannot be shown at all
245 if (!CanBeShown() && !pDlg) fUserToggle = false;
246 if (fUserToggle)
247 {
248 if (!pDlg)
249 {
250 if (!C4GameOverDlg::IsShown()) // never show during game over dlg
251 Game.pGUI->ShowRemoveDlg(pDlg: pDlg = new C4ScoreboardDlg(this));
252 }
253 else
254 pDlg->Close(fOK: false);
255 }
256}
257
258void C4Scoreboard::HideDlg()
259{
260 // safety: Only if GUI loaded, and GUI already in exclusive mode
261 if (!C4GUI::IsGUIValid() || Game.pGUI->IsExclusive()) return;
262 // hide scoreboard if it was active
263 if (pDlg) pDlg->Close(fOK: false);
264}
265
266void C4Scoreboard::CompileFunc(StdCompiler *pComp)
267{
268 bool fCompiler = pComp->isCompiler();
269 if (fCompiler) Clear();
270 pComp->Value(rStruct: mkNamingAdapt(rValue&: iRows, szName: "Rows", rDefault: 0));
271 pComp->Value(rStruct: mkNamingAdapt(rValue&: iCols, szName: "Cols", rDefault: 0));
272 pComp->Value(rStruct: mkNamingAdapt(rValue&: iDlgShow, szName: "DlgShow", rDefault: 0));
273 if (iRows * iCols)
274 {
275 if (fCompiler) pEntries = new Entry[iRows * iCols];
276 for (int32_t iRow = 0; iRow < iRows; ++iRow)
277 for (int32_t iCol = 0; iCol < iCols; ++iCol)
278 {
279 Entry *pEnt = GetCell(iCol, iRow);
280 pComp->Value(rStruct: mkNamingAdapt(rValue&: pEnt->Text, szName: std::format(fmt: "Cell{}_{}String", args&: iCol, args&: iRow).c_str()));
281 pComp->Value(rStruct: mkNamingAdapt(rValue&: pEnt->iVal, szName: std::format(fmt: "Cell{}_{}Value", args&: iCol, args&: iRow).c_str()));
282 }
283 // recheck dlg show in read mode
284 // will usually not do anything, because reading is done before enetering shared mode
285 if (pComp->isCompiler()) DoDlgShow(iChange: 0, fUserToggle: false);
286 }
287}
288
289// ************************************************
290// *** C4ScoreboardDlg
291
292C4ScoreboardDlg::C4ScoreboardDlg(C4Scoreboard *pForScoreboard)
293 : C4GUI::Dialog(100, 100, "nops", false), piColWidths(nullptr), pBrd(pForScoreboard)
294{
295 Update();
296}
297
298C4ScoreboardDlg::~C4ScoreboardDlg()
299{
300 delete[] piColWidths;
301 pBrd->pDlg = nullptr;
302}
303
304void C4ScoreboardDlg::Update()
305{
306 // counts
307 int32_t iRowCount = pBrd->iRows; int32_t iColCount = pBrd->iCols;
308 delete[] piColWidths; piColWidths = nullptr;
309 // invalid board - scipters can create those, but there's no reason why the engine
310 // should display something pretty then; just keep dialog defaults
311 if (!iRowCount || !iColCount) return;
312 // calc sizes as max col widths plus some indent pixels
313 piColWidths = new int32_t[iColCount];
314 int32_t iWdt = XMargin * 2, iHgt = YMargin * 2;
315 for (int32_t iCol = 0; iCol < iColCount; ++iCol)
316 {
317 piColWidths[iCol] = XIndent;
318 for (int32_t iRow = 0; iRow < iRowCount; ++iRow)
319 {
320 C4Scoreboard::Entry *pCell = pBrd->GetCell(iCol, iRow);
321 if ((iRow || iCol) && !pCell->Text.isNull()) piColWidths[iCol] = std::max<int32_t>(a: piColWidths[iCol], b: Game.GraphicsResource.FontRegular.GetTextWidth(szText: pCell->Text.getData()) + XIndent);
322 }
323 iWdt += piColWidths[iCol];
324 }
325 iHgt += iRowCount * (Game.GraphicsResource.FontRegular.GetLineHeight() + YIndent);
326 const char *szTitle = pBrd->GetCell(iCol: 0, iRow: 0)->Text.getData();
327 if (szTitle) iWdt = std::max<int32_t>(a: iWdt, b: Game.GraphicsResource.FontRegular.GetTextWidth(szText: szTitle) + 40);
328 if (!pTitle != !szTitle) SetTitle(szToTitle: szTitle); // needed for title margin...
329 iWdt += GetMarginLeft() + GetMarginRight();
330 iHgt += GetMarginTop() + GetMarginBottom();
331 // update dialog
332 SetBounds(C4Rect(rcBounds.x, rcBounds.y, iWdt, iHgt));
333 SetTitle(szToTitle: szTitle);
334 if (szTitle && pTitle) pTitle->SetIcon(C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_Player));
335 // realign
336 C4GUI::Screen *pScr = GetScreen();
337 if (pScr) DoPlacement(pOnScreen: pScr, rPreferredDlgRect: pScr->GetPreferredDlgRect());
338}
339
340bool C4ScoreboardDlg::DoPlacement(C4GUI::Screen *pOnScreen, const C4Rect &rPreferredDlgRect)
341{
342 // align topright
343 SetPos(iXPos: rPreferredDlgRect.x + rPreferredDlgRect.Wdt - rcBounds.Wdt - 20, iYPos: rPreferredDlgRect.y + 38);
344 return true;
345}
346
347void C4ScoreboardDlg::Draw(C4FacetEx &cgo)
348{
349 if (!piColWidths) Update();
350 typedef C4GUI::Dialog ParentClass;
351 ParentClass::Draw(cgo);
352}
353
354void C4ScoreboardDlg::DrawElement(C4FacetEx &cgo)
355{
356 typedef C4GUI::Dialog ParentClass;
357 ParentClass::DrawElement(cgo);
358 // draw spreadsheet
359 int32_t iRowCount = pBrd->iRows; int32_t iColCount = pBrd->iCols;
360 int32_t iY = YMargin + cgo.TargetY + rcClientRect.y;
361 for (int32_t iRow = 0; iRow < iRowCount; ++iRow)
362 {
363 int32_t iX = XMargin + cgo.TargetX + rcClientRect.x;
364 for (int32_t iCol = 0; iCol < iColCount; ++iCol)
365 {
366 const char *szText = pBrd->GetCell(iCol, iRow)->Text.getData();
367 if (szText && *szText && (iRow || iCol))
368 lpDDraw->TextOut(szText, rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: iCol ? iX + piColWidths[iCol] / 2 : iX, iTy: iY, dwFCol: 0xffffffff, byForm: iCol ? ACenter : ALeft);
369 iX += piColWidths[iCol];
370 }
371 iY += Game.GraphicsResource.FontRegular.GetLineHeight() + YIndent;
372 }
373}
374