OpenClonk
C4StartupModsDlg.cpp
Go to the documentation of this file.
1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2006-2009, RedWolf Design GmbH, http://www.clonk.de/
5  * Copyright (c) 2009-2017, The OpenClonk Team and contributors
6  *
7  * Distributed under the terms of the ISC license; see accompanying file
8  * "COPYING" for details.
9  *
10  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11  * See accompanying file "TRADEMARK" for details.
12  *
13  * To redistribute this file separately, substitute the full license texts
14  * for the above references.
15  */
16  // Screen for mod handling
17 
18 #include "C4Include.h"
19 #include "gui/C4StartupModsDlg.h"
20 
21 #include "C4Version.h"
22 #include "lib/SHA1.h"
23 #include "game/C4Application.h"
24 #include "gui/C4UpdateDlg.h"
25 #include "game/C4Game.h"
27 #include "graphics/C4Draw.h"
28 #include "network/C4HTTP.h"
29 
30 #include <tinyxml.h>
31 
32 #include <fstream>
33 #include <sstream>
34 #include <regex>
35 
36 // Used to parse values of subelements from an XML element.
37 std::string getSafeStringValue(const TiXmlElement *xml, const char *childName, std::string fallback = "", bool isAttribute = false)
38 {
39  if (xml == nullptr) return fallback;
40  if (isAttribute)
41  {
42  const char * value = xml->Attribute(childName);
43  if (value == nullptr)
44  return fallback;
45  return{ value };
46  }
47  const TiXmlElement *child = xml->FirstChildElement(childName);
48  if (child == nullptr) return fallback;
49  const char *nodeText = child->GetText();
50  if (nodeText == nullptr) return fallback;
51  const std::string value(nodeText);
52  if (!value.empty()) return value;
53  return fallback;
54 };
55 
56 ModXMLData::ModXMLData(const TiXmlElement *xml, Source source)
57 {
58 
59  // Remember whether the loaded the element from a local file / overview, because we
60  // then still have to do a query if we want to update.
61  this->source = source;
62  // It is possible to instantiate this without valid XML.
63  if (xml == nullptr)
64  {
65  metadataMissing = true;
66  return;
67  }
68  // Remember the XML element in case we need to pretty-print it later.
69  originalXMLElement = xml->Clone();
70  id = getSafeStringValue(xml, "id", "");
71  title = getSafeStringValue(xml, "title", "");
72  assert(IsValidUtf8(title.c_str()));
73  slug = getSafeStringValue(xml, "slug", title);
74  assert(IsValidUtf8(slug.c_str()));
75  description = getSafeStringValue(xml, "description");
76  assert(IsValidUtf8(description.c_str()));
77  longDescription = getSafeStringValue(xml, "long_description");
78  if (!description.empty())
79  {
80  const bool descriptionNeedsCut = description.size() > 150;
81  if (descriptionNeedsCut)
82  {
83  // Find a cutoff that is not inside a UTF-8 character.
84  std::string::size_type characterCount{ 0 };
85  for (const char* c = description.data(); *c != '\0';)
86  {
87  const uint8_t val = *c;
88  if (val <= 127)
89  {
90  c += 1;
91  }
92  else
93  {
95  }
96  characterCount += 1;
97 
98  const bool shouldCutHere = characterCount >= 150;
99  if (shouldCutHere)
100  {
101  uint32_t byteDifference = c - description.data();
102  description.resize(byteDifference);
103  break;
104  }
105  }
106  }
107  }
108  assert(IsValidUtf8(description.c_str()));
109  // Additional meta-information.
110 
111  const TiXmlElement* dependencies_node = xml->FirstChildElement("dependencies");
112  if (dependencies_node != nullptr)
113  for (const TiXmlElement *node = dependencies_node->FirstChildElement("item"); node != nullptr; node = node->NextSiblingElement("item"))
114  {
115  const char *depID = node->GetText();
116  if (depID != nullptr)
117  dependencies.push_back(depID);
118  }
119 
120  const TiXmlElement* tags_node = xml->FirstChildElement("tags");
121  if (tags_node != nullptr)
122  for (const TiXmlElement *node = tags_node->FirstChildElement("item"); node != nullptr; node = node->NextSiblingElement("item"))
123  {
124  const std::string tag = node->GetText();
125  if (!tag.empty())
126  tags.push_back(tag);
127  }
128 
129  const TiXmlElement* files_node = xml->FirstChildElement("files");
130  if (files_node != nullptr)
131  for (const TiXmlElement *filenode = files_node->FirstChildElement("item"); filenode != nullptr; filenode = filenode->NextSiblingElement("item"))
132  {
133  // We guarantee that we do not modify the handle below, thus the const_cast is safe.
134  const TiXmlHandle nodeHandle(const_cast<TiXmlNode*> (static_cast<const TiXmlNode*> (filenode)));
135 
136  const std::string handle = getSafeStringValue(filenode, "id", "");
137  const std::string name = getSafeStringValue(filenode, "filename", "");
138  const std::string lengthString = getSafeStringValue(filenode, "length", "");
139 
140  const std::string hashSHA1 = getSafeStringValue(filenode, "sha1", "");
141 
142  if (handle.empty() || name.empty() || lengthString.empty()) continue;
143  size_t length{ 0 };
144 
145  try
146  {
147  length = std::stoi(lengthString);
148  }
149  catch (...)
150  {
151  continue;
152  }
153 
154  files.emplace_back(FileInfo{ handle, length, name, hashSHA1 });
155  }
156 }
157 
159 {
160  delete originalXMLElement;
161  originalXMLElement = nullptr;
162 }
163 
164 // ----------- C4StartupNetListEntry -----------------------------------------------------------------------
165 
167  : pModsDlg(pModsDlg), pList(pForListBox), iInfoIconCount(0)
168 {
169  // calc height
170  int32_t iLineHgt = ::GraphicsResource.TextFont.GetLineHeight(), iHeight = iLineHgt * 2 + 4;
171  // add icons - normal icons use small size, only animated netgetref uses full size
172  rctIconLarge.Set(0, 0, iHeight, iHeight);
173  int32_t iSmallIcon = iHeight * 2 / 3; rctIconSmall.Set((iHeight - iSmallIcon)/2, (iHeight - iSmallIcon)/2, iSmallIcon, iSmallIcon);
174  pIcon = new C4GUI::Icon(rctIconSmall, C4GUI::Ico_None);
175  AddElement(pIcon);
176  SetBounds(pIcon->GetBounds());
177  // add to listbox (will get resized horizontally and moved)
178  pForListBox->InsertElement(this, pInsertBefore);
179  // add status icons and text labels now that width is known
180  CStdFont *pUseFont = &(::GraphicsResource.TextFont);
181  int32_t iIconSize = pUseFont->GetLineHeight();
182  C4Rect rcIconRect = GetContainedClientRect();
183  int32_t iThisWdt = rcIconRect.Wdt;
184  rcIconRect.x = iThisWdt - iIconSize * (iInfoIconCount + 1);
185  rcIconRect.Wdt = rcIconRect.Hgt = iIconSize;
186  for (int32_t iIcon = 0; iIcon<MaxInfoIconCount; ++iIcon)
187  {
188  AddElement(pInfoIcons[iIcon] = new C4GUI::Icon(rcIconRect, C4GUI::Ico_None));
189  rcIconRect.x -= rcIconRect.Wdt;
190  }
191  C4Rect rcLabelBounds;
192  rcLabelBounds.x = iHeight+3;
193  rcLabelBounds.Hgt = iLineHgt;
194  for (int i=0; i<InfoLabelCount; ++i)
195  {
196  const int alignments[] = { ALeft, ARight };
197  for (int c = 0; c < 2; ++c)
198  {
199  const int alignment = alignments[c];
200  rcLabelBounds.y = 1 + i*(iLineHgt + 2);
201  rcLabelBounds.Wdt = iThisWdt - rcLabelBounds.x - 1;
202  // The first line leaves some extra space for the icons.
203  if (i == 0)
204  rcLabelBounds.Wdt -= rcIconRect.x;
205  C4GUI::Label * const pLbl = new C4GUI::Label("", rcLabelBounds, alignment, C4GUI_CaptionFontClr);
206  if (alignment == ALeft) pInfoLbl[i] = pLbl;
207  else if (alignment == ARight) pInfoLabelsRight[i] = pLbl;
208  else assert(false);
209  AddElement(pLbl);
210  // label will have collapsed due to no text: Repair it
211  pLbl->SetAutosize(false);
212  pLbl->SetBounds(rcLabelBounds);
213  }
214  }
215  UpdateEntrySize();
216 }
217 
219 {
220  Clear();
221 }
222 
223 void C4StartupModsListEntry::FromXML(const TiXmlElement *xml, ModXMLData::Source source, std::string fallbackID, std::string fallbackName)
224 {
225  modXMLData = std::make_unique<ModXMLData>(xml, source);
226  if (modXMLData->id.empty()) modXMLData->id = fallbackID;
227  if (modXMLData->title.empty()) modXMLData->title = fallbackName;
228 
229  // Fallback for when only the folder is available.
230  if (xml == nullptr)
231  {
232  sInfoText[0].Format(LoadResStr("IDS_MODS_TITLE"), fallbackName.c_str(), "???");
233  return;
234  }
235 
236  if (source != ModXMLData::Source::Local)
237  {
238  std::string updated = getSafeStringValue(xml, "updatedAt", "");
239  std::string::size_type dateEnd;
240  if (!updated.empty() && (dateEnd = updated.find('T')) != std::string::npos)
241  {
242  updated = updated.substr(0, dateEnd);
243  }
244  else
245  updated = "/";
246  sInfoTextRight[0].Format(LoadResStr("IDS_MODS_METAINFO"), updated.c_str());
247  }
248  const std::string author = getSafeStringValue(xml, "author", "???");
249  const std::string title = modXMLData->title.empty() ? "???" : modXMLData->title;
250  sInfoText[0].Format(LoadResStr("IDS_MODS_TITLE"), title.c_str(), author.c_str());
251 
252  bool hasScenario = false;
253  bool hasObjectPackage = false;
254  static std::string openclonkVersionStringTag;
255  if (openclonkVersionStringTag.empty())
256  openclonkVersionStringTag = C4StartupModsDlg::GetOpenClonkVersionStringTag();
257 
258  std::ostringstream otherTagsStream;
259  bool isFirstOtherTag = true;
260 
261  for (auto &tag : modXMLData->tags)
262  {
263  if ((tag == ".scenario") && !hasScenario)
264  {
265  AddStatusIcon(C4GUI::Icons::Ico_Gfx, LoadResStr("IDS_MODS_TAGS_SCENARIO"));
266  hasScenario = true;
267  }
268  else if (tag == ".objects")
269  {
270  AddStatusIcon(C4GUI::Icons::Ico_Definition, LoadResStr("IDS_MODS_TAGS_OBJECT"));
271  hasObjectPackage = true;
272  }
273  else if (tag == "multiplayer")
274  AddStatusIcon(C4GUI::Icons::Ico_Team, LoadResStr("IDS_MODS_TAGS_MULTIPLAYER"));
275  else if (tag == openclonkVersionStringTag)
276  AddStatusIcon(C4GUI::Icons::Ico_Clonk, LoadResStr("IDS_MODS_TAGS_COMPATIBLE"));
277  else if (!tag.empty() && tag.front() != '.')
278  {
279  otherTagsStream << (isFirstOtherTag ? "" : ", ") << tag;
280  isFirstOtherTag = false;
281  }
282  }
283 
284  const std::string &otherTags = otherTagsStream.str();
285  if (!otherTags.empty())
286  {
287  AddStatusIcon(C4GUI::Icons::Ico_Star, (std::string(LoadResStr("IDS_MODS_TAGS_OTHERTAGS")) + otherTags).c_str(), false);
288  }
289 
290 
291  if (!modXMLData->metadataMissing && !modXMLData->longDescription.empty())
292  AddStatusIcon(C4GUI::Icons::Ico_Chart, modXMLData->longDescription.c_str(), false);
293 
294  if (hasScenario)
295  defaultIcon = C4GUI::Icons::Ico_Gfx;
296  else if (hasObjectPackage)
297  defaultIcon = C4GUI::Icons::Ico_Definition;
298 }
299 
301 {
302  isInfoEntry = true;
303 
304  const_cast<C4Facet &>(reinterpret_cast<const C4Facet &>(pIcon->GetFacet()))
306  pIcon->SetAnimated(true, 1);
307  pIcon->SetBounds(rctIconLarge);
308 
309  // set info
310  sInfoText[0].Copy(LoadResStr("IDS_MODS_SEARCHING"));
311  UpdateText();
312 }
313 
315 {
316  pIcon->SetAnimated(false, 1);
317  sInfoText[0].Copy(LoadResStr("IDS_MODS_SEARCH_NORESULTS"));
318  UpdateText();
319 }
320 
321 void C4StartupModsListEntry::ShowPageInfo(int page, int totalPages, int totalResults)
322 {
323  pIcon->SetAnimated(false, 1);
324  sInfoText[0].Copy(LoadResStr("IDS_MODS_SEARCH_NEXTPAGE"));
325  sInfoText[1].Format(LoadResStr("IDS_MODS_SEARCH_NEXTPAGE_DESC"), page, totalPages, totalResults);
326  UpdateText();
327 }
328 
329 void C4StartupModsListEntry::OnError(std::string message)
330 {
331  pIcon->SetAnimated(false, 1);
332  sInfoText[0].Copy(LoadResStr("IDS_MODS_SEARCH_ERROR"));
333  sInfoText[1].Copy(message.c_str());
334  UpdateText();
335 }
336 
338 {
339  typedef C4GUI::Window ParentClass;
340  // inherited
341  ParentClass::DrawElement(cgo);
342 }
343 
345 {
346  int32_t i;
347  for (i = 0; i < InfoLabelCount; ++i)
348  {
349  sInfoText[i].Clear();
350  sInfoTextRight[i].Clear();
351  }
352 }
353 
355 {
356  return true;
357 }
358 
359 void C4StartupModsListEntry::UpdateEntrySize()
360 {
361  if(fVisible) {
362  // restack all labels by their size
363  const int32_t iLblCnt = InfoLabelCount;
364  for (int c = 0; c < 2; ++c)
365  {
366  int iY = 1;
367  C4GUI::Label **labelList = (c == 0) ? pInfoLbl : pInfoLabelsRight;
368  for (int i = 0; i < iLblCnt; ++i)
369  {
370  C4Rect rcBounds = labelList[i]->GetBounds();
371  rcBounds.y = iY;
372  iY += rcBounds.Hgt + 2;
373  pInfoLbl[i]->SetBounds(rcBounds);
374  }
375  // Resize this control according to the labels on the left.
376  if (c == 0)
377  GetBounds().Hgt = iY - 1;
378  }
379  } else GetBounds().Hgt = 0;
380  UpdateSize();
381 }
382 
384 {
385  if (isInfoEntry) return;
386 
387  isInstalled = modInfo != nullptr;
388 
389  std::string fullDescription = "";
390  if (!modXMLData->metadataMissing)
391  fullDescription = modXMLData->description;
392  else
393  fullDescription = LoadResStr("IDS_MODS_METADATA_MISSING");
394 
395  if (modInfo != nullptr)
396  {
398 
399  fullDescription = std::string("<c 559955>") + LoadResStr("IDS_MODS_INSTALLED") + ".</c> " + fullDescription;
400  }
401  else
402  {
403  pIcon->SetIcon(defaultIcon);
404  }
405 
406  sInfoText[1].Format("%s", fullDescription.c_str());
407  UpdateText();
408 }
409 
410 void C4StartupModsListEntry::UpdateText()
411 {
412  bool fRestackElements=false;
413  CStdFont *pUseFont = &(::GraphicsResource.TextFont);
414  // adjust icons
415  int32_t sx=iInfoIconCount*pUseFont->GetLineHeight();
416  int32_t i;
417  for (i=iInfoIconCount; i<MaxInfoIconCount; ++i)
418  {
419  pInfoIcons[i]->SetIcon(C4GUI::Ico_None);
420  pInfoIcons[i]->SetToolTip(nullptr);
421  }
422  // text to labels
423  for (int c = 0; c < 2; ++c)
424  {
425  C4GUI::Label **infoLabels = (c == 0) ? pInfoLbl : pInfoLabelsRight;
426  StdStrBuf *infoTexts = (c == 0) ? sInfoText : sInfoTextRight;
427  for (i = 0; i < InfoLabelCount; ++i)
428  {
429  C4GUI::Label *infoLabel = infoLabels[i];
430  int iAvailableWdt = GetClientRect().Wdt - infoLabel->GetBounds().x - 1;
431  if (!i) iAvailableWdt -= sx;
432  StdStrBuf BrokenText;
433  pUseFont->BreakMessage(infoTexts[i].getData(), iAvailableWdt, &BrokenText, true);
434  int32_t iHgt, iWdt;
435  if (pUseFont->GetTextExtent(BrokenText.getData(), iWdt, iHgt, true))
436  {
437  if ((infoLabel->GetBounds().Hgt != iHgt) || (infoLabel->GetBounds().Wdt != iAvailableWdt))
438  {
439  C4Rect rcBounds = infoLabel->GetBounds();
440  rcBounds.Wdt = iAvailableWdt;
441  rcBounds.Hgt = iHgt;
442  infoLabel->SetBounds(rcBounds);
443  fRestackElements = true;
444  }
445  }
446  infoLabel->SetText(BrokenText.getData());
447  infoLabel->SetColor(C4GUI_MessageFontClr);
448  }
449  }
450  if (fRestackElements) UpdateEntrySize();
451 }
452 
454  bool fChange = fToValue != fVisible;
456  if(fChange) UpdateEntrySize();
457 }
458 
459 void C4StartupModsListEntry::AddStatusIcon(C4GUI::Icons eIcon, const char *szToolTip, bool insertLeft)
460 {
461  // safety
462  if (iInfoIconCount==MaxInfoIconCount) return;
463  // default: set icon to the left of the existing icons to the desired data
464  auto insertPosition = iInfoIconCount;
465 
466  if (!insertLeft)
467  {
468  // Move all icons one slot and insert on the right side.
469  for (auto i = iInfoIconCount - 1; i >= 0; --i)
470  {
471  pInfoIcons[i + 1]->SetFacet(pInfoIcons[i]->GetFacet());
472  pInfoIcons[i + 1]->SetToolTip(pInfoIcons[i]->GetToolTip());
473  }
474  insertPosition = 0;
475  }
476 
477  pInfoIcons[insertPosition]->SetIcon(eIcon);
478  pInfoIcons[insertPosition]->SetToolTip(szToolTip);
479  ++iInfoIconCount;
480 }
481 
483 {
484  assert(!discoveryFinished);
485  discoveryFinishedEvent.Reset();
486 
487  ExecuteDiscovery();
488 
489  discoveryFinished = true;
490  discoveryFinishedEvent.Set();
491 
492  parent->QueueSyncWithDiscovery();
493 
495 }
496 
497 void C4StartupModsLocalModDiscovery::ExecuteDiscovery()
498 {
499  // Check the mods directory for existing files.
500  const std::string path = std::string(Config.General.UserDataPath) + "mods";
501  for (DirectoryIterator iter(path.c_str()); *iter; ++iter)
502  {
503  const std::string filename(*iter);
504 
505  // No folder?
506  if (!DirectoryExists(filename.c_str())) continue;
507 
508  const size_t lastSeparaterPosition = filename.find_last_of(DirectorySeparator);
509  if (lastSeparaterPosition == std::string::npos) continue;
510  const std::string leaf = filename.substr(lastSeparaterPosition + 1);
511  // The leaf is prefixed with "<item ID>_" if it's a mod directory.
512  const size_t idSeparatorPosition = leaf.find_first_of("_");
513  if (idSeparatorPosition == std::string::npos) continue;
514  const std::string id = leaf.substr(0, idSeparatorPosition);
515  if (id.empty()) continue;
516  const std::string name = leaf.substr(idSeparatorPosition + 1);
517  AddMod(id, filename, name);
518  }
519 }
520 
522 {
523  this->parent = parent;
524 
525  if (entry != nullptr)
526  items.emplace_back(std::move(std::make_unique<ModInfo>(entry)));
527 
528  // Register timer.
529  Application.Add(this);
530 }
531 
532 void C4StartupModsDownloader::AddModToQueue(std::string modID, std::string name)
533 {
534  // Not if already contained.
535  for (auto &mod : items)
536  if (mod->modID == modID) return;
537  items.emplace_back(std::move(std::make_unique<ModInfo>(modID, name)));
538 }
539 
541 {
542  CStdLock lock(&guiThreadResponse);
543  Application.Remove(this);
544  CancelRequest();
545 }
546 
547 C4GUI::ProgressDialog * C4StartupModsDownloader::GetProgressDialog()
548 {
549  if (!progressDialog)
550  {
551  progressDialog = new C4GUI::ProgressDialog("", LoadResStr("IDS_MODS_SEARCHING"), 100, 0, C4GUI::Icons::Ico_Save);
552  parent->GetScreen()->ShowRemoveDlg(progressDialog);
553  progressDialog->SetDelOnClose(false);
554  }
555  return progressDialog;
556 }
557 
558 void C4StartupModsDownloader::CancelRequest()
559 {
560  for (auto & mod : items)
561  mod->CancelRequest();
562  items.resize(0);
563 
564  if (postMetadataClient.get())
565  {
566  Application.InteractiveThread.RemoveProc(postMetadataClient.get());
567  postMetadataClient.reset();
568  }
569 
570  if (progressDialog)
571  progressDialog->Close(true);
572  delete progressDialog;
573  progressDialog = nullptr;
574 
575  progressCallback = nullptr;
576  metadataQueriedForModIdx = -1;
577 }
578 
580 {
581  CStdLock lock(&guiThreadResponse);
582 
583  assert(!items.empty());
584  bool hasFile = false;
585  for (auto & item : items)
586  if (!item->files.empty()) hasFile = true;
587  assert(hasFile);
588 
589  GetProgressDialog()->SetTitle(LoadResStr("IDS_MODS_INSTALLANDDOWNLOAD"));
590  GetProgressDialog()->SetMessage("");
591  GetProgressDialog()->SetProgress(0);
592  GetProgressDialog()->SetVisibility(true);
593 
594  progressCallback = std::bind(&C4StartupModsDownloader::ExecuteCheckDownloadProgress, this);
595 }
596 
597 C4StartupModsDownloader::ModInfo::ModInfo(const C4StartupModsListEntry *entry) : ModInfo()
598 {
599  FromXMLData(entry->GetModXMLData());
600 }
601 
602 C4StartupModsDownloader::ModInfo::ModInfo(std::string modID, std::string name) : ModInfo()
603 {
604  Clear();
605  this->modID = modID;
606  this->name = name;
607  this->hasOnlyIncompleteInformation = true;
608 }
609 
610 void C4StartupModsDownloader::ModInfo::FromXMLData(const ModXMLData &xmlData)
611 {
612  Clear();
613  modID = xmlData.id;
614  name = xmlData.title;
615  slug = xmlData.slug;
616  dependencies = xmlData.dependencies;
617  originalXMLNode = xmlData.originalXMLElement != nullptr ? xmlData.originalXMLElement->Clone() : nullptr;
618  hasOnlyIncompleteInformation = xmlData.requiresUpdate();
619 
620  for (const auto & fileInfo : xmlData.files)
621  {
622  files.emplace_back(ModInfo::FileInfo{ fileInfo.handle, fileInfo.name, fileInfo.size, fileInfo.sha1 });
623  requiredFilenames.insert(fileInfo.name);
624  }
625 }
626 
627 void C4StartupModsDownloader::ModInfo::Clear()
628 {
629  CancelRequest();
630  files.resize(0);
631  requiredFilenames.clear();
632  downloadedBytes = totalBytes = 0;
633  delete originalXMLNode;
634  originalXMLNode = nullptr;
635 }
636 
637 void C4StartupModsDownloader::ModInfo::CancelRequest()
638 {
639  if (!postClient.get()) return;
640  Application.InteractiveThread.RemoveProc(postClient.get());
641  postClient.reset();
642 }
643 
644 std::string C4StartupModsDownloader::ModInfo::GetPath()
645 {
646  return std::string(Config.General.UserDataPath) + "mods" + DirectorySeparator + \
647  modID + "_" + slug;
648 }
649 
650 void C4StartupModsDownloader::ModInfo::CheckProgress()
651 {
652  // Determining success or starting a new download.
653  if (HasError()) return;
654  if (files.empty())
655  successful = true;
656  if (successful) return;
657 
658  if (postClient.get() == nullptr) // Start new file?
659  {
660  postClient = std::make_unique<C4HTTPClient>();
661 
662  if (!postClient->Init() || !postClient->SetServer((C4StartupModsDlg::GetBaseServerURL() + "files/" + files.back().handle).c_str()))
663  {
664  assert(false);
665  return;
666  }
667  postClient->SetExpectedResponseType(C4HTTPClient::ResponseType::NoPreference);
668 
669  // Do the actual request.
670  postClient->SetNotify(&Application.InteractiveThread);
671  Application.InteractiveThread.AddProc(postClient.get());
672  postClient->Query(nullptr, true); // Empty query for binary data.
673  }
674 
675  // Update progress bar.
676  // And add the expected size of all remaining and finished files.
677  downloadedBytes = postClient->getDownloadedSize() + totalSuccesfullyDownloadedBytes;
678  totalBytes = totalSuccesfullyDownloadedBytes;
679  for (const auto &file : files)
680  totalBytes += file.size;
681 
682  if (!postClient->isBusy())
683  {
684  if (!postClient->isSuccess())
685  {
686  errorMessage = std::string(LoadResStr("IDS_MODS_NOINSTALL_CONNECTIONFAIL")) + postClient->GetError();
687  CancelRequest();
688  return;
689  }
690  else
691  {
692  const std::string path = GetPath();
693  if (!CreatePath(path))
694  {
695  errorMessage = LoadResStr("IDS_MODS_NOINSTALL_CREATEDIR");
696  CancelRequest();
697  return;
698  }
699 
700  std::ofstream os(path + DirectorySeparator + files.back().name, std::iostream::out | std::iostream::binary);
701  if (!os.good())
702  {
703  errorMessage = LoadResStr("IDS_MODS_NOINSTALL_CREATEFILE");
704  CancelRequest();
705  return;
706  }
707 
708  const auto newDownloadedBytes = postClient->getDownloadedSize();;
709  totalSuccesfullyDownloadedBytes += newDownloadedBytes;
710  os.write(static_cast<const char*>(postClient->getResultBin().getData()), newDownloadedBytes);
711  os.close();
712 
713  CancelRequest();
714 
715  files.pop_back();
716  if (files.empty())
717  {
718  // Write mod metadata to info file - to bad we don't use tinyxml with STL support.
719  FILE *metadata = std::fopen((path + DirectorySeparator + "resource.xml").c_str(), "w");
720  if (metadata != nullptr)
721  {
722  originalXMLNode->Print(metadata, 0);
723  std::fclose(metadata);
724  successful = true;
725 
726  // Now clean up all files that don't belong to this mod.
727  // This can be necessary if e.g. filenames change over updates.
728  for (DirectoryIterator iter(path.c_str()); *iter; ++iter)
729  {
730  const std::string filename(*iter);
731  // No folders.
732  if (DirectoryExists(filename.c_str())) continue;
733  // Safety: touch only a special set of file endings.
734  const std::string::size_type typeIndex = filename.rfind(".");
735  if (typeIndex == std::string::npos) continue;
736  const std::string ending(filename.substr(typeIndex + 1));
737  if (ending != "ocd" && ending != "ocf" && ending != "ocs") continue;
738  // In the required files anyway?
739  const std::string::size_type leafIndex = filename.rfind(DirectorySeparator);
740  std::string leaf(filename);
741  if (leafIndex != std::string::npos)
742  leaf = filename.substr(leafIndex + 1);
743  if (requiredFilenames.count(leaf) > 0) continue;
744  EraseFile(filename.c_str());
745  }
746  }
747  }
748  return;
749  }
750  }
751 }
752 
753 void C4StartupModsDownloader::ExecuteCheckDownloadProgress()
754 {
755  // Not even progressing yet?
756  if (progressDialog == nullptr) return;
757 
758  if (progressDialog->IsAborted())
759  {
760  CancelRequest();
761  return;
762  }
763 
764  // Let mods check their progress.
765  size_t downloadedBytes{ 0 }, totalBytes{ 0 };
766 
767  bool anyNotFinished = false;
768 
769  for (auto & mod : items)
770  {
771  mod->CheckProgress();
772  size_t downloaded, total;
773  std::tie(downloaded, total) = mod->GetProgress();
774 
775  downloadedBytes += downloaded;
776  totalBytes += total;
777 
778  if (mod->IsBusy() || (!mod->HasError() && mod->HasFilesRemaining()))
779  {
780  anyNotFinished = true;
781  }
782  }
783 
784  if (totalBytes)
785  progressDialog->SetProgress(100 * downloadedBytes / totalBytes);
786 
787  // All done?
788  if (!anyNotFinished)
789  {
790  // Report errors (all in one).
791  std::string errorMessage;
792  for (auto & mod : items)
793  {
794  if (mod->WasSuccessful())
795  {
796  parent->modsDiscovery.AddMod(mod->modID, mod->GetPath(), mod->name);
797  parent->QueueSyncWithDiscovery();
798  }
799  const std::string modError = mod->GetErrorMessage();
800  if (!modError.empty())
801  errorMessage += "|" + modError;
802  }
803 
804  CancelRequest();
805 
806  if (!errorMessage.empty())
807  {
808  ::pGUI->ShowMessageModal(errorMessage.c_str(), LoadResStr("IDS_MODS_NOINSTALL"), C4GUI::MessageDialog::btnOK, C4GUI::Ico_Error);
809  }
810  return;
811  }
812 }
813 
814 void C4StartupModsDownloader::ExecuteMetadataUpdate()
815 {
816  if (!progressDialog) return;
817 
818  if (progressDialog->IsAborted())
819  {
820  CancelRequest();
821  return;
822  }
823 
824  // Start querying new metadata?
825  if (!postMetadataClient)
826  {
827  // Find first mod that requires an update.
828  for (size_t i = static_cast<size_t>(metadataQueriedForModIdx + 1); i < items.size(); ++i)
829  {
830  auto &mod = items[i];
831  if (!mod->RequiresMetadataUpdate() || mod->HasError()) continue;
832 
833  StdStrBuf progressMessage;
834  progressMessage.Format(LoadResStr("IDS_MODS_INSTALL_UPDATEMETADATA_FOR"), mod->name.c_str());
835  progressDialog->SetMessage(progressMessage.getData());
836 
837  postMetadataClient = std::make_unique<C4HTTPClient>();
838 
839  if (!postMetadataClient->Init() || !postMetadataClient->SetServer((C4StartupModsDlg::GetBaseServerURL() + "uploads/" + mod->modID).c_str()))
840  {
841  mod->SetError(LoadResStr("IDS_MODS_NOINSTALL_UPDATEMETADATAFAILED"));
842  postMetadataClient.reset();
843  continue;
844  }
845  postMetadataClient->SetExpectedResponseType(C4HTTPClient::ResponseType::XML);
846  // Do the actual request.
847  Application.InteractiveThread.AddProc(postMetadataClient.get());
848  postMetadataClient->Query(nullptr, false); // Empty query.
849 
850  metadataQueriedForModIdx = i;
851  return;
852  }
853  // Nothing to be updated found? Great, give execution back.
854  progressDialog->SetProgress(100);
855  progressCallback = std::bind(&C4StartupModsDownloader::ExecutePreRequestChecks, this);
856  return;
857  }
858 
859  // We are already running a query!
860  assert(metadataQueriedForModIdx >= 0);
861  assert(metadataQueriedForModIdx < items.size());
862  auto &mod = items[metadataQueriedForModIdx];
863  // Check whether the data has arrived yet.
864  if (!postMetadataClient->isBusy())
865  {
866  if (!postMetadataClient->isSuccess())
867  {
868  Log(postMetadataClient->GetError());
869  // Destroy client and try next mod.
870  Application.InteractiveThread.RemoveProc(postMetadataClient.get());
871  postMetadataClient.reset();
872 
873  mod->SetError(LoadResStr("IDS_MODS_NOINSTALL_UPDATEMETADATAFAILED"));
874  return;
875  }
876 
877  TiXmlDocument xmlDocument;
878  xmlDocument.Parse(postMetadataClient->getResultString());
879  std::cout << postMetadataClient->getResultString();
880 
881  if (xmlDocument.Error())
882  {
883  Log(xmlDocument.ErrorDesc());
884  CancelRequest();
885  ::pGUI->ShowMessageModal(LoadResStr("IDS_MODS_NOINSTALL_UPDATEMETADATAFAILED"), LoadResStr("IDS_MODS_NOINSTALL"), C4GUI::MessageDialog::btnOK, C4GUI::Ico_Resource);
886  return;
887  }
888  const char * resourceElementName = "root";
889  const TiXmlElement *root = xmlDocument.RootElement();
890  assert(strcmp(root->Value(), resourceElementName) == 0);
891 
892  // Re-use the parsing from the list entries.
893  ModXMLData modXMLData(root, ModXMLData::Source::DetailView);
894 
895  // Empty reply? Skip.
896  if (modXMLData.id.empty())
897  {
898  mod->SetError(LoadResStr("IDS_MODS_NOINSTALL_UPDATEMETADATAFAILED"));
899  // Destroy client and try next mod.
900  Application.InteractiveThread.RemoveProc(postMetadataClient.get());
901  postMetadataClient.reset();
902  return;
903  }
904 
905  // Find the mod matching the id from the metadata.
906  size_t foundIdx = 0;
907  for (;foundIdx < items.size(); ++foundIdx)
908  {
909  auto &mod = items[foundIdx];
910  if (mod->modID != modXMLData.id) continue;
911  mod->FromXMLData(modXMLData);
912  break;
913  }
914 
915  progressDialog->SetProgress(100 * foundIdx / items.size());
916 
917  // Somehow, the matching mod could not be found. That should not happen.
918  if (foundIdx == items.size())
919  {
920  Log((std::string("Answer for ") + modXMLData.id + " was not requested.").c_str());
921  CancelRequest();
922  ::pGUI->ShowMessageModal(LoadResStr("IDS_MODS_NOINSTALL_UPDATEMETADATAFAILED"), LoadResStr("IDS_MODS_NOINSTALL"), C4GUI::MessageDialog::btnOK, C4GUI::Ico_Resource);
923  assert(false);
924  return;
925  }
926  else
927  {
928  // The mod might have some additional dependencies that need to be retrieved.
929  const std::unique_ptr<C4StartupModsDownloader::ModInfo> &mod = items[foundIdx];
930 
931  for (const std::string &dependency : mod->dependencies)
932  AddModToQueue(dependency, dependency);
933  }
934 
935  Application.InteractiveThread.RemoveProc(postMetadataClient.get());
936  postMetadataClient.reset();
937  }
938 }
939 
941 {
942  progressCallback = std::bind(&C4StartupModsDownloader::ExecutePreRequestChecks, this);
943 }
944 
945 void C4StartupModsDownloader::ExecutePreRequestChecks()
946 {
947  // Disable callback during execution.
948  progressCallback = nullptr;
949 
950  // In case some of the mods need an information update, do that first.
951  for (auto &mod : items)
952  {
953  if (!mod->RequiresMetadataUpdate() || mod->HasError()) continue;
954  progressCallback = std::bind(&C4StartupModsDownloader::ExecuteMetadataUpdate, this);
955  GetProgressDialog()->SetTitle(LoadResStr("IDS_MODS_INSTALL_UPDATEMETADATA"));
956  return;
957  }
958  // To be able to check against the installed mods, the discovery needs to be finished.
959  parent->modsDiscovery.WaitForDiscoveryFinished();
960 
961  // Check all local files for compatibility.
962  bool anyModNeedsCheck = false;
963  for (auto &mod : items)
964  {
965  if (!mod->localDiscoveryCheck.needsCheck) continue;
966 
967  const bool modInstalled = mod->localDiscoveryCheck.installed = parent->modsDiscovery.IsModInstalled(mod->modID);
968  mod->localDiscoveryCheck.basePath = modInstalled ? parent->modsDiscovery.GetModInformation(mod->modID).path : "";
969  mod->localDiscoveryCheck.needsCheck = false;
970 
971  if (modInstalled)
972  {
973 
974  for (auto file : mod->files)
975  {
976  if (file.sha1.empty()) continue;
977  mod->localDiscoveryCheck.needsCheck = anyModNeedsCheck = true;
978  break;
979  }
980 
981  if (mod->localDiscoveryCheck.needsCheck)
982  {
983  mod->localDiscoveryCheck.Start();
984  }
985  }
986  }
987 
988  if (anyModNeedsCheck)
989  {
990  progressCallback = std::bind(&C4StartupModsDownloader::ExecuteWaitForChecksums, this);
991  GetProgressDialog()->SetTitle(LoadResStr("IDS_MODS_INSTALL_CHECKFILES"));
992  GetProgressDialog()->SetMessage("");
993  GetProgressDialog()->SetProgress(0);
994  }
995  else
996  {
997  // Just enter it directly.
998  ExecuteRequestConfirmation();
999  }
1000 }
1001 
1002 void C4StartupModsDownloader::ExecuteWaitForChecksums()
1003 {
1004  for (auto &mod : items)
1005  {
1006  if (mod->localDiscoveryCheck.needsCheck)
1007  return;
1008  }
1009  progressCallback = std::bind(&C4StartupModsDownloader::ExecuteRequestConfirmation, this);
1010 }
1011 
1012 void C4StartupModsDownloader::ExecuteRequestConfirmation()
1013 {
1014  progressCallback = nullptr;
1015 
1016  // Calculate total filesize to be downloaded.
1017  size_t totalSize{ 0 };
1018  bool atLeastOneFileExisted = false;
1019  std::string allMissingModNames;
1020  std::string errorMessages;
1021 
1022  for (auto &mod : items)
1023  {
1024  if (mod->localDiscoveryCheck.atLeastOneFileExisted)
1025  atLeastOneFileExisted = true;
1026  if (!mod->files.empty())
1027  {
1028  allMissingModNames += (allMissingModNames.empty() ? "\"" : ", \"") + mod->name + "\"";
1029  for (auto file : mod->files)
1030  {
1031  totalSize += file.size;
1032  }
1033  }
1034  if (mod->HasError())
1035  errorMessages += mod->GetErrorMessage() + "\n";
1036  }
1037 
1038  // Hide progress bar, so it's not behind the modal dialogs.
1039  if (progressDialog)
1040  progressDialog->SetVisibility(false);
1041 
1042  if (totalSize == 0)
1043  {
1044  CancelRequest();
1045  auto icon = C4GUI::Ico_Resource;
1046  if (!errorMessages.empty())
1047  errorMessages += "\n";
1048  if (atLeastOneFileExisted)
1049  {
1050  errorMessages += LoadResStr("IDS_MODS_NOINSTALL_ALREADYINSTALLED");
1051  }
1052  else
1053  {
1054  errorMessages += LoadResStr("IDS_MODS_NOINSTALL_NODATA");
1055  icon = C4GUI::Ico_Error;
1056  }
1057  ::pGUI->ShowMessageModal(errorMessages.c_str(), LoadResStr("IDS_MODS_NOINSTALL"), C4GUI::MessageDialog::btnOK, icon);
1058  return;
1059  }
1060 
1061  std::string filesizeString;
1062  const size_t totalSizeMB = (totalSize / 1000 + 500) / 1000;
1063  if (totalSizeMB == 0)
1064  {
1065  filesizeString = "< 1 MB";
1066  }
1067  else
1068  {
1069  filesizeString = std::string("~ ") + std::to_string(totalSizeMB) + " MB";
1070  }
1071 
1072  StdStrBuf confirmationMessage;
1073  confirmationMessage.Format(LoadResStr("IDS_MODS_INSTALL_CONFIRM"), allMissingModNames.c_str(), filesizeString.c_str());
1075  auto *dialog = new C4GUI::ConfirmationDialog(confirmationMessage.getData(), LoadResStr("IDS_MODS_INSTALL_CONFIRM_TITLE"), callbackHandler, C4GUI::MessageDialog::btnYesNo, false, C4GUI::Icons::Ico_Save);
1076  parent->GetScreen()->ShowRemoveDlg(dialog);
1077 }
1078 
1080 {
1081  assert(installed);
1082 
1083  for (auto fileIterator = mod.files.begin(); fileIterator != mod.files.end();)
1084  {
1085  auto &file = *fileIterator;
1086  bool fileExists = false;
1087 
1088  // Check if the file already exists.
1089  if (!file.sha1.empty())
1090  {
1091  const std::string &hashString = file.sha1;
1092  BYTE hash[SHA_DIGEST_LENGTH];
1093  const std::string filePath = basePath + DirectorySeparator + file.name;
1094  if (GetFileSHA1(filePath.c_str(), hash))
1095  {
1096  fileExists = true;
1097 
1098  // Match hashes (string against byte array).
1099  const size_t byteLen = 2;
1100  size_t index = 0;
1101  for (size_t offset = 0; offset < hashString.size(); offset += byteLen, index += 1)
1102  {
1103  // Oddly, the indices of the byte array do not correspond 1-to-1 to a standard sha1 string.
1104  const size_t hashIndex = (index / 4 * 4) + (3 - (index % 4));
1105  const BYTE &byte = hash[hashIndex];
1106 
1107  const std::string byteStr = hashString.substr(offset, byteLen);
1108  unsigned char byteStrValue = static_cast<unsigned char> (std::stoi(byteStr, nullptr, 16));
1109 
1110  if (byteStrValue != byte)
1111  {
1112  fileExists = false;
1113  break;
1114  }
1115  }
1116  }
1117  }
1118 
1119  if (fileExists)
1120  {
1121  fileIterator = mod.files.erase(fileIterator);
1122  atLeastOneFileExisted = true;
1123  }
1124  else
1125  {
1126  ++fileIterator;
1127  }
1128  }
1129 
1130  // Fire-once check done.
1131  needsCheck = false;
1133 }
1134 
1135 
1136 // ----------- C4StartupNetDlg ---------------------------------------------------------------------------------
1137 
1138 C4StartupModsDlg::C4StartupModsDlg() : C4StartupDlg(LoadResStr("IDS_DLG_MODS")), modsDiscovery(this)
1139 {
1140  // ctor
1141  // key bindings
1142  C4CustomKey::CodeList keys;
1143  keys.push_back(C4KeyCodeEx(K_BACK)); keys.push_back(C4KeyCodeEx(K_LEFT));
1144  pKeyBack = new C4KeyBinding(keys, "StartupNetBack", KEYSCOPE_Gui,
1146  pKeyRefresh = new C4KeyBinding(C4KeyCodeEx(K_F5), "StartupNetReload", KEYSCOPE_Gui,
1148 
1149  // screen calculations
1150  UpdateSize();
1151  int32_t iIconSize = C4GUI_IconExWdt;
1152  int32_t iButtonWidth,iCaptionFontHgt, iSideSize = std::max<int32_t>(GetBounds().Wdt/6, iIconSize);
1153  int32_t iButtonHeight = C4GUI_ButtonHgt, iButtonIndent = GetBounds().Wdt/40;
1154  ::GraphicsResource.CaptionFont.GetTextExtent("<< BACK", iButtonWidth, iCaptionFontHgt, true);
1155  iButtonWidth *= 3;
1156  C4GUI::ComponentAligner caMain(GetClientRect(), 0,0, true);
1157  C4GUI::ComponentAligner caButtonArea(caMain.GetFromBottom(caMain.GetHeight()/7),0,0);
1158  int32_t iButtonAreaWdt = caButtonArea.GetWidth()*7/8;
1159  iButtonWidth = std::min<int32_t>(iButtonWidth, (iButtonAreaWdt - 8 * iButtonIndent)/4);
1160  iButtonIndent = (iButtonAreaWdt - 4 * iButtonWidth) / 8;
1161  C4GUI::ComponentAligner caButtons(caButtonArea.GetCentered(iButtonAreaWdt, iButtonHeight),iButtonIndent,0);
1162  C4GUI::ComponentAligner caLeftBtnArea(caMain.GetFromLeft(iSideSize), std::min<int32_t>(caMain.GetWidth()/20, (iSideSize-C4GUI_IconExWdt)/2), caMain.GetHeight()/40);
1163  C4GUI::ComponentAligner caConfigArea(caMain.GetFromRight(iSideSize), std::min<int32_t>(caMain.GetWidth()/20, (iSideSize-C4GUI_IconExWdt)/2), caMain.GetHeight()/40);
1164 
1165  // main area: Tabular to switch between game list and chat
1166  pMainTabular = new C4GUI::Tabular(caMain.GetAll(), C4GUI::Tabular::tbNone);
1167  pMainTabular->SetDrawDecoration(false);
1168  pMainTabular->SetSheetMargin(0);
1169  AddElement(pMainTabular);
1170 
1171  // main area: game selection sheet
1172  C4GUI::Tabular::Sheet *pSheetGameList = pMainTabular->AddSheet(nullptr);
1173  C4GUI::ComponentAligner caGameList(pSheetGameList->GetContainedClientRect(), 0,0, false);
1175  pGameListLbl = new C4GUI::WoodenLabel(LoadResStr("IDS_MODS_MODSLIST"), caGameList.GetFromTop(iCaptHgt), C4GUI_Caption2FontClr, &::GraphicsResource.TextFont, ALeft);
1176  pSheetGameList->AddElement(pGameListLbl);
1177 
1178  // precalculate space needed for sorting labels
1179  int32_t maxSortLabelWidth = 0;
1180 
1181  sortingOptions =
1182  {
1183  { "title", "IDS_MODS_SORT_NAME_UP", "IDS_MODS_SORT_NAME_DOWN" },
1184  { "updatedAt", "IDS_MODS_SORT_DATE_DOWN", "IDS_MODS_SORT_DATE_UP" },
1185  };
1186  // Translate all labels.
1187  for (auto &option : sortingOptions)
1188  {
1189  int32_t iSortWdt = 100, iSortHgt;
1190  for (auto label : { &SortingOption::titleAsc, &SortingOption::titleDesc })
1191  {
1192  option.*label = LoadResStr(option.*label);
1193  // Get width of label and remember if it's the longest yet.
1194  ::GraphicsResource.TextFont.GetTextExtent(option.*label, iSortWdt, iSortHgt, true);
1195  if (iSortWdt > maxSortLabelWidth)
1196  maxSortLabelWidth = iSortWdt;
1197  }
1198  }
1199 
1200  // search field
1201  C4GUI::WoodenLabel *pSearchLbl;
1202  const char *szSearchLblText = LoadResStr("IDS_NET_MSSEARCH"); // Text is the same as in the network view.
1203  int32_t iSearchWdt=100, iSearchHgt;
1204  ::GraphicsResource.TextFont.GetTextExtent(szSearchLblText, iSearchWdt, iSearchHgt, true);
1205  C4GUI::ComponentAligner caSearch(caGameList.GetFromTop(iSearchHgt), 0,0);
1206  pSearchLbl = new C4GUI::WoodenLabel(szSearchLblText, caSearch.GetFromLeft(iSearchWdt+10), C4GUI_Caption2FontClr, &::GraphicsResource.TextFont);
1207  const char *szSearchTip = LoadResStr("IDS_MODS_SEARCH_DESC");
1208  pSearchLbl->SetToolTip(szSearchTip);
1209  pSheetGameList->AddElement(pSearchLbl);
1210  pSearchFieldEdt = new C4GUI::CallbackEdit<C4StartupModsDlg>(caSearch.GetFromLeft(caSearch.GetWidth() - maxSortLabelWidth - 40), this, &C4StartupModsDlg::OnSearchFieldEnter);
1211  pSearchFieldEdt->SetToolTip(szSearchTip);
1212  pSheetGameList->AddElement(pSearchFieldEdt);
1213 
1214  // Sorting options
1215  C4GUI::ComponentAligner caSorting(caSearch.GetAll(), 0, 0);
1216  auto pSortComboBox = new C4GUI::ComboBox(caSearch.GetAll());
1218  pSortComboBox->SetText(LoadResStr("IDS_MODS_SORT"));
1219  pSheetGameList->AddElement(pSortComboBox);
1220 
1221  pGameSelList = new C4GUI::ListBox(caGameList.GetFromTop(caGameList.GetHeight() - iCaptHgt));
1222  pGameSelList->SetDecoration(true, nullptr, true, true);
1223  pGameSelList->UpdateElementPositions();
1226  pSheetGameList->AddElement(pGameSelList);
1227 
1228  // button area
1230  AddElement(btn = new C4GUI::CallbackButton<C4StartupModsDlg>(LoadResStr("IDS_BTN_BACK"), caButtons.GetFromLeft(iButtonWidth), &C4StartupModsDlg::OnBackBtn));
1231  btn->SetToolTip(LoadResStr("IDS_DLGTIP_BACKMAIN"));
1232  AddElement(btnInstall = new C4GUI::CallbackButton<C4StartupModsDlg>(LoadResStr("IDS_MODS_INSTALL"), caButtons.GetFromLeft(iButtonWidth), &C4StartupModsDlg::OnInstallModBtn));
1233  btnInstall->SetToolTip(LoadResStr("IDS_MODS_INSTALL_DESC"));
1234  AddElement(btnRemove = new C4GUI::CallbackButton<C4StartupModsDlg>(LoadResStr("IDS_MODS_UNINSTALL"), caButtons.GetFromLeft(iButtonWidth), &C4StartupModsDlg::OnUninstallModBtn));
1235  btnRemove->SetToolTip(LoadResStr("IDS_MODS_UNINSTALL_DESC"));
1236  AddElement(btn = new C4GUI::CallbackButton<C4StartupModsDlg>(LoadResStr("IDS_MODS_UPDATEALL"), caButtons.GetFromLeft(iButtonWidth), &C4StartupModsDlg::OnUpdateAllBtn));
1237  btn->SetToolTip(LoadResStr("IDS_MODS_UPDATEALL_DESC"));
1238 
1239  // Left button area.
1240  buttonShowInstalled = new C4GUI::CallbackButton<C4StartupModsDlg, C4GUI::IconButton>(C4GUI::Ico_Save, caLeftBtnArea.GetFromTop(iIconSize, iIconSize), '\0', &C4StartupModsDlg::OnShowInstalledBtn);
1241  buttonShowInstalled->SetToolTip(LoadResStr("IDS_MODS_SHOWINSTALLED_DESC"));
1242  buttonShowInstalled->SetText(LoadResStr("IDS_MODS_SHOWINSTALLED"));
1243  AddElement(buttonShowInstalled);
1244 
1245  const auto showInstalledHint = LoadResStr("IDS_MODS_SHOWINSTALLED_HINT");
1246  StdStrBuf showInstalledHintBroken;
1247  const int32_t rawTextHeight = ::GraphicsResource.FontRegular.BreakMessage(showInstalledHint, caLeftBtnArea.GetWidth(), &showInstalledHintBroken, true);
1248  auto installedInfo = new C4GUI::Label(showInstalledHintBroken.getData(), caLeftBtnArea.GetFromTop(rawTextHeight), ACenter);
1249  AddElement(installedInfo);
1250 
1251  // Right button area.
1252  {
1253  auto filterLabel = new C4GUI::Label(LoadResStr("IDS_MODS_FILTER"), caConfigArea.GetFromTop(iSearchHgt), ALeft, C4GUI_Caption2FontClr, &::GraphicsResource.CaptionFont);
1254  AddElement(filterLabel);
1255  }
1256  {
1257  CStdFont *pUseFont = &(C4Startup::Get()->Graphics.BookFont);
1258  auto addCheckbox = [&pUseFont, &caConfigArea, this] (C4GUI::CheckBox**checkbox, const char *szText, const char *szTooltip, const C4GUI::Icons icon)
1259  {
1260  int iWdt = 150, iHgt = 12;
1261  C4GUI::CheckBox::GetStandardCheckBoxSize(&iWdt, &iHgt, szText, pUseFont);
1262  *checkbox = new C4GUI::CheckBox(caConfigArea.GetFromTop(iHgt, -1), szText, true);
1263  (*checkbox)->SetToolTip(szTooltip);
1264  AddElement(*checkbox);
1265  const auto& checkbox_bounds = (*checkbox)->GetBounds();
1266  const int32_t line_height = ::GraphicsResource.TextFont.GetLineHeight();
1267  C4Rect icon_rect(checkbox_bounds.GetRight(), checkbox_bounds.GetTop(), line_height, line_height);
1268  auto icon_element = new C4GUI::Icon(icon_rect, icon);
1269  AddElement(icon_element);
1270  };
1271  addCheckbox(&filters.showCompatible, LoadResStr("IDS_MODS_FILTER_COMPATIBLE"), LoadResStr("IDS_MODS_FILTER_COMPATIBLE_DESC"), C4GUI::Ico_Clonk);
1272  addCheckbox(&filters.showPlayable, LoadResStr("IDS_MODS_FILTER_PLAYABLE"), LoadResStr("IDS_MODS_FILTER_PLAYABLE_DESC"), C4GUI::Ico_Gfx);
1273  }
1274  // Add button to manually search the database (as opposed to hitting F5).
1275  AddElement(btn = new C4GUI::CallbackButton<C4StartupModsDlg>(LoadResStr("IDS_MODS_SEARCH_ONLINE"), caConfigArea.GetFromTop(iButtonHeight), &C4StartupModsDlg::OnSearchOnlineBtn));
1276  btn->SetToolTip(LoadResStr("IDS_MODS_SEARCH_ONLINE_DESC"));
1277  // initial focus
1278  SetFocus(GetDlgModeFocusControl(), false);
1279 
1280  // register timer
1281  Application.Add(this);
1282 
1283  // register as receiver of reference notifies
1285 
1286 }
1287 
1288 std::string C4StartupModsDlg::GetBaseServerURL()
1289 {
1290  std::string base = Config.Network.GetModDatabaseServerAddress();
1291  assert(!base.empty());
1292  if (base.empty()) return base;
1293  if (base.back() != '/')
1294  base += "/";
1295  return base;
1296 }
1297 
1298 std::string C4StartupModsDlg::GetOpenClonkVersionStringTag()
1299 {
1300  return "openclonk-" + std::to_string(C4XVER1);
1301 }
1302 
1304 {
1305  CancelRequest();
1306  // disable notifies
1308 
1309  Application.Remove(this);
1310  // dtor
1311  delete pKeyBack;
1312  delete pKeyRefresh;
1313 }
1314 
1316 {
1317  // draw background
1318  typedef C4GUI::FullscreenDialog Base;
1319  Base::DrawElement(cgo);
1320 }
1321 
1323 {
1324  // callback when shown: Start searching for games
1326  QueryModList();
1327  OnSec1Timer();
1328 }
1329 
1331 {
1332  // dlg abort: return to main screen
1333  CancelRequest();
1334  if (!fOK) DoBack();
1335 }
1336 
1338 {
1339  return nullptr;
1340 }
1341 
1343 {
1344  return pGameSelList;
1345 }
1346 
1347 void C4StartupModsDlg::QueryModList(bool loadNextPage)
1348 {
1349  buttonShowInstalled->SetHighlight(false);
1350 
1351  C4StartupModsListEntry *infoEntry{ nullptr };
1352  // New page requested? Leave the list as-is.
1353  if (loadNextPage && pGameSelList->GetLast() != nullptr)
1354  {
1355  infoEntry = static_cast<C4StartupModsListEntry*> (pGameSelList->GetLast());
1356  }
1357  else
1358  {
1359  // Clear the list and add an info entry.
1360  ClearList();
1361  infoEntry = new C4StartupModsListEntry(pGameSelList, nullptr, this);
1362  }
1363  infoEntry->MakeInfoEntry();
1364 
1365  // First, construct the 'where' part of the query.
1366  // Forward the filter-field to the server.
1367  std::string searchQueryPostfix("?");
1368  if (pSearchFieldEdt->GetText())
1369  {
1370  std::string searchText(pSearchFieldEdt->GetText());
1371  if (searchText.size() > 0)
1372  {
1373  // Sanity, escape quotes etc.
1374  searchText = std::regex_replace(searchText, std::regex("\""), "\\\"");
1375  searchText = std::regex_replace(searchText, std::regex("[ ]+"), "%20");
1376  searchQueryPostfix += "q=%22" + searchText + "%22&";
1377  }
1378  }
1379 
1380  std::vector<std::string> tagFilters;
1381  tagFilters.reserve(4);
1382  // Additional filter options set?
1383  if (filters.showCompatible->GetChecked())
1384  {
1385  static const std::string versionTag = GetOpenClonkVersionStringTag();
1386  tagFilters.push_back(versionTag);
1387  }
1388  if (filters.showPlayable->GetChecked())
1389  {
1390  tagFilters.push_back(".scenario");
1391  }
1392  if (!tagFilters.empty())
1393  {
1394  std::ostringstream os;
1395  for (size_t i = 0; i < tagFilters.size(); ++i)
1396  os << ((i == 0) ? "tags=" : ",") << tagFilters[i];
1397  searchQueryPostfix += os.str() + "&";
1398  }
1399 
1400  // Forward the sorting criterion to the server.
1401  if (!sortKeySuffix.empty())
1402  {
1403  searchQueryPostfix += "sort=" + sortKeySuffix + "&";
1404  }
1405 
1406  // Request the correct page.
1407  const int requestedOffset = loadNextPage ? pageInfo.currentlySkipped + pageInfo.maxResultsPerQuery: 0;
1408  searchQueryPostfix += "limit=" + std::to_string(pageInfo.maxResultsPerQuery) + "&skip=" + std::to_string(requestedOffset) + "&";
1409 
1410  // Initialize connection.
1411  // Abort possible running request.
1412  CancelRequest();
1413  queryWasSuccessful = false;
1414  postClient = std::make_unique<C4HTTPClient>();
1415 
1416  if (!postClient->Init() || !postClient->SetServer((C4StartupModsDlg::GetBaseServerURL() + "uploads" + searchQueryPostfix).c_str()))
1417  {
1418  infoEntry->OnError(std::string(LoadResStr("IDS_MODS_INVALID_SERVER")) + C4StartupModsDlg::GetBaseServerURL());
1419  postClient.reset();
1420  // Don't retry.
1421  queryWasSuccessful = true;
1422  return;
1423  }
1424  postClient->SetExpectedResponseType(C4HTTPClient::ResponseType::XML);
1425 
1426  // Do the actual request.
1427  postClient->SetNotify(&Application.InteractiveThread);
1428  Application.InteractiveThread.AddProc(postClient.get());
1429  postClient->Query(nullptr, false); // Empty query.
1430 }
1431 
1432 void C4StartupModsDlg::CancelRequest()
1433 {
1434  if (!postClient.get()) return;
1435  Application.InteractiveThread.RemoveProc(postClient.get());
1436  postClient.reset();
1437  lastQueryEndTime = time(nullptr);
1438 }
1439 
1440 void C4StartupModsDlg::ClearList()
1441 {
1442  C4GUI::Element *pElem, *pNextElem = pGameSelList->GetFirst();
1443  while ((pElem = pNextElem))
1444  {
1445  pNextElem = pElem->GetNext();
1446  C4StartupModsListEntry *pEntry = static_cast<C4StartupModsListEntry *>(pElem);
1447  delete pEntry;
1448  }
1449 }
1450 
1451 void C4StartupModsDlg::AddToList(std::vector<TiXmlElementLoaderInfo> elements, ModXMLData::Source source)
1452 {
1453  const bool modsDiscoveryFinished = modsDiscovery.IsDiscoveryFinished();
1454  for (const auto e : elements)
1455  {
1456  C4StartupModsListEntry *pEntry = new C4StartupModsListEntry(pGameSelList, nullptr, this);
1457  pEntry->FromXML(e.element, source, e.id, e.name);
1458 
1459  if (modsDiscoveryFinished && modsDiscovery.IsModInstalled(pEntry->GetID()))
1460  {
1461  C4StartupModsLocalModDiscovery::ModsInfo mod = modsDiscovery.GetModInformation(pEntry->GetID());
1462  pEntry->UpdateInstalledState(&mod);
1463  }
1464  else
1465  {
1466  pEntry->UpdateInstalledState(nullptr);
1467  }
1468  }
1469 }
1470 
1472 {
1473  if (!buttonShowInstalled->GetHighlight())
1474  UpdateList(false, true);
1475  else
1476  QueryModList();
1477 }
1478 
1479 void C4StartupModsDlg::UpdateList(bool fGotReference, bool onlyWithLocalFiles)
1480 {
1481  if (onlyWithLocalFiles)
1482  {
1483  buttonShowInstalled->SetHighlight(true);
1484 
1485  if (postClient.get() != nullptr)
1486  CancelRequest();
1487 
1488  ClearList();
1489  modsDiscovery.WaitForDiscoveryFinished();
1490 
1491  // Load XML from mod files.
1492  auto lock = std::move(modsDiscovery.Lock());
1493  auto installedMods = modsDiscovery.GetAllModInformation();
1494 
1495  std::vector<TiXmlElementLoaderInfo> elements;
1496  for (const auto & modData: installedMods)
1497  {
1498  const auto &mod = modData.second;
1499  TiXmlElement *metadataXMLElement{ nullptr };
1500 
1501  // Try to read the metadata file. Show elements without file, though.
1502  std::ifstream metadata(mod.path + DirectorySeparator + "resource.xml", std::ifstream::in);
1503  if (metadata.good())
1504  {
1505  std::stringstream stream;
1506  stream << metadata.rdbuf();
1507 
1508  TiXmlDocument xmlDocument;
1509  xmlDocument.Parse(stream.str().c_str());
1510  if (!xmlDocument.Error())
1511  {
1512  const TiXmlElement *root = xmlDocument.RootElement();
1513  metadataXMLElement = static_cast<TiXmlElement*>(root->Clone());
1514  }
1515  }
1516  elements.emplace_back(metadataXMLElement, mod.id, mod.name);
1517  }
1518  AddToList(elements, ModXMLData::Source::Local);
1519  // We took ownership, clear it.
1520  for (auto & e : elements)
1521  delete e.element;
1522 
1523  if (elements.empty())
1524  {
1525  C4StartupModsListEntry *infoEntry = new C4StartupModsListEntry(pGameSelList, nullptr, this);
1526  infoEntry->MakeInfoEntry();
1527  infoEntry->OnNoResultsFound();
1528  }
1529  // Updating the local files is always successful. Prevent automatic retries.
1530  queryWasSuccessful = true;
1531  }
1532 
1533  // Already running a query?
1534  if (postClient.get() != nullptr)
1535  {
1536  // Check whether the data has arrived yet.
1537  if (!postClient->isBusy())
1538  {
1539  // At this point we can assert that the info field is the last entry in the list.
1540  C4StartupModsListEntry *infoEntry = static_cast<C4StartupModsListEntry*> (pGameSelList->GetLast());
1541  assert(infoEntry != nullptr);
1542 
1543  if (!postClient->isSuccess())
1544  {
1545  Log(postClient->GetError());
1546  infoEntry->OnError(postClient->GetError());
1547  // Destroy client and try again later.
1548  CancelRequest();
1549  return;
1550  }
1551  queryWasSuccessful = true;
1552 
1553  TiXmlDocument xmlDocument;
1554  xmlDocument.Parse(postClient->getResultString());
1555  std::cout << postClient->getResultString() << std::endl;
1556 
1557  if (xmlDocument.Error())
1558  {
1559  Log(xmlDocument.ErrorDesc());
1560  CancelRequest();
1561  infoEntry->OnError(xmlDocument.ErrorDesc());
1562  return;
1563  }
1564  const char * rootElementName = "root";
1565  const TiXmlElement *root = xmlDocument.RootElement();
1566  assert(strcmp(root->Value(), rootElementName) == 0);
1567 
1568  // Parse pagination data.
1569  pageInfo.totalResults = pageInfo.currentlySkipped = 0;
1570  const TiXmlElement *meta = root->FirstChildElement("meta");
1571  if (meta != nullptr)
1572  {
1573  try
1574  {
1575  pageInfo.totalResults = std::stoi(getSafeStringValue(meta, "total", "0"));
1576  pageInfo.currentlySkipped = std::stoi(getSafeStringValue(meta, "skip", "0"));
1577  }
1578  catch (...) {}
1579  }
1580  const TiXmlElement* resources = root->FirstChildElement("resources");
1581  const char* resourceElementName = "item";
1582  std::vector<TiXmlElementLoaderInfo> elements;
1583  for (const TiXmlElement* e = resources->FirstChildElement(resourceElementName); e != NULL; e = e->NextSiblingElement(resourceElementName))
1584  {
1585  // Ignore empty elements.
1586  if (e->FirstChild() == nullptr)
1587  continue;
1588  elements.push_back(e);
1589  }
1590  AddToList(elements, ModXMLData::Source::Overview);
1591 
1592  // Nothing found? Notify!
1593  if (elements.empty())
1594  infoEntry->OnNoResultsFound();
1595  else if (pageInfo.getCurrentPage() < pageInfo.getTotalPages())
1596  {
1597  infoEntry->ShowPageInfo(pageInfo.getCurrentPage(), pageInfo.getTotalPages(), pageInfo.totalResults);
1598  pGameSelList->RemoveElement(infoEntry);
1599  pGameSelList->AddElement(infoEntry);
1600  }
1601  else
1602  delete infoEntry;
1603 
1604  CancelRequest();
1605  }
1606  }
1607  else // Not running a query.
1608  {
1609  if (!queryWasSuccessful && lastQueryEndTime + QueryRetryTimeout < time(nullptr)) // Last query failed?
1610  {
1611  QueryModList();
1612  return;
1613  }
1614  }
1615 
1616  pGameSelList->FreezeScrolling();
1617 
1618  // Refresh the "installed" state of all entries after new discovery.
1619  if (requiredSyncWithDiscovery)
1620  {
1621  requiredSyncWithDiscovery = false;
1622 
1623  C4GUI::Element *pElem, *pNextElem = pGameSelList->GetFirst();
1624  while ((pElem = pNextElem))
1625  {
1626  pNextElem = pElem->GetNext(); // determine next exec element now - execution
1627  C4StartupModsListEntry *pEntry = static_cast<C4StartupModsListEntry *>(pElem);
1628  if (pEntry->IsInfoEntry()) continue;
1629 
1630  if (!modsDiscovery.IsModInstalled(pEntry->GetID()))
1631  pEntry->UpdateInstalledState(nullptr);
1632  else
1633  {
1634  C4StartupModsLocalModDiscovery::ModsInfo info = modsDiscovery.GetModInformation(pEntry->GetID());
1635  pEntry->UpdateInstalledState(&info);
1636  }
1637  }
1638  }
1639 
1640  // done; selection might have changed
1641  pGameSelList->UnFreezeScrolling();
1642  UpdateSelection();
1643 }
1644 
1645 void C4StartupModsDlg::UpdateSelection()
1646 {
1647  C4StartupModsListEntry *selected = static_cast<C4StartupModsListEntry*>(pGameSelList->GetSelectedItem());
1648  btnInstall->SetEnabled(false);
1649  btnRemove->SetEnabled(false);
1650 
1651  if (selected != nullptr && !selected->IsInfoEntry())
1652  {
1653  btnInstall->SetEnabled(true);
1654 
1655  if (selected->IsInstalled())
1656  {
1657  btnRemove->SetEnabled(true);
1658  btnInstall->SetText(LoadResStr("IDS_MODS_UPDATE"));
1659  btnInstall->SetToolTip(LoadResStr("IDS_MODS_UPDATE_DESC"));
1660  }
1661  else
1662  {
1663  btnInstall->SetText(LoadResStr("IDS_MODS_INSTALL"));
1664  btnInstall->SetToolTip(LoadResStr("IDS_MODS_INSTALL_DESC"));
1665  }
1666  }
1667 }
1668 
1669 void C4StartupModsDlg::OnThreadEvent(C4InteractiveEventType eEvent, void *pEventData)
1670 {
1671  UpdateList(true);
1672 }
1673 
1674 void C4StartupModsDlg::CheckUpdateAll()
1675 {
1676  // Wait for the discovery (in the same thread here).
1677  modsDiscovery.WaitForDiscoveryFinished();
1678  auto lock = std::move(modsDiscovery.Lock());
1679 
1680  const auto & allInstalledMods = modsDiscovery.GetAllModInformation();
1681 
1682  if (allInstalledMods.empty())
1683  {
1684  ::pGUI->ShowMessageModal(LoadResStr("IDS_MODS_NOUPDATEALL_NOMODS"), LoadResStr("IDS_MODS_NOUPDATEALL"), C4GUI::MessageDialog::btnOK, C4GUI::Ico_Error);
1685  return;
1686  }
1687 
1688  if (downloader.get() != nullptr)
1689  downloader.reset();
1690  downloader = std::make_unique<C4StartupModsDownloader>(this, nullptr);
1691 
1692  for (const auto & pair : allInstalledMods)
1693  {
1694  const std::string &modID = pair.first;
1695  const std::string &name = pair.second.name;
1696 
1697  downloader->AddModToQueue(modID, name);
1698  }
1699 
1700  downloader->RequestConfirmation();
1701 }
1702 
1703 void C4StartupModsDlg::CheckRemoveMod()
1704 {
1705  C4StartupModsListEntry *selected = static_cast<C4StartupModsListEntry*>(pGameSelList->GetSelectedItem());
1706  if (selected == nullptr) return;
1707  if (selected->IsInfoEntry()) return;
1708 
1709  StdStrBuf confirmationMessage;
1710  confirmationMessage.Format(LoadResStr("IDS_MODS_UNINSTALL_CONFIRM"), selected->GetTitle().c_str());
1711  auto *callbackHandler = new C4GUI::CallbackHandler<C4StartupModsDlg>(this, &C4StartupModsDlg::OnConfirmRemoveMod);
1712  auto *dialog = new C4GUI::ConfirmationDialog(confirmationMessage.getData(), LoadResStr("IDS_MODS_UNINSTALL"), callbackHandler, C4GUI::MessageDialog::btnYesNo, false, C4GUI::Icons::Ico_Confirm);
1713  GetScreen()->ShowRemoveDlg(dialog);
1714 }
1715 
1716 void C4StartupModsDlg::OnConfirmRemoveMod(C4GUI::Element *element)
1717 {
1718  C4StartupModsListEntry *selected = static_cast<C4StartupModsListEntry*>(pGameSelList->GetSelectedItem());
1719  if (selected == nullptr) return;
1720  if (!selected->IsInstalled()) return;
1721 
1722  // Needs to have all infos for removing the mod.
1723  modsDiscovery.WaitForDiscoveryFinished();
1724  if (!modsDiscovery.IsModInstalled(selected->GetID())) return;
1725  const auto &mod = modsDiscovery.GetModInformation(selected->GetID());
1726 
1727  if (!EraseDirectory(mod.path.c_str()))
1728  {
1729  std::string errorMessage;
1730 #ifdef _WIN32
1731  auto dw = GetLastError();
1732  LPSTR messageBuffer = nullptr;
1733  size_t size = FormatMessageA(
1734  FORMAT_MESSAGE_ALLOCATE_BUFFER |
1735  FORMAT_MESSAGE_FROM_SYSTEM |
1736  FORMAT_MESSAGE_IGNORE_INSERTS,
1737  NULL,
1738  dw,
1739  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
1740  (LPSTR)&messageBuffer,
1741  0, NULL);
1742 
1743  errorMessage = std::string("||") + std::string(messageBuffer, size);
1744 #endif
1745  ::pGUI->ShowMessageModal((std::string(LoadResStr("IDS_MODS_NOUNINSTALL_REMOVEFAILED")) + errorMessage).c_str(), LoadResStr("IDS_MODS_NOUNINSTALL"), C4GUI::MessageDialog::btnOK, C4GUI::Ico_Error);
1746  }
1747  else
1748  {
1749  modsDiscovery.RemoveMod(selected->GetID());
1751  }
1752 }
1753 
1755 {
1756  if (GetFocus() == pSearchFieldEdt)
1757  {
1758  QueryModList();
1759  return true;
1760  }
1761  // get currently selected item
1762  C4GUI::Element *pSelection = pGameSelList->GetSelectedItem();
1763  StdCopyStrBuf strNoJoin(LoadResStr("IDS_MODS_NOINSTALL"));
1764  if (!pSelection)
1765  {
1766  // no ref selected: Oh noes!
1768  LoadResStr("IDS_MODS_NOINSTALL_NOMOD"),
1769  strNoJoin.getData(),
1772  return true;
1773  }
1774  else // Show confirmation dialogue.
1775  {
1776  auto *elem = static_cast<C4StartupModsListEntry*> (pSelection);
1777  if (elem->IsInfoEntry())
1778  {
1779  if (postClient.get() != nullptr) return false;
1780  // Next page?
1781  if (pageInfo.getCurrentPage() >= pageInfo.getTotalPages()) return false;
1782  QueryModList(true);
1783  return true;
1784  }
1785  if (elem->GetID().empty()) return false;
1786 
1787  if (downloader.get() != nullptr)
1788  downloader.reset();
1789  downloader = std::make_unique<C4StartupModsDownloader>(this, elem);
1790  downloader->RequestConfirmation();
1791  }
1792  return true;
1793 }
1794 
1796 {
1797  // abort dialog: Back to main
1799  return true;
1800 }
1801 
1803 {
1804  if (!postClient.get())
1805  DoRefresh();
1806  return true;
1807 }
1808 
1810 {
1811  // restart server query
1812  QueryModList();
1813  // done; update stuff
1814  UpdateList();
1815 }
1816 
1818 {
1819  // no updates if dialog is inactive (e.g., because a join password dlg is shown!)
1820  if (!IsActive(true))
1821  return;
1822 
1823  UpdateList(false);
1824 }
1825 
1826 
1828 {
1829  int32_t counter = 0;
1830  for (auto & option : sortingOptions)
1831  {
1832  // The labels were already translated earlier.
1833  pFiller->AddEntry(option.titleAsc, counter++);
1834  pFiller->AddEntry(option.titleDesc, counter++);
1835  }
1836 }
1837 
1838 bool C4StartupModsDlg::OnSortComboSelChange(C4GUI::ComboBox *pForCombo, int32_t idNewSelection)
1839 {
1840  const size_t selected = idNewSelection / 2;
1841  const bool descending = idNewSelection % 2 == 1;
1842  const std::string newSortKeySuffix = std::string(descending ? "-" : "") + sortingOptions[selected].key;
1843  if (newSortKeySuffix == sortKeySuffix) return true;
1844  sortKeySuffix = newSortKeySuffix;
1845  // Update label.
1846  const char *sortLabel = descending ? sortingOptions[selected].titleDesc : sortingOptions[selected].titleAsc;
1847  pForCombo->SetText(sortLabel);
1848  // Refresh view.
1849  QueryModList();
1850  return true;
1851 }
1852 
1853 bool C4StartupModsDlg::SetSubscreen(const char *toScreen)
1854 {
1855  std::string modID(toScreen);
1856  if (modID.empty()) return false;
1857 
1858  if (downloader.get() != nullptr)
1859  downloader.reset();
1860 
1861  downloader = std::make_unique<C4StartupModsDownloader>(this, nullptr);
1862 
1863  std::stringstream ss(modID);
1864  while (std::getline(ss, modID, '-'))
1865  if (!modID.empty())
1866  downloader->AddModToQueue(modID, "???");
1867 
1868  downloader->RequestConfirmation();
1869  return true;
1870 }
C4Config Config
Definition: C4Config.cpp:930
C4Application Application
Definition: C4Globals.cpp:44
C4GraphicsResource GraphicsResource
C4GUIScreen * pGUI
Definition: C4Gui.cpp:1191
#define C4GUI_CaptionFontClr
Definition: C4Gui.h:37
#define C4GUI_MessageFontClr
Definition: C4Gui.h:43
#define C4GUI_ButtonHgt
Definition: C4Gui.h:111
#define C4GUI_Caption2FontClr
Definition: C4Gui.h:38
#define C4GUI_IconExWdt
Definition: C4Gui.h:95
C4InteractiveEventType
@ Ev_HTTP_Response
@ KEYSCOPE_Gui
const char * LoadResStr(const char *id)
Definition: C4Language.h:83
bool Log(const char *szMessage)
Definition: C4Log.cpp:204
std::string getSafeStringValue(const TiXmlElement *xml, const char *childName, std::string fallback="", bool isAttribute=false)
const int ARight
Definition: C4Surface.h:41
const int ALeft
Definition: C4Surface.h:41
const int ACenter
Definition: C4Surface.h:41
bool GetFileSHA1(const char *szFilename, BYTE *pSHA1)
Definition: CStdFile.cpp:381
#define DirectorySeparator
uint8_t BYTE
#define SHA_DIGEST_LENGTH
Definition: SHA1.h:42
bool IsValidUtf8(const char *text, int length)
Definition: Standard.cpp:702
uint32_t GetNextUTF8Character(const char **pszString)
Definition: Standard.cpp:755
bool DirectoryExists(const char *szFilename)
Definition: StdFile.cpp:708
bool EraseDirectory(const char *szDirName)
Definition: StdFile.cpp:785
bool CreatePath(const std::string &path)
Definition: StdFile.cpp:656
bool EraseFile(const char *szFileName)
C4InteractiveThread InteractiveThread
Definition: C4Application.h:45
char UserDataPath[CFG_MaxString+1]
Definition: C4Config.h:56
C4ConfigGeneral General
Definition: C4Config.h:255
C4ConfigNetwork Network
Definition: C4Config.h:259
const char * GetModDatabaseServerAddress()
Definition: C4Config.cpp:675
std::vector< C4KeyCodeEx > CodeList
void SetEnabled(bool fToVal)
Definition: C4Gui.h:1131
void SetText(const char *szToText)
Definition: C4GuiButton.cpp:55
static bool GetStandardCheckBoxSize(int *piWdt, int *piHgt, const char *szForCaptionText, CStdFont *pUseFont)
void AddEntry(const char *szText, int32_t id)
void SetText(const char *szToText)
int32_t GetWidth() const
Definition: C4Gui.h:2803
bool GetFromLeft(int32_t iWdt, int32_t iHgt, C4Rect &rcOut)
Definition: C4Gui.cpp:1076
bool GetCentered(int32_t iWdt, int32_t iHgt, C4Rect &rcOut)
Definition: C4Gui.cpp:1133
int32_t GetHeight() const
Definition: C4Gui.h:2804
bool GetFromRight(int32_t iWdt, int32_t iHgt, C4Rect &rcOut)
Definition: C4Gui.cpp:1093
bool GetFromTop(int32_t iHgt, int32_t iWdt, C4Rect &rcOut)
Definition: C4Gui.cpp:1059
void GetAll(C4Rect &rcOut)
Definition: C4Gui.cpp:1125
bool GetFromBottom(int32_t iHgt, int32_t iWdt, C4Rect &rcOut)
Definition: C4Gui.cpp:1109
void AddElement(Element *pChild)
void SetVisibility(bool fToValue) override
void SetFocus(Control *pCtrl, bool fByMouse)
void SetDelOnClose(bool fToVal=true)
Definition: C4Gui.h:2191
void Close(bool fOK)
bool IsActive(bool fForKeyboard)
bool fOK
Definition: C4Gui.h:2083
void UpdateSize() override
bool IsAborted()
Definition: C4Gui.h:2150
virtual void OnShown()
Definition: C4Gui.h:2209
void SetTitle(const char *szToTitle, bool fShowCloseButton=true)
Control * GetFocus()
Definition: C4Gui.h:2116
const char * GetText()
Definition: C4Gui.h:1339
C4Rect rcBounds
Definition: C4Gui.h:385
virtual Screen * GetScreen()
Definition: C4Gui.cpp:289
Element * GetNext() const
Definition: C4Gui.h:449
C4Rect GetContainedClientRect()
Definition: C4Gui.h:448
virtual void UpdateSize()
Definition: C4Gui.cpp:185
void SetBounds(const C4Rect &rcNewBound)
Definition: C4Gui.h:446
bool fVisible
Definition: C4Gui.h:383
void SetToolTip(const char *szNewTooltip, bool is_immediate=false)
Definition: C4Gui.cpp:409
const char * GetToolTip()
Definition: C4Gui.cpp:423
C4Rect & GetBounds()
Definition: C4Gui.h:445
void SetHighlight(bool fToVal)
Definition: C4Gui.h:1151
bool GetHighlight() const
Definition: C4Gui.h:1152
void SetIcon(Icons icoNewIconIndex)
void SetText(const char *szToText, bool fAllowHotkey=true)
Definition: C4GuiLabels.cpp:74
void SetAutosize(bool fToVal)
Definition: C4Gui.h:507
void SetColor(DWORD dwToClr, bool fMakeReadableOnBlack=true)
Definition: C4Gui.h:505
Element * GetSelectedItem()
Definition: C4Gui.h:1581
bool InsertElement(Element *pChild, Element *pInsertBefore, int32_t iIndent=0)
void SetSelectionDblClickFn(BaseCallbackHandler *pToHandler)
Definition: C4Gui.h:1554
void SetSelectionChangeCallbackFn(BaseCallbackHandler *pToHandler)
Definition: C4Gui.h:1549
bool AddElement(Element *pChild, int32_t iIndent=0)
void UnFreezeScrolling()
Definition: C4Gui.h:1564
Element * GetFirst()
Definition: C4Gui.h:1572
void UpdateElementPositions()
Element * GetLast()
Definition: C4Gui.h:1573
void SetDecoration(bool fDrawBG, ScrollBarFacets *pToGfx, bool fAutoScroll, bool fDrawBorder=false)
Definition: C4Gui.h:1567
void FreezeScrolling()
Definition: C4Gui.h:1563
void RemoveElement(Element *pChild) override
void SetFacet(const C4Facet &fct)
Definition: C4Gui.h:612
void SetAnimated(bool fEnabled, int iDelay)
const C4FacetSurface & GetFacet() const
Definition: C4Gui.h:610
void SetProgress(int32_t iToProgress)
Definition: C4Gui.h:2379
void SetMessage(const char *szMessage)
bool ShowRemoveDlg(Dialog *pDlg)
bool ShowMessageModal(const char *szMessage, const char *szCaption, DWORD dwButtons, Icons icoIcon, int32_t *piConfigDontShowAgainSetting=nullptr)
Sheet * AddSheet(const char *szTitle, int32_t icoTitle=Ico_None)
void SetDrawDecoration(bool fToVal)
Definition: C4Gui.h:1715
void SetSheetMargin(int32_t iMargin)
Definition: C4Gui.h:1714
C4Rect & GetClientRect() override
Definition: C4Gui.h:864
static int32_t GetDefaultHeight(CStdFont *pUseFont=nullptr)
void RemoveProc(StdSchedulerProc *pProc)
void ClearCallback(C4InteractiveEventType eEvent, Callback *pnNetworkCallback)
bool AddProc(StdSchedulerProc *pProc)
void SetCallback(C4InteractiveEventType eEvent, Callback *pnNetworkCallback)
Definition: C4Rect.h:28
int32_t y
Definition: C4Rect.h:30
int32_t Hgt
Definition: C4Rect.h:30
int32_t Wdt
Definition: C4Rect.h:30
int32_t x
Definition: C4Rect.h:30
void Set(int32_t iX, int32_t iY, int32_t iWdt, int32_t iHgt)
Definition: C4Rect.cpp:86
C4FacetID fctNetGetRef
Definition: C4Startup.h:86
CStdFont BookFont
Definition: C4Startup.h:77
static C4Startup * Get()
Definition: C4Startup.h:147
class C4StartupDlg * SwitchDialog(DialogID eToDlg, bool fFade=true, const char *szSubDialog=nullptr)
Definition: C4Startup.cpp:139
C4StartupGraphics Graphics
Definition: C4Startup.h:112
virtual bool SetSubscreen(const char *toScreen) override
C4GUI::Control * GetDlgModeFocusControl()
void OnUpdateAllBtn(C4GUI::Control *btn)
virtual void DrawElement(C4TargetFacet &cgo)
virtual C4GUI::Control * GetDefaultControl()
void OnInstallModBtn(C4GUI::Control *btn)
void OnSelChange(class C4GUI::Element *pEl)
void OnSelDblClick(class C4GUI::Element *pEl)
bool OnSortComboSelChange(C4GUI::ComboBox *pForCombo, int32_t idNewSelection)
virtual void OnClosed(bool fOK)
friend class C4StartupModsListEntry
virtual void OnShown()
void OnShowInstalledBtn(C4GUI::Control *btn)
void OnSortComboFill(C4GUI::ComboBox_FillCB *pFiller)
C4GUI::Edit::InputResult OnSearchFieldEnter(C4GUI::Edit *edt, bool fPasting, bool fPastingMore)
void OnSearchOnlineBtn(C4GUI::Control *btn)
void OnBackBtn(C4GUI::Control *btn)
void OnUninstallModBtn(C4GUI::Control *btn)
C4StartupModsDownloader(C4StartupModsDlg *parent, const C4StartupModsListEntry *entry)
void OnConfirmInstallation(C4GUI::Element *element)
void AddModToQueue(std::string modID, std::string name)
void UpdateInstalledState(C4StartupModsLocalModDiscovery::ModsInfo *modInfo)
bool Execute()
std::string GetTitle() const
void OnNoResultsFound()
void MakeInfoEntry()
bool IsInstalled() const
const ModXMLData & GetModXMLData() const
void ShowPageInfo(int page, int totalPages, int totalResults)
std::string GetID() const
~C4StartupModsListEntry()
virtual void DrawElement(C4TargetFacet &cgo)
void FromXML(const TiXmlElement *xml, ModXMLData::Source source, std::string fallbackID="", std::string fallbackName="")
void SetVisibility(bool fToValue)
@ MaxInfoIconCount
@ InfoLabelCount
C4StartupModsListEntry(C4GUI::ListBox *pForListBox, C4GUI::Element *pInsertBefore, class C4StartupModsDlg *pModsDlg)
void Clear()
void OnError(std::string message)
bool IsInfoEntry() const
void RemoveMod(const std::string &id)
const std::map< std::string, ModsInfo > & GetAllModInformation()
const bool IsModInstalled(const std::string &id)
ModsInfo & AddMod(const std::string &id, const std::string &path, const std::string &name)
ModsInfo GetModInformation(const std::string &id)
void Set()
Definition: StdSync.h:158
void Reset()
Definition: StdSync.h:160
int GetLineHeight() const
Definition: C4FontLoader.h:125
std::tuple< std::string, int > BreakMessage(const char *szMsg, int iWdt, bool fCheckMarkup, float fZoom=1.0f)
bool GetTextExtent(const char *szText, int32_t &rsx, int32_t &rsy, bool fCheckMarkup=true)
void Remove(StdSchedulerProc *pProc)
void Add(StdSchedulerProc *pProc)
const char * getData() const
Definition: StdBuf.h:442
void Copy()
Definition: StdBuf.h:467
void Clear()
Definition: StdBuf.h:466
void Format(const char *szFmt,...) GNUC_FORMAT_ATTRIBUTE_O
Definition: StdBuf.cpp:174
void SignalStop()
Icons
Definition: C4Gui.h:638
@ Ico_None
Definition: C4Gui.h:640
@ Ico_Gfx
Definition: C4Gui.h:666
@ Ico_Team
Definition: C4Gui.h:662
@ Ico_Definition
Definition: C4Gui.h:673
@ Ico_Chart
Definition: C4Gui.h:665
@ Ico_Star
Definition: C4Gui.h:692
@ Ico_Resource
Definition: C4Gui.h:651
@ Ico_Clonk
Definition: C4Gui.h:641
@ Ico_Confirm
Definition: C4Gui.h:661
@ Ico_Error
Definition: C4Gui.h:652
@ Ico_Save
Definition: C4Gui.h:654
std::string slug
std::string id
std::string longDescription
bool metadataMissing
std::string description
std::string title
std::vector< std::string > dependencies
std::vector< FileInfo > files
TiXmlNode * originalXMLElement
std::vector< std::string > tags
bool requiresUpdate() const
ModXMLData(const TiXmlElement *xml, Source source=Source::Unknown)