1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 1999-2004 Eidos Interactive
4 Copyright (C) 2005-2020 Warzone 2100 Project
5
6 Warzone 2100 is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 Warzone 2100 is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Warzone 2100; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /**
21 * @file challenge.c
22 * Run challenges dialog.
23 *
24 */
25
26 #include <physfs.h>
27 #include <ctime>
28 #include <string>
29
30 #include "lib/framework/frame.h"
31 #include "lib/framework/input.h"
32 #include "lib/framework/wzconfig.h"
33 #include "lib/framework/physfs_ext.h"
34 #include "lib/netplay/netplay.h"
35 #include "lib/ivis_opengl/bitimage.h"
36 #include "lib/ivis_opengl/pieblitfunc.h"
37 #include "lib/widget/button.h"
38
39 #include "challenge.h"
40 #include "frontend.h"
41 #include "hci.h"
42 #include "intdisplay.h"
43 #include "loadsave.h"
44 #include "multiplay.h"
45 #include "mission.h"
46 #include "lib/framework/wztime.h"
47 #include "titleui/titleui.h"
48 #include "titleui/multiplayer.h"
49
50 #define totalslots 36 // challenge slots
51 #define slotsInColumn 12 // # of slots in a column
52 #define totalslotspace 256 // max chars for slot strings.
53
54 #define CHALLENGE_X D_W + 16
55 #define CHALLENGE_Y D_H + 5
56 #define CHALLENGE_W 610
57 #define CHALLENGE_H 215
58
59 #define CHALLENGE_HGAP 9
60 #define CHALLENGE_VGAP 9
61 #define CHALLENGE_BANNER_DEPTH 40 //top banner which displays either load or save
62
63 #define CHALLENGE_ENTRY_W ((CHALLENGE_W / 3 )-(3 * CHALLENGE_HGAP))
64 #define CHALLENGE_ENTRY_H (CHALLENGE_H -(5 * CHALLENGE_VGAP )- (CHALLENGE_BANNER_DEPTH+CHALLENGE_VGAP) ) /5
65
66 #define ID_LOADSAVE 21000
67 #define CHALLENGE_FORM ID_LOADSAVE+1 // back form.
68 #define CHALLENGE_CANCEL ID_LOADSAVE+2 // cancel but.
69 #define CHALLENGE_LABEL ID_LOADSAVE+3 // load/save
70 #define CHALLENGE_BANNER ID_LOADSAVE+4 // banner.
71
72 #define CHALLENGE_ENTRY_START ID_LOADSAVE+10 // each of the buttons.
73 #define CHALLENGE_ENTRY_END ID_LOADSAVE+10 +totalslots // must have unique ID hmm -Q
74
75 static std::shared_ptr<W_SCREEN> psRequestScreen = nullptr; // Widget screen for requester
76
77 bool challengesUp = false; ///< True when interface is up and should be run.
78 bool challengeActive = false; ///< Whether we are running a challenge
79 std::string challengeName;
80 WzString challengeFileName;
81
displayLoadBanner(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)82 static void displayLoadBanner(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
83 {
84 PIELIGHT col = WZCOL_GREEN;
85 UDWORD x = xOffset + psWidget->x();
86 UDWORD y = yOffset + psWidget->y();
87
88 pie_BoxFill(x, y, x + psWidget->width(), y + psWidget->height(), col);
89 pie_BoxFill(x + 2, y + 2, x + psWidget->width() - 2, y + psWidget->height() - 2, WZCOL_MENU_BACKGROUND);
90 }
91
currentChallengeName()92 const char* currentChallengeName()
93 {
94 if (challengeActive)
95 {
96 return challengeName.c_str();
97 }
98 return nullptr;
99 }
100
101 // quite the hack, game name is stored in global sRequestResult
updateChallenge(bool gameWon)102 void updateChallenge(bool gameWon)
103 {
104 char sPath[64], *fStr;
105 int seconds = 0, newtime = (gameTime - mission.startTime) / GAME_TICKS_PER_SEC;
106 bool victory = false;
107 WzConfig scores(CHALLENGE_SCORES, WzConfig::ReadAndWrite);
108
109 fStr = strrchr(sRequestResult, '/');
110 fStr++; // skip slash
111 if (*fStr == '\0')
112 {
113 debug(LOG_ERROR, "Bad path to challenge file (%s)", sRequestResult);
114 return;
115 }
116 sstrcpy(sPath, fStr);
117 sPath[strlen(sPath) - 5] = '\0'; // remove .json
118 scores.beginGroup(sPath);
119 victory = scores.value("victory", false).toBool();
120 seconds = scores.value("seconds", 0).toInt();
121
122 // Update score if we have a victory and best recorded was a loss,
123 // or both were losses but time is higher, or both were victories
124 // but time is lower.
125 if ((!victory && gameWon)
126 || (!gameWon && !victory && newtime > seconds)
127 || (gameWon && victory && newtime < seconds))
128 {
129 scores.setValue("seconds", newtime);
130 scores.setValue("victory", gameWon);
131 scores.setValue("player", NetPlay.players[selectedPlayer].name);
132 }
133 scores.endGroup();
134 }
135
136 // ////////////////////////////////////////////////////////////////////////////
137
138 struct DisplayLoadSlotCache {
139 std::string fullText;
140 WzText wzText;
141 };
142
143 struct DisplayLoadSlotData {
144 DisplayLoadSlotCache cache;
145 const char * filename;
146 };
147
displayLoadSlot(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)148 static void displayLoadSlot(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
149 {
150 // Any widget using displayLoadSlot must have its pUserData initialized to a (DisplayLoadSlotData*)
151 assert(psWidget->pUserData != nullptr);
152 DisplayLoadSlotData& data = *static_cast<DisplayLoadSlotData *>(psWidget->pUserData);
153
154 UDWORD x = xOffset + psWidget->x();
155 UDWORD y = yOffset + psWidget->y();
156 char butString[64];
157
158 drawBlueBox(x, y, psWidget->width(), psWidget->height()); //draw box
159
160 if (!((W_BUTTON *)psWidget)->pText.isEmpty())
161 {
162 sstrcpy(butString, ((W_BUTTON *)psWidget)->pText.toUtf8().c_str());
163
164 if (data.cache.fullText != butString)
165 {
166 // Update cache
167 while (iV_GetTextWidth(butString, font_regular) > psWidget->width())
168 {
169 butString[strlen(butString) - 1] = '\0';
170 }
171 data.cache.wzText.setText(butString, font_regular);
172 data.cache.fullText = butString;
173 }
174
175 data.cache.wzText.render(x + 4, y + 17, WZCOL_FORM_TEXT);
176 }
177 }
178
challengesScreenSizeDidChange(unsigned int oldWidth,unsigned int oldHeight,unsigned int newWidth,unsigned int newHeight)179 void challengesScreenSizeDidChange(unsigned int oldWidth, unsigned int oldHeight, unsigned int newWidth, unsigned int newHeight)
180 {
181 if (psRequestScreen == nullptr) return;
182 psRequestScreen->screenSizeDidChange(oldWidth, oldHeight, newWidth, newHeight);
183 }
184
185 //****************************************************************************************
186 // Challenge menu
187 //*****************************************************************************************
addChallenges()188 bool addChallenges()
189 {
190 char sPath[PATH_MAX];
191 const char *sSearchPath = "challenges";
192 UDWORD slotCount;
193 static char sSlotCaps[totalslots][totalslotspace];
194 static char sSlotTips[totalslots][totalslotspace];
195 static char sSlotFile[totalslots][totalslotspace];
196
197 psRequestScreen = W_SCREEN::make(); // init the screen
198
199 WIDGET *parent = psRequestScreen->psForm.get();
200
201 /* add a form to place the tabbed form on */
202 auto challengeForm = std::make_shared<IntFormAnimated>();
203
204 #if defined(WZ_CC_GNU) && !defined(WZ_CC_INTEL) && !defined(WZ_CC_CLANG) && (7 <= __GNUC__)
205 # pragma GCC diagnostic push
206 # pragma GCC diagnostic ignored "-Wnull-dereference"
207 #endif
208 challengeForm->id = CHALLENGE_FORM;
209 #if defined(WZ_CC_GNU) && !defined(WZ_CC_INTEL) && !defined(WZ_CC_CLANG) && (7 <= __GNUC__)
210 # pragma GCC diagnostic pop
211 #endif
212 parent->attach(challengeForm);
213 challengeForm->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
214 psWidget->setGeometry(CHALLENGE_X, CHALLENGE_Y, CHALLENGE_W, (slotsInColumn * CHALLENGE_ENTRY_H + CHALLENGE_HGAP * slotsInColumn) + CHALLENGE_BANNER_DEPTH + 20);
215 }));
216
217 // Add Banner
218 W_FORMINIT sFormInit;
219 sFormInit.formID = CHALLENGE_FORM;
220 sFormInit.id = CHALLENGE_BANNER;
221 sFormInit.style = WFORM_PLAIN;
222 sFormInit.x = CHALLENGE_HGAP;
223 sFormInit.y = CHALLENGE_VGAP;
224 sFormInit.width = CHALLENGE_W - (2 * CHALLENGE_HGAP);
225 sFormInit.height = CHALLENGE_BANNER_DEPTH;
226 sFormInit.pDisplay = displayLoadBanner;
227 sFormInit.UserData = 0;
228 widgAddForm(psRequestScreen, &sFormInit);
229
230 // add cancel.
231 W_BUTINIT sButInit;
232 sButInit.formID = CHALLENGE_BANNER;
233 sButInit.x = 8;
234 sButInit.y = 8;
235 sButInit.width = iV_GetImageWidth(IntImages, IMAGE_NRUTER);
236 sButInit.height = iV_GetImageHeight(IntImages, IMAGE_NRUTER);
237 sButInit.UserData = PACKDWORD_TRI(0, IMAGE_NRUTER , IMAGE_NRUTER);
238
239 sButInit.id = CHALLENGE_CANCEL;
240 sButInit.pTip = _("Close");
241 sButInit.pDisplay = intDisplayImageHilight;
242 widgAddButton(psRequestScreen, &sButInit);
243
244 // Add Banner Label
245 W_LABINIT sLabInit;
246 sLabInit.formID = CHALLENGE_BANNER;
247 sLabInit.FontID = font_large;
248 sLabInit.id = CHALLENGE_LABEL;
249 sLabInit.style = WLAB_ALIGNCENTRE;
250 sLabInit.x = 0;
251 sLabInit.y = 0;
252 sLabInit.width = CHALLENGE_W - (2 * CHALLENGE_HGAP); //CHALLENGE_W;
253 sLabInit.height = CHALLENGE_BANNER_DEPTH; //This looks right -Q
254 sLabInit.pText = WzString::fromUtf8("Challenge");
255 widgAddLabel(psRequestScreen, &sLabInit);
256
257 // add slots
258 sButInit = W_BUTINIT();
259 sButInit.formID = CHALLENGE_FORM;
260 sButInit.width = CHALLENGE_ENTRY_W;
261 sButInit.height = CHALLENGE_ENTRY_H;
262 sButInit.pDisplay = displayLoadSlot;
263 sButInit.initPUserDataFunc = []() -> void * { return new DisplayLoadSlotData(); };
264 sButInit.onDelete = [](WIDGET *psWidget) {
265 assert(psWidget->pUserData != nullptr);
266 delete static_cast<DisplayLoadSlotData *>(psWidget->pUserData);
267 psWidget->pUserData = nullptr;
268 };
269
270 for (slotCount = 0; slotCount < totalslots; slotCount++)
271 {
272 sButInit.id = slotCount + CHALLENGE_ENTRY_START;
273
274 if (slotCount < slotsInColumn)
275 {
276 sButInit.x = 22 + CHALLENGE_HGAP;
277 sButInit.y = (SWORD)((CHALLENGE_BANNER_DEPTH + (2 * CHALLENGE_VGAP)) + (
278 slotCount * (CHALLENGE_VGAP + CHALLENGE_ENTRY_H)));
279 }
280 else if (slotCount >= slotsInColumn && (slotCount < (slotsInColumn * 2)))
281 {
282 sButInit.x = 22 + (2 * CHALLENGE_HGAP + CHALLENGE_ENTRY_W);
283 sButInit.y = (SWORD)((CHALLENGE_BANNER_DEPTH + (2 * CHALLENGE_VGAP)) + (
284 (slotCount % slotsInColumn) * (CHALLENGE_VGAP + CHALLENGE_ENTRY_H)));
285 }
286 else
287 {
288 sButInit.x = 22 + (3 * CHALLENGE_HGAP + (2 * CHALLENGE_ENTRY_W));
289 sButInit.y = (SWORD)((CHALLENGE_BANNER_DEPTH + (2 * CHALLENGE_VGAP)) + (
290 (slotCount % slotsInColumn) * (CHALLENGE_VGAP + CHALLENGE_ENTRY_H)));
291 }
292 widgAddButton(psRequestScreen, &sButInit);
293 }
294
295 // fill slots.
296 slotCount = 0;
297
298 sstrcpy(sPath, sSearchPath);
299 sstrcat(sPath, "/*.json");
300
301 debug(LOG_SAVE, "Searching \"%s\" for challenges", sPath);
302
303 // add challenges to buttons
304 WZ_PHYSFS_enumerateFiles(sSearchPath, [&](const char *i) -> bool {
305 W_BUTTON *button;
306 WzString name, map, difficulty, highscore, description;
307 bool victory;
308 int seconds;
309
310 // See if this filename contains the extension we're looking for
311 if (!strstr(i, ".json"))
312 {
313 // If it doesn't, move on to the next filename
314 return true; // continue;
315 }
316
317 /* First grab any high score associated with this challenge */
318 sstrcpy(sPath, i);
319 sPath[strlen(sPath) - 5] = '\0'; // remove .json
320 highscore = "no score";
321 WzConfig scores(CHALLENGE_SCORES, WzConfig::ReadOnly);
322 scores.beginGroup(sPath);
323 name = scores.value("player", "NO NAME").toWzString();
324 victory = scores.value("victory", false).toBool();
325 seconds = scores.value("seconds", -1).toInt();
326 if (seconds > 0)
327 {
328 char psTimeText[sizeof("HH:MM:SS")] = { 0 };
329 struct tm tmp = getUtcTime(static_cast<time_t>(seconds));
330 strftime(psTimeText, sizeof(psTimeText), "%H:%M:%S", &tmp);
331 highscore = WzString::fromUtf8(psTimeText) + " by " + name + " (" + WzString(victory ? "Victory" : "Survived") + ")";
332 }
333 scores.endGroup();
334 ssprintf(sPath, "%s/%s", sSearchPath, i);
335 WzConfig challenge(sPath, WzConfig::ReadOnlyAndRequired);
336 ASSERT(challenge.contains("challenge"), "Invalid challenge file %s - no challenge section!", sPath);
337 challenge.beginGroup("challenge");
338 ASSERT(challenge.contains("name"), "Invalid challenge file %s - no name", sPath);
339 name = challenge.value("name", "BAD NAME").toWzString();
340 ASSERT(challenge.contains("map"), "Invalid challenge file %s - no map", sPath);
341 map = challenge.value("map", "BAD MAP").toWzString();
342 difficulty = challenge.value("difficulty", "BAD DIFFICULTY").toWzString();
343 description = map + ", " + difficulty + ", " + highscore + ".\n" + challenge.value("description", "").toWzString();
344
345 button = (W_BUTTON *)widgGetFromID(psRequestScreen, CHALLENGE_ENTRY_START + slotCount);
346
347 debug(LOG_SAVE, "We found [%s]", i);
348
349 /* Set the button-text */
350 sstrcpy(sSlotCaps[slotCount], name.toUtf8().c_str()); // store it!
351 sstrcpy(sSlotTips[slotCount], description.toUtf8().c_str()); // store it, too!
352 sstrcpy(sSlotFile[slotCount], sPath); // store filename
353
354 /* Add button */
355 button->pTip = sSlotTips[slotCount];
356 button->pText = WzString::fromUtf8(sSlotCaps[slotCount]);
357 assert(button->pUserData != nullptr);
358 static_cast<DisplayLoadSlotData *>(button->pUserData)->filename = sSlotFile[slotCount];
359 slotCount++; // go to next button...
360 if (slotCount == totalslots)
361 {
362 return false; // break;
363 }
364 challenge.endGroup();
365 return true; // continue
366 });
367
368 challengesUp = true;
369
370 return true;
371 }
372
373 // ////////////////////////////////////////////////////////////////////////////
closeChallenges()374 bool closeChallenges()
375 {
376 psRequestScreen = nullptr;
377 // need to "eat" up the return key so it don't pass back to game.
378 inputLoseFocus();
379 challengesUp = false;
380 return true;
381 }
382
383 // ////////////////////////////////////////////////////////////////////////////
384 // Returns true if cancel pressed or a valid game slot was selected.
385 // if when returning true strlen(sRequestResult) != 0 then a valid game
386 // slot was selected otherwise cancel was selected..
runChallenges()387 bool runChallenges()
388 {
389 WidgetTriggers const &triggers = widgRunScreen(psRequestScreen);
390 for (const auto &trigger : triggers)
391 {
392 unsigned id = trigger.widget->id;
393
394 sstrcpy(sRequestResult, ""); // set returned filename to null;
395
396 // cancel this operation...
397 if (id == CHALLENGE_CANCEL || CancelPressed())
398 {
399 goto failure;
400 }
401
402 // clicked a load entry
403 if (id >= CHALLENGE_ENTRY_START && id <= CHALLENGE_ENTRY_END)
404 {
405 W_BUTTON * psWidget = static_cast<W_BUTTON *>(widgGetFromID(psRequestScreen, id));
406 assert(psWidget != nullptr);
407 if (!(psWidget->pText.isEmpty()))
408 {
409 DisplayLoadSlotData * data = static_cast<DisplayLoadSlotData *>(psWidget->pUserData);
410 assert(data != nullptr);
411 assert(data->filename != nullptr);
412 sstrcpy(sRequestResult, data->filename);
413 challengeFileName = sRequestResult;
414 challengeName = psWidget->pText.toStdString();
415 }
416 else
417 {
418 goto failure; // clicked on an empty box
419 }
420 goto success;
421 }
422 }
423
424 return false;
425
426 // failed and/or cancelled..
427 failure:
428 closeChallenges();
429 challengeActive = false;
430 return false;
431
432 // success on load.
433 success:
434 closeChallenges();
435 challengeActive = true;
436 ingame.side = InGameSide::HOST_OR_SINGLEPLAYER;
437 changeTitleUI(std::make_shared<WzMultiplayerOptionsTitleUI>(wzTitleUICurrent));
438 return true;
439 }
440
441 // ////////////////////////////////////////////////////////////////////////////
442 // should be done when drawing the other widgets.
displayChallenges()443 bool displayChallenges()
444 {
445 widgDisplayScreen(psRequestScreen); // display widgets.
446 return true;
447 }
448