2
3
mirror of https://foundry.openuru.org/gitblit/r/CWE-ou-minkata.git synced 2025-07-14 02:27:40 -04:00

Merge pull request #352 from Hoikas/patcher-thread

Unified Patcher (Part 1: In-Game)
This commit is contained in:
2013-11-30 14:30:07 -08:00
39 changed files with 1108 additions and 1064 deletions

View File

@ -17,6 +17,6 @@ add_subdirectory(pfJournalBook)
# add_subdirectory(pfKI)
add_subdirectory(pfLocalizationMgr)
add_subdirectory(pfMessage)
add_subdirectory(pfPatcher)
add_subdirectory(pfPython)
add_subdirectory(pfSecurePreloader)
add_subdirectory(pfSurface)

View File

@ -57,6 +57,5 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com
#include "pfCCR/plCCRCreatable.h"
#include "pfJournalBook/pfJournalBookCreatable.h"
#include "pfGameMgr/pfGameMgrCreatables.h"
#include "pfSecurePreloader/pfSecurePreloaderCreatable.h"
#endif // pfAllCreatables_inc

View File

@ -0,0 +1,19 @@
include_directories("../../CoreLib")
include_directories("../../NucleusLib")
include_directories("../../NucleusLib/inc")
include_directories("../../PubUtilLib")
set(pfPatcher_SOURCES
plManifests.cpp
pfPatcher.cpp
)
set(pfPatcher_HEADERS
plManifests.h
pfPatcher.h
)
add_library(pfPatcher STATIC ${pfPatcher_SOURCES} ${pfPatcher_HEADERS})
source_group("Source Files" FILES ${pfPatcher_SOURCES})
source_group("Header Files" FILES ${pfPatcher_HEADERS})

View File

