/*==LICENSE==*
CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011 Cyan Worlds, Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
You can contact Cyan Worlds, Inc. by email legal@cyan.com
or by snail mail at:
Cyan Worlds, Inc.
14617 N Newport Hwy
Mead, WA 99021
*==LICENSE==*/
#include "plBackgroundDownloader.h"
#include
#include "pnUtils/pnUtils.h"
#include "pnNetBase/pnNetBase.h"
#include "plEncryption/plChecksum.h"
#include "hsResMgr.h"
#include "plAgeDescription/plAgeManifest.h"
#include "plResMgr/plResManager.h"
#include "plFile/plFileUtils.h"
#include "plFile/plEncryptedStream.h"
#include "plCompression/plZlibStream.h"
#include "plAudioCore/plAudioFileReader.h"
#include "plProgressMgr/plProgressMgr.h"
#include "pnAsyncCore/pnAsyncCore.h"
#include "pnNetCli/pnNetCli.h"
#include "plNetGameLib/plNetGameLib.h"
#include "pnDispatch/plDispatch.h"
#include "plStatusLog/plStatusLog.h"
static const unsigned kMaxDownloadTries = 10;
static const wchar s_manifest[] = L"AllAges";
plBackgroundDownloader* plBackgroundDownloader::fInstance = NULL;
hsBool gUseBackgroundDownloader = false;
//============================================================================
enum DownloaderLogType
{
kHeader,
kInfo,
kMajorStatus,
kStatus,
kError,
};
void BackgroundDownloaderLog(DownloaderLogType type, const char* format, ...)
{
UInt32 color = 0;
switch (type)
{
case kHeader: color = plStatusLog::kWhite; break;
case kInfo: color = plStatusLog::kBlue; break;
case kMajorStatus: color = plStatusLog::kYellow; break;
case kStatus: color = plStatusLog::kGreen; break;
case kError: color = plStatusLog::kRed; break;
}
static plStatusLog* gStatusLog = nil;
if (!gStatusLog)
{
gStatusLog = plStatusLogMgr::GetInstance().CreateStatusLog(
20,
"bgdownload.log",
plStatusLog::kFilledBackground | plStatusLog::kAlignToTop | plStatusLog::kDeleteForMe);
}
va_list args;
va_start(args, format);
gStatusLog->AddLineV(color, format, args);
va_end(args);
}
//============================================================================
class plBGDownloadStream : public plZlibStream
{
public:
plBGDownloadStream() : plZlibStream() {}
virtual ~plBGDownloadStream() {}
virtual UInt32 Write(UInt32 byteCount, const void* buffer);
};
UInt32 plBGDownloadStream::Write(UInt32 byteCount, const void* buffer)
{
return plZlibStream::Write(byteCount, buffer);
}
//============================================================================
static void DownloadFileCallback(ENetError result, void* param, const wchar filename[], hsStream* writer)
{
plBackgroundDownloader* bgdownloader = (plBackgroundDownloader*)param;
// Retry download unless shutting down or file not found
switch (result) {
case kNetSuccess:
writer->Close();
bgdownloader->DoneWithFile(true);
break;
case kNetErrFileNotFound:
case kNetErrRemoteShutdown:
writer->Close();
bgdownloader->DoneWithFile(false);
break;
default:
writer->Rewind();
NetCliFileDownloadRequest(
filename,
writer,
DownloadFileCallback,
param
);
break;
}
}
static void ManifestCallback(ENetError result, void* param, const wchar group[], const NetCliFileManifestEntry manifest[], unsigned entryCount)
{
plBackgroundDownloader* bgdownloader = (plBackgroundDownloader*)param;
bgdownloader->DoneWithManifest(result == kNetSuccess, manifest, entryCount);
}
//============================================================================
plBackgroundDownloader* plBackgroundDownloader::GetInstance()
{
return fInstance;
}
void plBackgroundDownloader::ThreadMain(void * param)
{
Init();
plBackgroundDownloader::GetInstance()->Run();
plBackgroundDownloader::GetInstance()->CleanUp();
Shutdown();
}
void plBackgroundDownloader::StartThread()
{
_beginthread(plBackgroundDownloader::ThreadMain, 0, NULL);
}
void plBackgroundDownloader::Init()
{
fInstance = TRACKED_NEW plBackgroundDownloader();
}
void plBackgroundDownloader::Shutdown()
{
delete fInstance;
fInstance = NULL;
}
plBackgroundDownloader::plBackgroundDownloader()
{
BackgroundDownloaderLog(kHeader, "--- Starting background download ---");
fBGDownloaderRun = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is signaled
NULL // unnamed
);
fBGDownloaderIsPaused = CreateEvent(
NULL, // default security attributes
FALSE, // manual-reset event
TRUE, // initial state is signaled
NULL // unnamed
);
}
plBackgroundDownloader::~plBackgroundDownloader()
{
HANDLE runHandle = fBGDownloaderRun;
fBGDownloaderRun = NULL;
CloseHandle(runHandle);
HANDLE pausedHandle = fBGDownloaderIsPaused;
fBGDownloaderIsPaused = NULL;
CloseHandle(pausedHandle);
BackgroundDownloaderLog(kHeader, "--- Background download done ---");
}
UInt32 plBackgroundDownloader::IGetDownloadSize()
{
if (!IGetDataManifest())
return 0;
UInt32 downloadSize = 0;
UInt32 downloadFiles = 0;
for (MfsFileVec::iterator i = fMfsVec.begin(); i != fMfsVec.end(); ++i)
{
plManifestFile* mfsFile = (*i);
if (!mfsFile->IsLocalUpToDate())
{
downloadFiles++;
downloadSize += mfsFile->GetDownloadSize();
}
}
BackgroundDownloaderLog(kInfo, "Got download stats, %d files, %d bytes", downloadFiles, downloadSize);
return downloadSize;
}
bool plBackgroundDownloader::CheckFreeSpace(UInt32 bytesNeeded)
{
#ifdef HS_BUILD_FOR_WIN32
ULARGE_INTEGER freeBytesAvailable, totalNumberOfBytes, neededBytes;
if (GetDiskFreeSpaceEx(NULL, &freeBytesAvailable, &totalNumberOfBytes, NULL))
{
neededBytes.HighPart = 0;
neededBytes.LowPart = bytesNeeded;
if (neededBytes.QuadPart > freeBytesAvailable.QuadPart)
{
BackgroundDownloaderLog(kInfo, "Not enough disk space (asked for %d bytes)", bytesNeeded);
return false;
}
}
#endif // HS_BUILD_FOR_WIN32
return true;
}
bool plBackgroundDownloader::IDecompressSound(plManifestFile* mfsFile, bool noOverwrite)
{
UInt32 flags = mfsFile->GetFlags();
if ( (hsCheckBits(flags, plManifestFile::kSndFlagCacheSplit) || hsCheckBits(flags, plManifestFile::kSndFlagCacheStereo)) && stricmp(plFileUtils::GetFileExt(mfsFile->GetName()), "ogg") == 0)
{
plAudioFileReader* reader = plAudioFileReader::CreateReader(mfsFile->GetName(), plAudioCore::kAll, plAudioFileReader::kStreamNative);
if (!reader)
{
BackgroundDownloaderLog(kInfo, "Unable to create audio file reader for %s", mfsFile->GetName());
return false;
}
UInt32 size = reader->GetDataSize();
delete reader;
// Make sure we have enough free space
if (!CheckFreeSpace(size))
return false;
if (hsCheckBits(flags, plManifestFile::kSndFlagCacheSplit))
plAudioFileReader::CacheFile(mfsFile->GetName(), true, noOverwrite);
if (hsCheckBits(flags, plManifestFile::kSndFlagCacheStereo))
plAudioFileReader::CacheFile(mfsFile->GetName(), false, noOverwrite);
}
return true;
}
bool plBackgroundDownloader::Run()
{
// Wait to be signaled that we've gotten at least as far as the startup age
WaitForSingleObject(fBGDownloaderRun, INFINITE);
IGetDataManifest();
plFileUtils::CreateDir("dat");
plFileUtils::CreateDir("sfx");
bool result = true;
plResManager* resMgr = ((plResManager*)hsgResMgr::ResMgr());
for (MfsFileVec::iterator i = fMfsVec.begin(); i != fMfsVec.end(); ++i)
{
plManifestFile* mfsFile = (*i);
if (!mfsFile->IsLocalUpToDate())
{
if (!CheckFreeSpace(mfsFile->GetDiskSize()))
return false;
FileType type = IGetFile(mfsFile);
if (type == kPrp)
{
// Checks for existence before attempting to remove
resMgr->RemoveSinglePage(mfsFile->GetName());
if (!resMgr->FindSinglePage(mfsFile->GetName()))
{
resMgr->AddSinglePage(mfsFile->GetName());
}
}
else if (type == kOther)
{
if (!IDecompressSound(mfsFile, false))
{
char text[MAX_PATH];
StrPrintf(text, arrsize(text), "%s could not be decompressed", mfsFile->GetName());
BackgroundDownloaderLog(kInfo, text );
hsAssert(false, text);
result = false;
}
}
else
{
char text[MAX_PATH];
StrPrintf(text, arrsize(text), "Failed downloading file: %s", mfsFile->GetName());
BackgroundDownloaderLog(kInfo, text );
hsAssert(false, text);
result = false;
}
}
}
return result;
}
void plBackgroundDownloader::CleanUp()
{
BackgroundDownloaderLog(kMajorStatus, "Cleaning up background downloader..." );
for (MfsFileVec::iterator i = fMfsVec.begin(); i != fMfsVec.end(); ++i)
{
plManifestFile* file = (*i);
delete file;
}
fMfsVec.clear();
}
void plBackgroundDownloader::Pause()
{
if (fBGDownloaderRun != NULL && fBGDownloaderIsPaused != NULL)
{
ResetEvent(fBGDownloaderRun);
WaitForSingleObject(fBGDownloaderIsPaused, INFINITE);
BackgroundDownloaderLog(kStatus, "--- Background download paused ---");
}
}
void plBackgroundDownloader::UnPause()
{
if (fBGDownloaderRun != NULL && fBGDownloaderIsPaused != NULL)
{
SetEvent(fBGDownloaderRun);
BackgroundDownloaderLog(kStatus, "--- Background download resumed ---");
}
}
plBackgroundDownloader::FileType plBackgroundDownloader::IGetFile(const plManifestFile* mfsFile)
{
BackgroundDownloaderLog(kInfo, " Setting up to download file %s", mfsFile->GetName());
bool downloadDone = false;
wchar* wServerPath = hsStringToWString(mfsFile->GetServerPath());
int numTries = 0;
while (!downloadDone)
{
if (WaitForSingleObject(fBGDownloaderRun, 0) == WAIT_TIMEOUT)
SignalObjectAndWait(fBGDownloaderIsPaused, fBGDownloaderRun, INFINITE, FALSE);
if (numTries >= kMaxDownloadTries)
{
BackgroundDownloaderLog(kInfo, " Max download tries exceeded (%d). Aborting download...", kMaxDownloadTries);
return kFail;
}
plBGDownloadStream* downloadStream = TRACKED_NEW plBGDownloadStream();
if (!downloadStream->Open(mfsFile->GetName(), "wb"))
{
BackgroundDownloaderLog(kInfo, " Unable to create file. Aborting download...");
return kFail;
}
BackgroundDownloaderLog(kInfo, " Downloading file %s...", mfsFile->GetName());
fSuccess = false;
fDoneWithFile = false;
NetCliFileDownloadRequest(
wServerPath,
downloadStream,
DownloadFileCallback,
this
);
while (!fDoneWithFile) {
AsyncSleep(100);
}
if (!fSuccess) {
// remove partial file and die (server didn't have the file or server is shutting down)
plFileUtils::RemoveFile(mfsFile->GetName(), true);
BackgroundDownloaderLog(kError, " File %s failed to download.", mfsFile->GetName());
}
else {
AsyncSleep(100);
if (downloadStream->DecompressedOk()) {
BackgroundDownloaderLog(kInfo, " Decompress successful." );
// download and decompress successful, do a md5 check on the resulting file
plMD5Checksum localMD5(mfsFile->GetName());
if (localMD5 != mfsFile->GetChecksum()) {
plFileUtils::RemoveFile(mfsFile->GetName(), true);
BackgroundDownloaderLog(kError, " File %s MD5 check FAILED.", mfsFile->GetName());
// don't set downloadDone so we attempt to re-download from the server
}
else {
BackgroundDownloaderLog(kInfo, " MD5 check succeeded.");
downloadDone = true;
}
}
else {
plFileUtils::RemoveFile(mfsFile->GetName(), true);
BackgroundDownloaderLog(kError, " File %s failed to decompress.", mfsFile->GetName());
// don't set downloadDone so we attempt to re-download from the server
}
}
delete downloadStream;
++numTries;
}
delete [] wServerPath;
if (!fSuccess)
return kFail;
if (stricmp(plFileUtils::GetFileExt(mfsFile->GetName()), "prp") == 0)
return kPrp;
return kOther;
}
bool plBackgroundDownloader::IGetDataManifest()
{
if (fMfsVec.size() > 0)
return true;
BackgroundDownloaderLog(kMajorStatus, "Downloading new manifest from data server..." );
fSuccess = false;
unsigned numTries = 0;
while (!fSuccess)
{
numTries++;
fDoneWithFile = false;
NetCliFileManifestRequest(ManifestCallback, this, s_manifest);
while (!fDoneWithFile)
{
NetClientUpdate();
plgDispatch::Dispatch()->MsgQueueProcess();
AsyncSleep(10);
}
if (!fSuccess)
{
fMfsVec.clear(); // clear out any bad data
if (numTries > kMaxDownloadTries)
break; // abort
}
}
if (fSuccess)
BackgroundDownloaderLog(kStatus, "New manifest read; number of files: %d", fMfsVec.size() );
else
BackgroundDownloaderLog(kStatus, "Failed to download manifest after trying %d times", kMaxDownloadTries);
return fSuccess;
}
void plBackgroundDownloader::DoneWithFile(bool success)
{
fDoneWithFile = true;
fSuccess = success;
}
void plBackgroundDownloader::DoneWithManifest(bool success, const NetCliFileManifestEntry manifestEntires[], unsigned entryCount)
{
BackgroundDownloaderLog(kStatus, "New age manifest received. Reading...");
if (success)
{
for (unsigned i = 0; i < entryCount; i++)
{
char* name = hsWStringToString(manifestEntires[i].clientName);
char* serverPath = hsWStringToString(manifestEntires[i].downloadName);
char* md5Str = hsWStringToString(manifestEntires[i].md5);
int size = manifestEntires[i].fileSize;
int zipsize = manifestEntires[i].zipSize;
int flags = manifestEntires[i].flags;
if (stricmp(plFileUtils::GetFileExt(name), "gz"))
flags |= plManifestFile::kFlagZipped; // add zipped flag if necessary
plMD5Checksum sum;
sum.SetFromHexString(md5Str);
fMfsVec.push_back(TRACKED_NEW plManifestFile(name, serverPath, sum, size, zipsize, flags, false));
delete [] name;
delete [] serverPath;
delete [] md5Str;
}
}
fDoneWithFile = true;
fSuccess = success;
}