hostile-takeover/game/dlmissionpack.cpp
Scott Ludwig dd56fab4bd Fix reported crash when downloading mission pack index
Clicking on the "Downloading X of Y" message, visible
while the index is downloading, caused this crash.

Bug and fix reported by Nathan Fulton.
2016-01-25 12:40:12 -08:00

708 lines
20 KiB
C++

#include "game/ht.h"
#include "game/httpindexloader.h"
#include "game/httppackinfomanager.h"
#include "game/httppackmanager.h"
#include "base/format.h"
#include "base/thread.h"
#include "yajl/wrapper/jsontypes.h"
#include <ctype.h>
namespace wi {
#define CTX_INDEX ((void *)0)
#define CTX_INFO ((void *)1)
#define PACKID_CUSTOMURL 0xFEFEFEFE
class DownloadMissionPackForm : public ShellForm, ProgressCallback {
public:
DownloadMissionPackForm(bool fMultiplayer) : fMultiplayer_(fMultiplayer),
idxl_(gphttp, gppackm), fWantsPlay_(false) {}
~DownloadMissionPackForm();
bool WantsPlay(PackId *ppackid) {
if (fWantsPlay_) {
*ppackid = packidPlay_;
return true;
}
return false;
}
// Form overrides
virtual bool Init(FormMgr *pfrmm, IniReader *pini, word idf);
virtual void OnControlSelected(word idc);
virtual void OnControlNotify(word idc, int nNotify);
virtual bool OnPenEvent(Event *pevt);
virtual bool OnFilterEvent(Event *pevt);
private:
void HandleCustomURL();
void Sort(word id);
void PositionColumns();
void HideShow();
void ShowStatus(void *ctx, const char *psz);
void ShowIndex(const PackId *ppackidSelect = NULL);
bool ShowInfo(bool fRequest);
const char *GetItemString(const IndexEntry *pentry);
const char *GetString(const json::JsonMap *map, const char *key,
const char *missing);
void UpdateLabelRects(LabelControl *plbl);
// ProgressCallback
virtual void OnBegin(void *ctx, int cbLength);
virtual void OnProgress(void *ctx, int cbTotal, int cbLength);
virtual void OnFinished(void *ctx);
virtual void OnError(void *ctx, const char *pszError);
HttpIndexLoader idxl_;
int x0_, x1_, x2_, x3_;
word idSortIndicator_;
HttpIndexLoader::SortType sort_;
Rect rcStatus_;
Rect rcTitle_;
Rect rcNumPlayers_;
Rect rcNumMissions_;
bool fWantsPlay_;
PackId packidPlay_;
bool fMultiplayer_;
};
bool ShowDownloadMissionPackForm(PackId *ppackid) {
bool fMultiplayer = false;
#ifdef MULTIPLAYER
// First, choose single, or multiplayer. This UI added due to feedback.
ShellForm *pfrmT = (ShellForm *)gpmfrmm->LoadForm(gpiniForms,
kidfAddOnSingleMulti, new ShellForm());
if (pfrmT == NULL) {
return false;
}
int idc;
pfrmT->DoModal(&idc);
delete pfrmT;
if (idc == kidcCancel || gevm.IsAppStopping()) {
return false;
}
if (idc == kidcPlayMultiPlayer) {
fMultiplayer = true;
}
#endif
// Bring up the download form
DownloadMissionPackForm *pfrm;
pfrm = (DownloadMissionPackForm *)gpmfrmm->LoadForm(gpiniForms,
kidfDownloadMissionPackWide, new DownloadMissionPackForm(
fMultiplayer));
if (pfrm != NULL) {
pfrm->DoModal();
if (pfrm->WantsPlay(ppackid)) {
delete pfrm;
return true;
}
delete pfrm;
}
return false;
}
DownloadMissionPackForm::~DownloadMissionPackForm() {
gppim->Reset();
}
bool DownloadMissionPackForm::Init(FormMgr *pfrmm, IniReader *pini, word idf) {
if (!ShellForm::Init(pfrmm, pini, idf)) {
return false;
}
// Start downloading the mission pack index
if (!idxl_.Start(CTX_INDEX, this)) {
return false;
}
// No operation is available until a mission is selected
GetControlPtr(kidcOk)->Show(false);
GetControlPtr(kidcDiscuss)->Show(false);
// Show status
ShowStatus(CTX_INDEX, "Contacting Server...");
// Position Column labels
PositionColumns();
// Set info label to use ellipsis
LabelControl *plbl = (LabelControl *)GetControlPtr(kidcMissionPackInfo);
plbl->SetFlags(plbl->GetFlags() | kfLblEllipsis);
// No sorting yet
idSortIndicator_ = 0;
sort_ = HttpIndexLoader::SORT_UNSORTED;
return true;
}
void DownloadMissionPackForm::PositionColumns() {
// Calc tab stops in the list
// S Title # Players Downloads
// S is + for installed, or ! for upgrade
// Title is the title. Make this as wide as possible
// # Players is as close to Downloads as is reasonable
// # Missions is right aligned
ListControl *plstc = (ListControl *)GetControlPtr(kidcMissionPackList);
Font *pfnt = plstc->GetFont();
LabelControl *plbl = (LabelControl *)GetControlPtr(kidcStatus);
#if 1
plbl->SetText("");
int cxPlus = pfnt->GetTextExtent("+");
#else
int cxPlus = pfnt->GetTextExtent(plbl->GetText());
#endif
plbl = (LabelControl *)GetControlPtr(kidcTitle);
int cxTitle = pfnt->GetTextExtent(plbl->GetText());
plbl = (LabelControl *)GetControlPtr(kidcNumPlayers);
int cxNumPlayers = pfnt->GetTextExtent(plbl->GetText());
int cxAsterisk = pfnt->GetTextExtent("*");
plbl = (LabelControl *)GetControlPtr(kidcNumMissions);
int cxNumMissions = pfnt->GetTextExtent(plbl->GetText());
Rect rcList;
plstc->GetRect(&rcList);
int xStatus = 2;
int xTitle = (rcList.Width() / 4 - cxTitle) / 2;
int xNumMissions = rcList.Width() - cxAsterisk - cxNumMissions;
int xNumPlayers = xNumMissions - 10 - cxNumPlayers;
// Calc the top of the list (past the arrow) for better column
// label hittesting
Size sizArrow;
ListControl::s_ptbmScrollUpUp->GetSize(&sizArrow);
int yListTop = rcList.top + sizArrow.cy;
// Set the label positions
// Enable the labels for hittesting
Rect rc;
plbl = (LabelControl *)GetControlPtr(kidcStatus);
plbl->GetRect(&rc);
rc.left = rcList.left + xStatus;
rc.right = xTitle;
rc.bottom += (yListTop - rc.bottom) / 2;
plbl->SetRect(&rc);
plbl->SetFlags(plbl->GetFlags() | kfLblHitTest);
rcStatus_ = rc;
plbl = (LabelControl *)GetControlPtr(kidcTitle);
plbl->GetRect(&rc);
rc.Offset(rcList.left + xTitle - rc.left, 0);
rc.right += rc.Width();
rc.bottom += (yListTop - rc.bottom) / 2;
plbl->SetRect(&rc);
plbl->SetFlags(plbl->GetFlags() | kfLblHitTest);
rcTitle_ = rc;
plbl = (LabelControl *)GetControlPtr(kidcNumPlayers);
plbl->GetRect(&rc);
rc.Offset(rcList.left + xNumPlayers - rc.left, 0);
rc.bottom += (yListTop - rc.bottom) / 2;
plbl->SetRect(&rc);
plbl->SetFlags(plbl->GetFlags() | kfLblHitTest);
rcNumPlayers_ = rc;
plbl = (LabelControl *)GetControlPtr(kidcNumMissions);
plbl->GetRect(&rc);
rc.Offset(rcList.left + xNumMissions - rc.left, 0);
rc.bottom += (yListTop - rc.bottom) / 2;
plbl->SetRect(&rc);
plbl->SetFlags(plbl->GetFlags() | kfLblHitTest);
rcNumMissions_ = rc;
// Remember the tab settings for later use
x0_ = 0;
x1_ = cxPlus + 4;
x2_ = xNumPlayers + cxNumPlayers / 2;
x3_ = xNumMissions + cxNumMissions / 2;
}
bool DownloadMissionPackForm::OnPenEvent(Event *pevt) {
if (pevt->eType != penDownEvent) {
return ShellForm::OnPenEvent(pevt);
}
Control *pctlCaptureBefore = GetControlCapture();
bool f = ShellForm::OnPenEvent(pevt);
Control *pctlCaptureAfter = GetControlCapture();
if (pctlCaptureBefore == NULL && pctlCaptureAfter != NULL) {
Sort(pctlCaptureAfter->GetId());
}
return f;
}
void DownloadMissionPackForm::Sort(word id) {
word aidColumns[] = {
kidcStatus, kidcTitle, kidcNumPlayers, kidcNumMissions
};
int iCol;
for (iCol = 0; iCol < ARRAYSIZE(aidColumns); iCol++) {
if (id == aidColumns[iCol]) {
break;
}
}
if (iCol >= ARRAYSIZE(aidColumns)) {
return;
}
HttpIndexLoader::SortType asortFlip[] = {
HttpIndexLoader::SORT_UNSORTED,
HttpIndexLoader::SORT_INSTALLEDDESCENDING,
HttpIndexLoader::SORT_INSTALLEDASCENDING,
HttpIndexLoader::SORT_TITLEDESCENDING,
HttpIndexLoader::SORT_TITLEASCENDING,
HttpIndexLoader::SORT_PLAYERSDESCENDING,
HttpIndexLoader::SORT_PLAYERSASCENDING,
HttpIndexLoader::SORT_MISSIONSDESCENDING,
HttpIndexLoader::SORT_MISSIONSASCENDING
};
// If this is already the sort column, flip the sort
if (id == idSortIndicator_) {
sort_ = asortFlip[sort_];
idxl_.Sort(sort_);
ShowIndex();
return;
}
// Take sort indicator off
char szT[32];
if (idSortIndicator_ != 0) {
LabelControl *plbl = (LabelControl *)GetControlPtr(idSortIndicator_);
strncpyz(szT, plbl->GetText(), sizeof(szT));
int cch = (int)strlen(szT);
if (szT[cch - 1] == '*') {
szT[cch - 1] = 0;
plbl->SetText(szT);
}
UpdateLabelRects(plbl);
}
// Put on new column
idSortIndicator_ = id;
LabelControl *plbl = (LabelControl *)GetControlPtr(idSortIndicator_);
strncpyz(szT, plbl->GetText(), sizeof(szT));
int cch = (int)strlen(szT);
if (szT[cch - 1] != '*') {
szT[cch] = '*';
szT[cch + 1] = 0;
plbl->SetText(szT);
UpdateLabelRects(plbl);
}
// Force this before the sort occurs, for immediate visual feedback
gpmfrmm->DrawFrame(false);
// Perform default sort for this column
HttpIndexLoader::SortType asortDefault[] = {
HttpIndexLoader::SORT_INSTALLEDDESCENDING,
HttpIndexLoader::SORT_TITLEASCENDING,
HttpIndexLoader::SORT_PLAYERSASCENDING,
HttpIndexLoader::SORT_MISSIONSDESCENDING
};
sort_ = asortDefault[iCol];
idxl_.Sort(sort_);
ShowIndex();
}
void DownloadMissionPackForm::UpdateLabelRects(LabelControl *plbl) {
switch (plbl->GetId()) {
case kidcStatus:
plbl->SetRect(&rcStatus_);
break;
case kidcTitle:
plbl->SetRect(&rcTitle_);
break;
case kidcNumPlayers:
plbl->SetRect(&rcNumPlayers_);
break;
case kidcNumMissions:
plbl->SetRect(&rcNumMissions_);
break;
}
}
void DownloadMissionPackForm::ShowStatus(void *ctx, const char *psz) {
if (ctx == CTX_INDEX) {
ListControl *plstc = (ListControl *)GetControlPtr(kidcMissionPackList);
plstc->Clear();
plstc->SetTabStops(0);
plstc->SetTabFlags(0);
plstc->Add(psz, NULL);
}
if (ctx == CTX_INFO) {
LabelControl *plbl =
(LabelControl *)GetControlPtr(kidcMissionPackInfo);
plbl->SetText((char *)psz);
}
// Force the frame draw, since this gets called while the main loop
// is "asleep".
gpmfrmm->DrawFrame(false);
}
void DownloadMissionPackForm::OnBegin(void *ctx, int cbLength) {
OnProgress(ctx, 0, cbLength);
}
void DownloadMissionPackForm::OnProgress(void *ctx, int cbTotal, int cbLength) {
ShowStatus(ctx, base::Format::ToString("Downloaded %d of %d bytes...",
cbTotal, cbLength));
}
void DownloadMissionPackForm::OnError(void *ctx, const char *pszError) {
// Don't show info errors since it's ok to not have info, and the error
// is confusing
if (ctx == CTX_INFO) {
ShowStatus(ctx, "");
} else {
ShowStatus(ctx, base::Format::ToString("Download error: %s",
pszError));
}
}
void DownloadMissionPackForm::OnFinished(void *ctx) {
if (ctx == CTX_INDEX) {
ShowStatus(ctx, "Download Complete! Sorting results...");
// Merge with installed packs so they are all listed
idxl_.MergeInstalled(gppackm);
// Add the fake entry for custom URLs
PackId packid;
memset(&packid, 0, sizeof(packid));
packid.id = PACKID_CUSTOMURL;
idxl_.AddFakeEntry(&packid, " Custom URL...", 999, 999, 0);
// Sort the single list
Sort(kidcNumMissions);
}
if (ctx == CTX_INFO) {
ShowInfo(false);
}
// Wake up to process the paint
base::Thread::current().Post(base::kidmNullEvent, NULL);
}
const char *DownloadMissionPackForm::GetItemString(const IndexEntry *pentry) {
// Figure out installed / upgrade state
char s;
switch (gppackm->IsInstalled(&pentry->packid)) {
default:
case 0:
// Not installed
s = ' ';
break;
case 1:
// Installed, current version is up to date
s = '+';
break;
case 2:
// Installed, but needs upgrade
s = '!';
break;
}
char szPlayers[8];
if (pentry->cPlayersMin == pentry->cPlayersMax) {
strncpyz(szPlayers, base::Format::ToString("%d", pentry->cPlayersMin),
sizeof(szPlayers));
} else {
strncpyz(szPlayers, base::Format::ToString("%d-%d",
pentry->cPlayersMin, pentry->cPlayersMax), sizeof(szPlayers));
}
if (pentry->packid.id == PACKID_CUSTOMURL) {
return base::Format::ToString(" \t%s\t \t ", pentry->title.c_str());
} else {
return base::Format::ToString("%c\t%s\t%s\t%d",
s, pentry->title.c_str(), szPlayers, pentry->cMissions);
}
}
void DownloadMissionPackForm::ShowIndex(const PackId *ppackidSelect) {
// Add entries to the list
ListControl *plstc = (ListControl *)GetControlPtr(kidcMissionPackList);
plstc->SetTabStops(x0_, x1_, x2_, x3_);
plstc->SetTabFlags(kfLstTabCenter, kfLstTabEllipsis, kfLstTabCenterOn,
kfLstTabCenterOn);
plstc->Clear();
int iSelect = 0;
int cAdded = 0;
int c = idxl_.GetCount();
for (int i = 0; i < c; i++) {
const IndexEntry *pentry = idxl_.GetEntry(i);
// Show either single player packs, or multiplayer packs.
// If a pack has both single and multiplayer missions, show
// these in both cases.
if (pentry->packid.id != PACKID_CUSTOMURL) {
if (fMultiplayer_) {
if (pentry->cPlayersMax == 1) {
continue;
}
} else {
if (pentry->cPlayersMin != 1) {
continue;
}
}
}
// Remember which entry to select
if (iSelect == 0 && ppackidSelect != NULL) {
if (memcmp(ppackidSelect, &pentry->packid,
sizeof(*ppackidSelect)) == 0) {
iSelect = cAdded;
}
}
plstc->Add(GetItemString(pentry), (void *)pentry);
cAdded++;
}
if (cAdded > 0) {
plstc->Select(iSelect, true, true);
}
}
bool DownloadMissionPackForm::ShowInfo(bool fRequest) {
ListControl *plstc = (ListControl *)GetControlPtr(kidcMissionPackList);
IndexEntry *entry = (IndexEntry *)plstc->GetSelectedItemData();
if (entry == NULL) {
GetControlPtr(kidcDiscuss)->Show(false);
return false;
}
const json::JsonMap *map = gppim->GetInfoMap(&entry->packid);
if (map == NULL) {
if (fRequest) {
if (entry->packid.id == PACKID_CUSTOMURL) {
ShowStatus(CTX_INFO, "Press Download to download from a custom URL");
} else {
gppim->Start(&entry->packid, CTX_INFO, this);
ShowStatus(CTX_INFO, "Loading Mission Pack info...");
}
}
GetControlPtr(kidcDiscuss)->Show(false);
return false;
}
const char *author = GetString(map, "a", "unknown");
const char *desc = GetString(map, "d", "no description");
const char *s = base::Format::ToString("By %s: %s", author, desc);
delete map;
LabelControl *plbl = (LabelControl *)GetControlPtr(kidcMissionPackInfo);
plbl->SetText((char *)s);
GetControlPtr(kidcDiscuss)->Show(true);
return true;
}
const char *DownloadMissionPackForm::GetString(const json::JsonMap *map,
const char *key, const char *missing) {
const json::JsonObject *obj = map->GetObject(key);
if (obj == NULL || obj->type() != json::JSONTYPE_STRING) {
return missing;
}
const json::JsonString *s = (json::JsonString *)obj;
bool fWhitespace = true;
char ch;
const char *psz = s->GetString();
while ((ch = *psz++) != 0) {
if (!isspace(ch)) {
fWhitespace = false;
break;
}
}
if (fWhitespace) {
return missing;
}
return s->GetString();
}
void DownloadMissionPackForm::OnControlNotify(word idc, int nNotify) {
if (idc == kidcMissionPackList && nNotify == knNotifySelectionChange) {
HideShow();
}
Form::OnControlNotify(idc, nNotify);
}
void DownloadMissionPackForm::HideShow() {
ListControl *plstc = (ListControl *)GetControlPtr(kidcMissionPackList);
LabelControl *plbl = (LabelControl *)GetControlPtr(kidcMissionPackInfo);
ButtonControl *pbtn = (ButtonControl *)GetControlPtr(kidcOk);
int selected = plstc->GetSelectedItemIndex();
if (selected < 0) {
gppim->Reset();
plbl->SetText("");
pbtn->Show(false);
GetControlPtr(kidcDiscuss)->Show(false);
return;
}
// Get the pack info for this pack
IndexEntry *entry = (IndexEntry *)plstc->GetSelectedItemData();
if (entry == NULL) {
return;
}
char *psz = NULL;
switch (gppackm->IsInstalled(&entry->packid)) {
case 0:
psz = "DOWNLOAD";
break;
case 1:
psz = "REMOVE";
break;
case 2:
psz = "UPGRADE";
break;
}
if (psz != NULL) {
pbtn->SetText(psz);
}
pbtn->Show(true);
ShowInfo(true);
}
void DownloadMissionPackForm::OnControlSelected(word idc) {
if (idc == kidcCancel) {
EndForm(idc);
return;
}
ListControl *plstc = (ListControl *)GetControlPtr(kidcMissionPackList);
IndexEntry *entry = (IndexEntry *)plstc->GetSelectedItemData();
if (entry == NULL) {
return;
}
switch (idc) {
case kidcOk:
switch (gppackm->IsInstalled(&entry->packid)) {
case 0:
// If this is a custom URL entry handle specially
if (entry->packid.id == PACKID_CUSTOMURL) {
HandleCustomURL();
break;
}
// fall through
case 2:
// Pack not install or needs upgrade. In either case, download
if (DownloadMissionPack(&entry->packid, NULL, true)) {
fWantsPlay_ = true;
packidPlay_ = entry->packid;
EndForm(kidcOk);
}
plstc->SetSelectedItemText(GetItemString(entry));
HideShow();
break;
case 1:
// Pack is installed, so this action means remove
if (HtMessageBox(kidfMessageBoxQuery,
kfMbWhiteBorder | kfMbKeepTimersEnabled,
"Remove Mission Pack", "Are you sure?")) {
gppackm->Remove(&entry->packid);
if (idxl_.OnRemoved(&entry->packid)) {
ShowIndex();
} else {
plstc->SetSelectedItemText(GetItemString(entry));
}
HideShow();
}
break;
}
break;
case kidcDiscuss:
{
json::JsonMap *map = gppim->GetInfoMap(&entry->packid);
if (map != NULL) {
const json::JsonObject *obj = map->GetObject("di");
if (obj != NULL && obj->type() == json::JSONTYPE_STRING) {
HostOpenUrl(((const json::JsonString *)obj)->GetString());
}
delete map;
}
}
break;
}
}
bool DownloadMissionPackForm::OnFilterEvent(Event *pevt) {
if (pevt->eType != askStringEvent) {
return false;
}
// Get the URL and Save preferences so this url is remembered
HostGetAskString(gszAskURL, sizeof(gszAskURL));
ggame.SavePreferences();
// Download this custom pack
PackId packid;
bool play = false;
if (!DownloadMissionPackFromURL(gszAskURL, &packid, &play)) {
// Some kind of error occured
return true;
}
// User wants to play now?
if (play) {
// The user wants to play the mission
fWantsPlay_ = true;
packidPlay_ = packid;
EndForm(kidcOk);
return true;
}
// Add this mission pack to the index, and re-fill the list control,
// and select it.
idxl_.AddIndexEntry(&packid);
idxl_.Sort(sort_);
ShowIndex(&packid);
return true;
}
void DownloadMissionPackForm::HandleCustomURL() {
HostInitiateAsk("Custom URL", -1, gszAskURL, knKeyboardAskURL);
}
} // namespace wi