@ -0,0 +1,598 @@
/*==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 <http://www.gnu.org/licenses/>.
Additional permissions under GNU GPL version 3 section 7
If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.
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 <algorithm>
#include <deque>
#include "pfPatcher.h"
#include "HeadSpin.h"
#include "plCompression/plZlibStream.h"
#include "pnEncryption/plChecksum.h"
#include "plFileSystem.h"
#include "pnNetBase/pnNbError.h"
#include "plNetGameLib/plNetGameLib.h"
#include "plStatusLog/plStatusLog.h"
#include "hsStream.h"
#include "hsThread.h"
#include "hsTimer.h"
// Some log helper defines
#define PatcherLogGreen(...) pfPatcher::GetLog()->AddLineF(plStatusLog::kGreen, __VA_ARGS__)
#define PatcherLogRed(...) pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, __VA_ARGS__)
#define PatcherLogWhite(...) pfPatcher::GetLog()->AddLineF(plStatusLog::kWhite, __VA_ARGS__)
#define PatcherLogYellow(...) pfPatcher::GetLog()->AddLineF(plStatusLog::kYellow, __VA_ARGS__)
/** Patcher grunt work thread */
struct pfPatcherWorker : public hsThread
{
/** Represents a File/Auth download request */
struct Request
{
enum { kFile, kManifest, kSecurePreloader, kAuthFile, kPythonList, kSdlList };
plString fName;
uint8_t fType;
class pfPatcherStream* fStream;
Request(const plString& name, uint8_t type, class pfPatcherStream* s=nullptr) :
fName(name), fType(type), fStream(s)
{ }
};
/** Human readable file flags */
enum FileFlags
{
// Sound files only
kSndFlagCacheSplit = 1<<0,
kSndFlagStreamCompressed = 1<<1,
kSndFlagCacheStereo = 1<<2,
// Any file
kFlagZipped = 1<<3,
};
std::deque<Request> fRequests;
std::deque<NetCliFileManifestEntry> fQueuedFiles;
hsMutex fRequestMut;
hsMutex fFileMut;
hsSemaphore fFileSignal;
pfPatcher::CompletionFunc fOnComplete;
pfPatcher::FileDownloadFunc fFileBeginDownload;
pfPatcher::FileDownloadFunc fFileDownloaded;
pfPatcher::GameCodeDiscoverFunc fGameCodeDiscovered;
pfPatcher::ProgressTickFunc fProgressTick;
pfPatcher* fParent;
volatile bool fStarted;
volatile bool fRequestActive;
uint64_t fCurrBytes;
uint64_t fTotalBytes;
pfPatcherWorker();
~pfPatcherWorker();
void OnQuit();
void EndPatch(ENetError result, const plString& msg=plString::Null);
bool IssueRequest();
virtual hsError Run();
void ProcessFile();
void WhitelistFile(const plFileName& file, bool justDownloaded, hsStream* s=nullptr);
};
// ===================================================
class pfPatcherStream : public plZlibStream
{
pfPatcherWorker* fParent;
plFileName fFilename;
uint32_t fFlags;
uint64_t fBytesWritten;
float fDLStartTime;
plString IMakeStatusMsg() const
{
float secs = hsTimer::GetSysSeconds() - fDLStartTime;
float bytesPerSec = fBytesWritten / secs;
return plFileSystem::ConvertFileSize(bytesPerSec) + "/s";
}
void IUpdateProgress(uint32_t count)
{
fBytesWritten += count; // just this file
fParent->fCurrBytes += count; // the entire everything
// tick-tick-tick, tick-tick-tock
if (fParent->fProgressTick)
fParent->fProgressTick(fParent->fCurrBytes, fParent->fTotalBytes, IMakeStatusMsg());
}
public:
pfPatcherStream(pfPatcherWorker* parent, const plFileName& filename, uint64_t size)
: fParent(parent), fFilename(filename), fFlags(0), fBytesWritten(0)
{
fParent->fTotalBytes += size;
fOutput = new hsRAMStream;
}
pfPatcherStream(pfPatcherWorker* parent, const plFileName& filename, const NetCliFileManifestEntry& entry)
: fParent(parent), fFlags(entry.flags), fBytesWritten(0)
{
// ugh. eap removed the compressed flag in his fail manifests
if (filename.GetFileExt().CompareI("gz") == 0) {
fFlags |= pfPatcherWorker::kFlagZipped;
parent->fTotalBytes += entry.zipSize;
} else
parent->fTotalBytes += entry.fileSize;
}
virtual bool Open(const plFileName& filename, const char* mode)
{
fFilename = filename;
return plZlibStream::Open(filename, mode);
}
virtual uint32_t Write(uint32_t count, const void* buf)
{
// tick whatever progress bar we have
IUpdateProgress(count);
// write the appropriate blargs
if (hsCheckBits(fFlags, pfPatcherWorker::kFlagZipped))
return plZlibStream::Write(count, buf);
else
return fOutput->Write(count, buf);
}
virtual bool AtEnd() { return fOutput->AtEnd(); }
virtual uint32_t GetEOF() { return fOutput->GetEOF(); }
virtual uint32_t GetPosition() const { return fOutput->GetPosition(); }
virtual uint32_t GetSizeLeft() const { return fOutput->GetSizeLeft(); }
virtual uint32_t Read(uint32_t count, void* buf) { return fOutput->Read(count, buf); }
virtual void Rewind() { fOutput->Rewind(); }
virtual void SetPosition(uint32_t pos) { fOutput->SetPosition(pos); }
virtual void Skip(uint32_t deltaByteCount) { fOutput->Skip(deltaByteCount); }
void Begin() { fDLStartTime = hsTimer::GetSysSeconds(); }
plFileName GetFileName() const { return fFilename; }
void Unlink() const { plFileSystem::Unlink(fFilename); }
};
// ===================================================
static void IAuthThingDownloadCB(ENetError result, void* param, const plFileName& filename, hsStream* writer)
{
pfPatcherWorker* patcher = static_cast<pfPatcherWorker*>(param);
if (IS_NET_SUCCESS(result)) {
PatcherLogGreen("\tDownloaded Legacy File '%s'", filename.AsString().c_str());
patcher->IssueRequest();
// Now, we pass our RAM-backed file to the game code handlers. In the main client,
// this will trickle down and add a new friend to plStreamSource. This should never
// happen in any other app...
writer->Rewind();
patcher->WhitelistFile(filename, true, writer);
} else {
PatcherLogRed("\tDownloaded Failed: File '%s'", filename.AsString().c_str());
patcher->EndPatch(result, filename.AsString());
}
}
static void IGotAuthFileList(ENetError result, void* param, const NetCliAuthFileInfo infoArr[], unsigned infoCount)
{
pfPatcherWorker* patcher = static_cast<pfPatcherWorker*>(param);
if (IS_NET_SUCCESS(result)) {
// so everything goes directly into the Requests deque because AuthSrv lists
// don't have any hashes attached. WHY did eap think this was a good idea?!?!
{
hsTempMutexLock lock(patcher->fRequestMut);
for (unsigned i = 0; i < infoCount; ++i) {
PatcherLogYellow("\tEnqueuing Legacy File '%S'", infoArr[i].filename);
plFileName fn = plString::FromWchar(infoArr[i].filename);
plFileSystem::CreateDir(fn.StripFileName());
// We purposefully do NOT Open this stream! This uses a special auth-file constructor that
// utilizes a backing hsRAMStream. This will be fed to plStreamSource later...
pfPatcherStream* s = new pfPatcherStream(patcher, fn, infoArr[i].filesize);
pfPatcherWorker::Request req = pfPatcherWorker::Request(fn.AsString(), pfPatcherWorker::Request::kAuthFile, s);
patcher->fRequests.push_back(req);
}
}
patcher->IssueRequest();
} else {
PatcherLogRed("\tSHIT! Some legacy manifest phailed");
patcher->EndPatch(result, "SecurePreloader failed");
}
}
static void IHandleManifestDownload(pfPatcherWorker* patcher, const wchar_t group[], const NetCliFileManifestEntry manifest[], unsigned entryCount)
{
PatcherLogGreen("\tDownloaded Manifest '%S'", group);
{
hsTempMutexLock lock(patcher->fFileMut);
for (unsigned i = 0; i < entryCount; ++i)
patcher->fQueuedFiles.push_back(manifest[i]);
patcher->fFileSignal.Signal();
}
patcher->IssueRequest();
}
static void IPreloaderManifestDownloadCB(ENetError result, void* param, const wchar_t group[], const NetCliFileManifestEntry manifest[], unsigned entryCount)
{
pfPatcherWorker* patcher = static_cast<pfPatcherWorker*>(param);
if (IS_NET_SUCCESS(result))
IHandleManifestDownload(patcher, group, manifest, entryCount);
else {
PatcherLogYellow("\tWARNING: *** Falling back to AuthSrv file lists to get game code ***");
// so, we need to ask the AuthSrv about our game code
{
hsTempMutexLock lock(patcher->fRequestMut);
patcher->fRequests.push_back(pfPatcherWorker::Request(plString::Null, pfPatcherWorker::Request::kPythonList));
patcher->fRequests.push_back(pfPatcherWorker::Request(plString::Null, pfPatcherWorker::Request::kSdlList));
}
// continue pumping requests
patcher->IssueRequest();
}
}
static void IFileManifestDownloadCB(ENetError result, void* param, const wchar_t group[], const NetCliFileManifestEntry manifest[], unsigned entryCount)
{
pfPatcherWorker* patcher = static_cast<pfPatcherWorker*>(param);
if (IS_NET_SUCCESS(result))
IHandleManifestDownload(patcher, group, manifest, entryCount);
else {
PatcherLogRed("\tDownload Failed: Manifest '%S'", group);
patcher->EndPatch(result, plString::FromWchar(group));
}
}
static void IFileThingDownloadCB(ENetError result, void* param, const plFileName& filename, hsStream* writer)
{
pfPatcherWorker* patcher = static_cast<pfPatcherWorker*>(param);
pfPatcherStream* stream = static_cast<pfPatcherStream*>(writer);
stream->Close();
if (IS_NET_SUCCESS(result)) {
PatcherLogGreen("\tDownloaded File '%s'", stream->GetFileName().AsString().c_str());
patcher->WhitelistFile(stream->GetFileName(), true);
patcher->IssueRequest();
} else {
PatcherLogRed("\tDownloaded Failed: File '%s'", stream->GetFileName().AsString().c_str());
stream->Unlink();
patcher->EndPatch(result, filename.AsString());
}
delete stream;
}
// ===================================================
pfPatcherWorker::pfPatcherWorker() :
fStarted(false), fCurrBytes(0), fTotalBytes(0), fRequestActive(true)
{ }
pfPatcherWorker::~pfPatcherWorker()
{
{
hsTempMutexLock lock(fRequestMut);
std::for_each(fRequests.begin(), fRequests.end(),
[] (const Request& req) {
if (req.fStream) req.fStream->Close();
delete req.fStream;
}
);
fRequests.clear();
}
{
hsTempMutexLock lock(fFileMut);
fQueuedFiles.clear();
}
}
void pfPatcherWorker::OnQuit()
{
// the thread's Run() has exited sanely... now we can commit hara-kiri
delete fParent;
}
void pfPatcherWorker::EndPatch(ENetError result, const plString& msg)
{
// Guard against multiple calls
if (fStarted) {
// Send end status
if (fOnComplete)
fOnComplete(result, msg);
// yay log hax
if (IS_NET_SUCCESS(result))
PatcherLogWhite("--- Patch Complete ---");
else {
PatcherLogRed("\tNetwork Error: %S", NetErrorToString(result));
PatcherLogWhite("--- Patch Killed by Error ---");
}
}
fStarted = false;
fFileSignal.Signal();
}
bool pfPatcherWorker::IssueRequest()
{
hsTempMutexLock lock(fRequestMut);
if (fRequests.empty()) {
fRequestActive = false;
fFileSignal.Signal(); // make sure the patch thread doesn't deadlock!
return false;
} else
fRequestActive = true;
const Request& req = fRequests.front();
switch (req.fType) {
case Request::kFile:
req.fStream->Begin();
if (fFileBeginDownload)
fFileBeginDownload(req.fStream->GetFileName());
NetCliFileDownloadRequest(req.fName, req.fStream, IFileThingDownloadCB, this);
break;
case Request::kManifest:
NetCliFileManifestRequest(IFileManifestDownloadCB, this, req.fName.ToWchar());
break;
case Request::kSecurePreloader:
// so, yeah, this is usually the "SecurePreloader" manifest on the file server...
// except on legacy servers, this may not exist, so we need to fall back without nuking everything!
NetCliFileManifestRequest(IPreloaderManifestDownloadCB, this, req.fName.ToWchar());
break;
case Request::kAuthFile:
// ffffffuuuuuu
req.fStream->Begin();
if (fFileBeginDownload)
fFileBeginDownload(req.fStream->GetFileName());
NetCliAuthFileRequest(req.fName, req.fStream, IAuthThingDownloadCB, this);
break;
case Request::kPythonList:
NetCliAuthFileListRequest(L"Python", L"pak", IGotAuthFileList, this);
break;
case Request::kSdlList:
NetCliAuthFileListRequest(L"SDL", L"sdl", IGotAuthFileList, this);
break;
DEFAULT_FATAL(req.fType);
}
fRequests.pop_front();
return true;
}
hsError pfPatcherWorker::Run()
{
// So here's the rub:
// We have one or many manifests in the fRequests deque. We begin issuing those requests one-by one, starting here.
// As we receive the answer, the NetCli thread populates fQueuedFiles and pings the fFileSignal semaphore, then issues the next request...
// In this non-UI/non-Net thread, we do the stutter-prone/time-consuming IO/hashing operations. (Typically, the UI thread == Net thread)
// As we find files that need updating, we add them to fRequests.
// If there is no net request from ME when we find a file, we issue the request
// Once a file is downloaded, the next request is issued.
// When there are no files in my deque and no requests in my deque, we exit without errors.
PatcherLogWhite("--- Patch Started (%i requests) ---", fRequests.size());
fStarted = true;
IssueRequest();
// Now, work until we're done processing files
do {
fFileSignal.Wait();
hsTempMutexLock fileLock(fFileMut);
if (!fQueuedFiles.empty()) {
ProcessFile();
continue;
}
// This makes sure both queues are empty before exiting.
if (!fRequestActive)
if(!IssueRequest())
break;
} while (fStarted);
EndPatch(kNetSuccess);
return hsOK;
}
void pfPatcherWorker::ProcessFile()
{
do {
const NetCliFileManifestEntry& entry = fQueuedFiles.front();
// eap sucks
plFileName clName = plString::FromWchar(entry.clientName);
plString dlName = plString::FromWchar(entry.downloadName);
// Check to see if ours matches
plFileInfo mine(clName);
if (mine.FileSize() == entry.fileSize) {
plMD5Checksum cliMD5(clName);
plMD5Checksum srvMD5;
srvMD5.SetFromHexString(plString::FromWchar(entry.md5, 32).c_str());
if (cliMD5 == srvMD5) {
WhitelistFile(clName, false);
fQueuedFiles.pop_front();
continue;
}
}
// If you got here, they're different.
PatcherLogYellow("\tEnqueuing '%S'", entry.clientName);
plFileSystem::CreateDir(plFileName(clName).StripFileName());
pfPatcherStream* s = new pfPatcherStream(this, dlName, entry);
s->Open(clName, "wb");
hsTempMutexLock lock(fRequestMut);
fRequests.push_back(Request(dlName, Request::kFile, s));
fQueuedFiles.pop_front();
if (!fRequestActive)
IssueRequest();
} while (!fQueuedFiles.empty());
}
void pfPatcherWorker::WhitelistFile(const plFileName& file, bool justDownloaded, hsStream* stream)
{
// if this is a newly downloaded file, fire off a completion callback
if (justDownloaded && fFileDownloaded)
fFileDownloaded(file);
// we want to whitelist our game code, so here we go...
if (fGameCodeDiscovered) {
plString ext = file.GetFileExt();
if (ext.CompareI("pak") == 0 || ext.CompareI("sdl") == 0) {
if (!stream) {
stream = new hsUNIXStream;
stream->Open(file, "rb");
}
// if something terrible goes wrong (eg bad encryption), we can exit sanely
// callback eats stream
if (!fGameCodeDiscovered(file, stream))
EndPatch(kNetErrInternalError, "SecurePreloader failed.");
}
} else if (stream) {
// no dad gum memory leaks, m'kay?
stream->Close();
delete stream;
}
}
// ===================================================
plStatusLog* pfPatcher::GetLog()
{
static plStatusLog* log = nullptr;
if (!log)
{
log = plStatusLogMgr::GetInstance().CreateStatusLog(
20,
"patcher.log",
plStatusLog::kFilledBackground | plStatusLog::kAlignToTop | plStatusLog::kDeleteForMe);
}
return log;
}
pfPatcher::pfPatcher() : fWorker(new pfPatcherWorker) { }
pfPatcher::~pfPatcher() { }
// ===================================================
void pfPatcher::OnCompletion(CompletionFunc cb)
{
fWorker->fOnComplete = cb;
}
void pfPatcher::OnFileDownloadBegin(FileDownloadFunc cb)
{
fWorker->fFileBeginDownload = cb;
}
void pfPatcher::OnFileDownloaded(FileDownloadFunc cb)
{
fWorker->fFileDownloaded = cb;
}
void pfPatcher::OnGameCodeDiscovery(GameCodeDiscoverFunc cb)
{
fWorker->fGameCodeDiscovered = cb;
}
void pfPatcher::OnProgressTick(ProgressTickFunc cb)
{
fWorker->fProgressTick = cb;
}
// ===================================================
void pfPatcher::RequestGameCode()
{
hsTempMutexLock lock(fWorker->fRequestMut);
fWorker->fRequests.push_back(pfPatcherWorker::Request("SecurePreloader", pfPatcherWorker::Request::kSecurePreloader));
}
void pfPatcher::RequestManifest(const plString& mfs)
{
hsTempMutexLock lock(fWorker->fRequestMut);
fWorker->fRequests.push_back(pfPatcherWorker::Request(mfs, pfPatcherWorker::Request::kManifest));
}
void pfPatcher::RequestManifest(const std::vector<plString>& mfs)
{
hsTempMutexLock lock(fWorker->fRequestMut);
std::for_each(mfs.begin(), mfs.end(),
[&] (const plString& name) {
fWorker->fRequests.push_back(pfPatcherWorker::Request(name, pfPatcherWorker::Request::kManifest));
}
);
}
bool pfPatcher::Start()
{
hsAssert(!fWorker->fStarted, "pfPatcher is one-use only. kthx.");
if (!fWorker->fStarted) {
fWorker->fParent = this; // wheeeee circular
fWorker->Start();
return true;
}
return false;
}

View File

@ -0,0 +1,125 @@
/*==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 <http://www.gnu.org/licenses/>.
Additional permissions under GNU GPL version 3 section 7
If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.
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==*/
#ifndef _pfPatcher_inc_
#define _pfPatcher_inc_
#include <functional>
#include <memory>
#include <vector>
#include "plString.h"
#include "pnNetBase/pnNbError.h"
class plFileName;
class plStatusLog;
class hsStream;
/** Plasma File Patcher
* This is used to patch the client with one or many manifests at once. It assumes that
* we have permission to modify the game files, so be sure that you do! We memory manage
* ourselves, so allocate a new pfPatcher, add your manifests, and Start!
*/
class pfPatcher
{
std::unique_ptr<struct pfPatcherWorker> fWorker;
public:
static plStatusLog* GetLog();
public:
/** Represents a function that takes the status and an optional message on completion. */
typedef std::function<void(ENetError, const plString&)> CompletionFunc;
/** Represents a function that takes (const plFileName&) on an interesting file operation. */
typedef std::function<void(const plFileName&)> FileDownloadFunc;
/** Represents a function that takes (const plFileName&, hsStream*) on game code discovery.
* You are responsible for closing and deleting the provided stream.
*/
typedef std::function<bool(const plFileName&, hsStream*)> GameCodeDiscoverFunc;
/** Represents a function that takes (bytesDLed, totalBytes, statsStr) as a progress indicator. */
typedef std::function<void(uint64_t, uint64_t, const plString&)> ProgressTickFunc;
pfPatcher();
~pfPatcher();
/** Set a callback that will be fired when the patcher finishes its dirty work.
* \remarks This may be called from any thread, so make sure your callback is
* thread safe!
*/
void OnCompletion(CompletionFunc cb);
/** Set a callback that will be fired when the patcher issues a download request to the server.
* \remarks This will be called from the network thread.
*/
void OnFileDownloadBegin(FileDownloadFunc cb);
/** Set a callback that will be fired when the patcher has finished downloading a file from the server.
* \remarks This will be called from the network thread.
*/
void OnFileDownloaded(FileDownloadFunc cb);
/** This is called when the patcher discovers an up-to-date Python package or SDL file.
* \remarks This can be called from any thread when the patcher downloads or encounters an up-to-date
* python package or SDL file that the server knows about.
*/
void OnGameCodeDiscovery(GameCodeDiscoverFunc cb);
/** Set a callback that will be fired when the patcher receives a chunk from the server. The status string
* will contain the current download speed.
* \remarks This will be called from the network thread.
*/
void OnProgressTick(ProgressTickFunc cb);
void RequestGameCode();
void RequestManifest(const plString& mfs);
void RequestManifest(const std::vector<plString>& mfs);
/** Start patching the requested manifests. Please note that after calling this, you should
* discard your pointer to this object as it will memory-manage itself.
*/
bool Start();
};
#endif // _pfPatcher_inc_

View File

@ -0,0 +1,91 @@
/*==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 <http://www.gnu.org/licenses/>.
Additional permissions under GNU GPL version 3 section 7
If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.
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 "plManifests.h"
#include "plFileSystem.h"
// Helper that returns the appropriate string per build
#ifdef PLASMA_EXTERNAL_RELEASE
# define MANIFEST(in, ex) ex
#else
# define MANIFEST(in, ex) in
#endif // PLASMA_EXTERNAL_RELEASE
plFileName plManifest::ClientExecutable()
{
return MANIFEST("plClient.exe", "UruExplorer.exe");
}
plFileName plManifest::PatcherExecutable()
{
return MANIFEST("plUruLauncher.exe", "UruLauncher.exe");
}
plString plManifest::ClientManifest()
{
return MANIFEST("ThinInternal", "ThinExternal");
}
plString plManifest::ClientImageManifest()
{
return MANIFEST("Internal", "External");
}
plString plManifest::PatcherManifest()
{
return MANIFEST("InternalPatcher", "ExternalPatcher");
}
std::vector<plString> plManifest::EssentialGameManifests()
{
std::vector<plString> mfs;
mfs.push_back("CustomAvatars");
mfs.push_back("GlobalAnimations");
mfs.push_back("GlobalAvatars");
mfs.push_back("GlobalClothing");
mfs.push_back("GlobalMarkers");
mfs.push_back("GUI");
mfs.push_back("StartUp");
return mfs;
}

View File

@ -39,19 +39,34 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com
Mead, WA 99021
*==LICENSE==*/
/*****************************************************************************
*
* $/Plasma20/Sources/Plasma/FeatureLib/pfSecurePreloader/pfSecurePreloaderCreatable.h
*
***/
#ifndef PLASMA20_SOURCES_PLASMA_FEATURELIB_PFSECUREPRELOADER_PFSECUREPRELOADERCREATABLE_H
#define PLASMA20_SOURCES_PLASMA_FEATURELIB_PFSECUREPRELOADER_PFSECUREPRELOADERCREATABLE_H
#ifndef _plManifests_inc_
#define _plManifests_inc_
#include "pnFactory/plCreator.h"
#include <vector>
#include "pfSecurePreloader.h"
REGISTER_NONCREATABLE(pfSecurePreloader);
class plFileName;
class plString;
namespace plManifest
{
/** Get the name of the client executable for this build type.*/
plFileName ClientExecutable();
#endif // PLASMA20_SOURCES_PLASMA_FEATURELIB_PFSECUREPRELOADER_PFSECUREPRELOADERCREATABLE_H
/** Get the name of the patcher executable for this build type.*/
plFileName PatcherExecutable();
/** Get the name of the baseline client manifest for this build type. */
plString ClientManifest();
/** Get the name of the full game manifest for this build type. */
plString ClientImageManifest();
/** Get the name of the patcher manifest for this build type. */
plString PatcherManifest();
/** Get a vector containing all manifests the game requires to initialize. */
std::vector<plString> EssentialGameManifests();
}
#endif // _plManifests_inc_

View File

@ -1,22 +0,0 @@
include_directories(../../CoreLib)
include_directories(../../NucleusLib)
include_directories(../../NucleusLib/inc)
include_directories(../../PubUtilLib)
if(WIN32)
add_definitions(-DWIN32)
endif(WIN32)
set(pfSecurePreloader_SOURCES
pfSecurePreloader.cpp
)
set(pfSecurePreloader_HEADERS
pfSecurePreloader.h
pfSecurePreloaderCreatable.h
)
add_library(pfSecurePreloader STATIC ${pfSecurePreloader_SOURCES} ${pfSecurePreloader_HEADERS})
source_group("Source Files" FILES ${pfSecurePreloader_SOURCES})
source_group("Header Files" FILES ${pfSecurePreloader_HEADERS})

View File

@ -1,414 +0,0 @@
/*==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 <http://www.gnu.org/licenses/>.
Additional permissions under GNU GPL version 3 section 7
If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.
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 "pfSecurePreloader.h"
#include "hsStream.h"
#include "plgDispatch.h"
#include "plCompression/plZlibStream.h"
#include "pnEncryption/plChecksum.h"
#include "plFile/plSecureStream.h"
#include "plFile/plStreamSource.h"
#include "plMessage/plNetCommMsgs.h"
#include "plMessage/plPreloaderMsg.h"
#include "plProgressMgr/plProgressMgr.h"
bool gSkipPreload = false;
pfSecurePreloader* pfSecurePreloader::fInstance = nil;
/////////////////////////////////////////////////////////////////////
typedef std::pair<const wchar_t*, const wchar_t*> WcharPair;
struct AuthRequestParams
{
pfSecurePreloader* fThis;
std::queue<WcharPair> fFileGroups;
AuthRequestParams(pfSecurePreloader* parent)
: fThis(parent) { }
};
/////////////////////////////////////////////////////////////////////
void ProcAuthDownloadParams(AuthRequestParams* params);
void GotAuthSrvManifest(
ENetError result,
void* param,
const NetCliAuthFileInfo infoArr[],
uint32_t infoCount
) {
AuthRequestParams* arp = (AuthRequestParams*)param;
if (IS_NET_ERROR(result))
{
FATAL("Failed to get AuthSrv manifest!");
arp->fThis->Terminate();
delete arp;
} else {
arp->fThis->PreloadManifest(infoArr, infoCount);
ProcAuthDownloadParams(arp);
}
}
void GotFileSrvManifest(
ENetError result,
void* param,
const wchar_t group[],
const NetCliFileManifestEntry manifest[],
uint32_t entryCount
) {
pfSecurePreloader* sp = (pfSecurePreloader*)param;
if (result == kNetErrFileNotFound)
{
AuthRequestParams* params = new AuthRequestParams(sp);
params->fFileGroups.push(WcharPair(L"Python", L"pak"));
params->fFileGroups.push(WcharPair(L"SDL", L"sdl"));
ProcAuthDownloadParams(params);
return;
} else if (!entryCount) {
FATAL("SecurePreloader manifest empty!");
sp->Terminate();
return;
}
sp->PreloadManifest(manifest, entryCount);
}
void FileDownloaded(
ENetError result,
void* param,
const plFileName & filename,
hsStream* writer
) {
pfSecurePreloader* sp = (pfSecurePreloader*)param;
if (IS_NET_ERROR(result))
{
FATAL("SecurePreloader download failed");
sp->Terminate();
} else {
sp->FilePreloaded(filename, writer);
}
}
void ProcAuthDownloadParams(AuthRequestParams* params)
{
// Request the "manifests" until there are none left, then download the files
if (params->fFileGroups.empty())
{
params->fThis->PreloadNextFile();
delete params;
} else {
WcharPair wp = params->fFileGroups.front();
params->fFileGroups.pop();
NetCliAuthFileListRequest(wp.first, wp.second, GotAuthSrvManifest, params);
}
}
/////////////////////////////////////////////////////////////////////
class pfSecurePreloaderStream : public plZlibStream
{
plOperationProgress* fProgress;
bool fIsZipped;
public:
pfSecurePreloaderStream(plOperationProgress* prog, bool zipped)
: fProgress(prog), fIsZipped(zipped), plZlibStream()
{
fOutput = new hsRAMStream;
}
~pfSecurePreloaderStream()
{
delete fOutput;
fOutput = nil;
plZlibStream::Close();
}
bool AtEnd() { return fOutput->AtEnd(); }
uint32_t GetEOF() { return fOutput->GetEOF(); }
uint32_t GetPosition() const { return fOutput->GetPosition(); }
uint32_t GetSizeLeft() const { return fOutput->GetSizeLeft(); }
uint32_t Read(uint32_t count, void* buf) { return fOutput->Read(count, buf); }
void Rewind() { fOutput->Rewind(); }
void SetPosition(uint32_t pos) { fOutput->SetPosition(pos); }
void Skip(uint32_t deltaByteCount) { fOutput->Skip(deltaByteCount); }
uint32_t Write(uint32_t count, const void* buf)
{
if (fProgress)
fProgress->Increment((float)count);
if (fIsZipped)
return plZlibStream::Write(count, buf);
else
return fOutput->Write(count, buf);
}
};
/////////////////////////////////////////////////////////////////////
hsRAMStream* pfSecurePreloader::LoadToMemory(const plFileName& file) const
{
if (!plFileInfo(file).Exists())
return nil;
hsUNIXStream s;
hsRAMStream* ram = new hsRAMStream;
s.Open(file);
uint32_t loadLen = 1024 * 1024;
uint8_t* buf = new uint8_t[loadLen];
while (uint32_t read = s.Read(loadLen, buf))
ram->Write(read, buf);
delete[] buf;
s.Close();
ram->Rewind();
return ram;
}
void pfSecurePreloader::SaveFile(hsStream* file, const plFileName& name) const
{
hsUNIXStream s;
s.Open(name, "wb");
uint32_t pos = file->GetPosition();
file->Rewind();
uint32_t loadLen = 1024 * 1024;
uint8_t* buf = new uint8_t[loadLen];
while (uint32_t read = file->Read(loadLen, buf))
s.Write(read, buf);
file->SetPosition(pos);
s.Close();
delete[] buf;
}
bool pfSecurePreloader::IsZipped(const plFileName& filename) const
{
return filename.GetFileExt().CompareI("gz") == 0;
}
void pfSecurePreloader::PreloadNextFile()
{
if (fManifestEntries.empty())
{
Finish();
return;
}
plFileName filename = fDownloadEntries.front();
hsStream* s = new pfSecurePreloaderStream(fProgress, IsZipped(filename));
// Thankfully, both callbacks have the same arguments
if (fLegacyMode)
NetCliAuthFileRequest(filename, s, FileDownloaded, this);
else
NetCliFileDownloadRequest(filename, s, FileDownloaded, this);
}
void pfSecurePreloader::Init()
{
if (!fInstance)
fInstance = new pfSecurePreloader;
fInstance->RegisterAs(kSecurePreloader_KEY);
// TODO: If we're going to support reconnects, then let's do it right.
// Later...
//plgDispatch::Dispatch()->RegisterForExactType(plNetCommAuthConnectedMsg::Index(), fInstance->GetKey());
}
void pfSecurePreloader::Start()
{
#ifndef PLASMA_EXTERNAL_RELEASE
// Finer grained control of the preloader allows us to have synched data but our own python/SDL
// This is useful on outdated/black-box shards like MOULa
if (gSkipPreload)
{
Finish();
return;
}
#endif
NetCliAuthGetEncryptionKey(fEncryptionKey, 4);
// TODO: Localize
fProgress = plProgressMgr::GetInstance()->RegisterOperation(0.0f, "Checking for updates", plProgressMgr::kUpdateText, false, true);
// Now, we need to fetch the "SecurePreloader" manifest from the file server, which will contain the python and SDL files.
// We're basically reimplementing plResPatcher here, except preferring to keep everything in memory, then flush to disk
// when we're done. If this fails, then we shall download everything from the AuthSrv like in the old days.
NetCliFileManifestRequest(GotFileSrvManifest, this, L"SecurePreloader");
}
void pfSecurePreloader::Terminate()
{
FATAL("pfSecurePreloader failure");
fProgress->SetAborting();
plPreloaderMsg* msg = new plPreloaderMsg;
msg->fSuccess = false;
plgDispatch::Dispatch()->MsgSend(msg);
}
void pfSecurePreloader::Finish()
{
plPreloaderMsg* msg = new plPreloaderMsg;
msg->fSuccess = true;
plgDispatch::Dispatch()->MsgSend(msg);
}
void pfSecurePreloader::Shutdown()
{
SetInstance(nil);
if (fProgress)
{
delete fProgress;
fProgress = nil;
}
// Takes care of UnReffing us
UnRegisterAs(kSecurePreloader_KEY);
}
void pfSecurePreloader::PreloadManifest(const NetCliAuthFileInfo manifestEntries[], uint32_t entryCount)
{
uint32_t totalBytes = 0;
if (fProgress)
totalBytes = (uint32_t)fProgress->GetMax();
fLegacyMode = true;
for (uint32_t i = 0; i < entryCount; ++i)
{
const NetCliAuthFileInfo mfs = manifestEntries[i];
plFileName filename = plString::FromWchar(mfs.filename);
fDownloadEntries.push(filename);
if (IsZipped(filename))
fManifestEntries.push(filename.StripFileExt());
else
fManifestEntries.push(filename);
totalBytes += mfs.filesize;
}
if (fProgress)
{
fProgress->SetLength((float)totalBytes);
fProgress->SetTitle("Downloading...");
}
}
void pfSecurePreloader::PreloadManifest(const NetCliFileManifestEntry manifestEntries[], uint32_t entryCount)
{
uint32_t totalBytes = 0;
for (uint32_t i = 0; i < entryCount; ++i)
{
const NetCliFileManifestEntry mfs = manifestEntries[i];
bool fetchMe = true;
hsRAMStream* s = nil;
plFileName clientName = plString::FromWchar(mfs.clientName);
plFileName downloadName = plString::FromWchar(mfs.downloadName);
if (plFileInfo(clientName).Exists())
{
s = LoadToMemory(clientName);
if (s)
{
// Damn this
plMD5Checksum srvHash;
srvHash.SetFromHexString(plString::FromWchar(mfs.md5, 32).c_str());
// Now actually copare the hashes
plMD5Checksum lclHash;
lclHash.CalcFromStream(s);
fetchMe = (srvHash != lclHash);
}
}
if (fetchMe)
{
fManifestEntries.push(clientName);
fDownloadEntries.push(downloadName);
if (IsZipped(downloadName))
totalBytes += mfs.zipSize;
else
totalBytes += mfs.fileSize;
} else {
plSecureStream* ss = new plSecureStream(s, fEncryptionKey);
plStreamSource::GetInstance()->InsertFile(clientName, ss);
}
if (s)
delete s;
}
if (totalBytes && fProgress)
{
fProgress->SetLength((float)totalBytes);
fProgress->SetTitle("Downloading...");
}
// This method uses only one manifest, so we're good to go now!
PreloadNextFile();
}
void pfSecurePreloader::FilePreloaded(const plFileName& file, hsStream* stream)
{
// Clear out queue
fDownloadEntries.pop();
plFileName clientName = fManifestEntries.front();
fManifestEntries.pop();
if (!fLegacyMode) // AuthSrv data caching is useless
{
plFileSystem::CreateDir(clientName.StripFileName(), true);
SaveFile(stream, clientName);
}
plSecureStream* ss = new plSecureStream(stream, fEncryptionKey);
plStreamSource::GetInstance()->InsertFile(clientName, ss);
delete stream; // SecureStream holds its own decrypted buffer
// Continue down the warpath
PreloadNextFile();
}

View File

@ -1,96 +0,0 @@
/*==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 <http://www.gnu.org/licenses/>.
Additional permissions under GNU GPL version 3 section 7
If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.
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==*/
#ifndef __pfSecurePreloader_h__
#define __pfSecurePreloader_h__
#include "HeadSpin.h"
#include "pnKeyedObject/hsKeyedObject.h"
#include "plNetGameLib/plNetGameLib.h"
#include <queue>
class plOperationProgress;
class hsRAMStream;
///////////////////////////////////////////////////////////////////////////////
// pfSecurePreloader - a class for handling files we want downloaded from the
// server into a temporary directory, secured, and deleted on exit. Puts stuff
// into plStreamSource for us
///////////////////////////////////////////////////////////////////////////////
class pfSecurePreloader : public hsKeyedObject
{
private:
static pfSecurePreloader* fInstance;
std::queue<plFileName> fManifestEntries;
std::queue<plFileName> fDownloadEntries;
plOperationProgress* fProgress;
uint32_t fEncryptionKey[4];
bool fLegacyMode;
hsRAMStream* LoadToMemory(const plFileName& file) const;
void SaveFile(hsStream* file, const plFileName& name) const;
bool IsZipped(const plFileName& filename) const;
public:
pfSecurePreloader() : fProgress(nil), fLegacyMode(false) { }
CLASSNAME_REGISTER(pfSecurePreloader);
GETINTERFACE_ANY(pfSecurePreloader, hsKeyedObject);
void Init();
void Start();
void Terminate();
void Finish();
void Shutdown();
void PreloadManifest(const NetCliFileManifestEntry manifestEntries[], uint32_t entryCount);
void PreloadManifest(const NetCliAuthFileInfo manifestEntries[], uint32_t entryCount);
void PreloadNextFile();
void FilePreloaded(const plFileName& filename, hsStream* stream);
plOperationProgress* GetProgressBar() { return fProgress; }
static pfSecurePreloader* GetInstance() { return fInstance; }
static void SetInstance(pfSecurePreloader* instance) { fInstance = instance; }
};
#endif // __pfSecurePreloader_h__