You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
485 lines
15 KiB
485 lines
15 KiB
/*==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 "HeadSpin.h" |
|
#include "plClientLauncher.h" |
|
#include "plFileSystem.h" |
|
#include "plProduct.h" |
|
#include "hsThread.h" |
|
#include "hsTimer.h" |
|
#include "plCmdParser.h" |
|
|
|
#include "pnUtils/pnUtils.h" |
|
#include "pnAsyncCore/pnAsyncCore.h" |
|
#include "plNetGameLib/plNetGameLib.h" |
|
#include "plStatusLog/plStatusLog.h" |
|
|
|
#include "pfPatcher/plManifests.h" |
|
#include "pfPatcher/pfPatcher.h" |
|
|
|
#include "pfConsoleCore/pfConsoleEngine.h" |
|
PF_CONSOLE_LINK_FILE(Core) |
|
|
|
#include <algorithm> |
|
#include <curl/curl.h> |
|
#include <deque> |
|
|
|
plClientLauncher::ErrorFunc s_errorProc = nullptr; // don't even ask, cause I'm not happy about this. |
|
|
|
const int kNetTransTimeout = 5 * 60 * 1000; // 5m |
|
const int kShardStatusUpdateTime = 5; // 5s |
|
const int kAsyncCoreShutdownTime = 2 * 1000; // 2s |
|
const int kNetCoreUpdateSleepTime = 10; // 10ms |
|
|
|
// =================================================== |
|
|
|
class plShardStatus : public hsThread |
|
{ |
|
double fLastUpdate; |
|
volatile bool fRunning; |
|
hsEvent fUpdateEvent; |
|
char fCurlError[CURL_ERROR_SIZE]; |
|
|
|
public: |
|
plClientLauncher::StatusFunc fShardFunc; |
|
|
|
plShardStatus() : |
|
fRunning(true), fLastUpdate(0) |
|
{ } |
|
|
|
virtual hsError Run(); |
|
void Shutdown(); |
|
void Update(); |
|
}; |
|
|
|
static size_t ICurlCallback(void* buffer, size_t size, size_t nmemb, void* thread) |
|
{ |
|
static char status[256]; |
|
|
|
strncpy(status, (const char *)buffer, std::min<size_t>(size * nmemb, arrsize(status))); |
|
status[arrsize(status) - 1] = 0; |
|
static_cast<plShardStatus*>(thread)->fShardFunc(status); |
|
return size * nmemb; |
|
} |
|
|
|
hsError plShardStatus::Run() |
|
{ |
|
{ |
|
plString url = GetServerStatusUrl(); |
|
|
|
// initialize CURL |
|
std::unique_ptr<CURL, std::function<void(CURL*)>> curl(curl_easy_init(), curl_easy_cleanup); |
|
curl_easy_setopt(curl.get(), CURLOPT_ERRORBUFFER, fCurlError); |
|
curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, "UruClient/1.0"); |
|
curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); |
|
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, this); |
|
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, ICurlCallback); |
|
|
|
// we want to go ahead and run once |
|
fUpdateEvent.Signal(); |
|
|
|
// loop until we die! |
|
do |
|
{ |
|
fUpdateEvent.Wait(); |
|
if (!fRunning) |
|
break; |
|
|
|
if (!url.IsEmpty() && curl_easy_perform(curl.get())) |
|
fShardFunc(fCurlError); |
|
fLastUpdate = hsTimer::GetSysSeconds(); |
|
} while (fRunning); |
|
} |
|
|
|
return hsOK; |
|
} |
|
|
|
void plShardStatus::Shutdown() |
|
{ |
|
fRunning = false; |
|
fUpdateEvent.Signal(); |
|
} |
|
|
|
void plShardStatus::Update() |
|
{ |
|
double now = hsTimer::GetSysSeconds(); |
|
if ((now - fLastUpdate) >= kShardStatusUpdateTime) |
|
fUpdateEvent.Signal(); |
|
} |
|
|
|
// =================================================== |
|
|
|
class plRedistUpdater : public hsThread |
|
{ |
|
bool fSuccess; |
|
|
|
public: |
|
plClientLauncher* fParent; |
|
plClientLauncher::InstallRedistFunc fInstallProc; |
|
std::deque<plFileName> fRedistQueue; |
|
|
|
plRedistUpdater() |
|
: fSuccess(true) |
|
{ } |
|
|
|
~plRedistUpdater() |
|
{ |
|
// If anything is left in the deque, it was not installed. |
|
// We should unlink them so the next launch will redownload and install them. |
|
std::for_each(fRedistQueue.begin(), fRedistQueue.end(), |
|
[] (const plFileName& file) { |
|
plFileSystem::Unlink(file); |
|
} |
|
); |
|
} |
|
|
|
virtual void OnQuit() |
|
{ |
|
// If we succeeded, then we should launch the game client... |
|
if (fSuccess) |
|
fParent->LaunchClient(); |
|
} |
|
|
|
virtual hsError Run() |
|
{ |
|
while (!fRedistQueue.empty()) { |
|
if (fInstallProc(fRedistQueue.back())) |
|
fRedistQueue.pop_back(); |
|
else { |
|
s_errorProc(kNetErrInternalError, fRedistQueue.back().AsString()); |
|
fSuccess = false; |
|
return hsFail; |
|
} |
|
} |
|
return hsOK; |
|
} |
|
|
|
virtual void Start() |
|
{ |
|
if (fRedistQueue.empty()) |
|
OnQuit(); |
|
else |
|
hsThread::Start(); |
|
} |
|
}; |
|
|
|
// =================================================== |
|
|
|
plClientLauncher::plClientLauncher() : |
|
fFlags(0), |
|
fServerIni("server.ini"), |
|
fPatcherFactory(nullptr), |
|
fClientExecutable(plManifest::ClientExecutable()), |
|
fStatusThread(new plShardStatus()), |
|
fInstallerThread(new plRedistUpdater()) |
|
{ |
|
pfPatcher::GetLog()->AddLine(plProduct::ProductString().c_str()); |
|
} |
|
|
|
plClientLauncher::~plClientLauncher() { } |
|
|
|
// =================================================== |
|
|
|
plString plClientLauncher::GetAppArgs() const |
|
{ |
|
// If -Repair was specified, there are no args for the next call... |
|
if (hsCheckBits(fFlags, kRepairGame)) { |
|
return ""; |
|
} |
|
|
|
plStringStream ss; |
|
ss << "-ServerIni="; |
|
ss << fServerIni.AsString(); |
|
|
|
// optional args |
|
if (hsCheckBits(fFlags, kClientImage)) |
|
ss << " -Image"; |
|
if (hsCheckBits(fFlags, kPatchOnly)) |
|
ss << " -PatchOnly"; |
|
if (hsCheckBits(fFlags, kSkipLoginDialog)) |
|
ss << " -SkipLoginDialog"; |
|
|
|
return ss.GetString(); |
|
} |
|
|
|
void plClientLauncher::IOnPatchComplete(ENetError result, const plString& msg) |
|
{ |
|
if (IS_NET_SUCCESS(result)) { |
|
// a couple of options |
|
// 1. we self-patched and didn't update anything. patch the main client. |
|
// 2. we self-patched and did things and stuff... re-run myself. |
|
// 3. we patched the client... run it. |
|
if (!hsCheckBits(fFlags, kHaveSelfPatched) && (fClientExecutable == plManifest::ClientExecutable())) { |
|
// case 1 |
|
hsSetBits(fFlags, kHaveSelfPatched); |
|
PatchClient(); |
|
} else { |
|
// cases 2 & 3 -- update any redistributables, then launch the client. |
|
fInstallerThread->fParent = this; |
|
fInstallerThread->Start(); |
|
} |
|
} else if (s_errorProc) |
|
s_errorProc(result, msg); |
|
} |
|
|
|
bool plClientLauncher::IApproveDownload(const plFileName& file) |
|
{ |
|
// So, for a repair, what we want to do is quite simple. |
|
// That is: download everything that is NOT in the root directory. |
|
plFileName path = file.StripFileName(); |
|
return !path.AsString().IsEmpty(); |
|
} |
|
|
|
void plClientLauncher::LaunchClient() const |
|
{ |
|
if (fStatusFunc) |
|
fStatusFunc("Launching..."); |
|
fLaunchClientFunc(fClientExecutable, GetAppArgs()); |
|
} |
|
|
|
void plClientLauncher::PatchClient() |
|
{ |
|
if (fStatusFunc) { |
|
if (hsCheckBits(fFlags, kGameDataOnly)) |
|
fStatusFunc("Verifying game data..."); |
|
else |
|
fStatusFunc("Checking for updates..."); |
|
} |
|
hsAssert(fPatcherFactory, "why is the patcher factory nil?"); |
|
|
|
pfPatcher* patcher = fPatcherFactory(); |
|
patcher->OnCompletion(std::bind(&plClientLauncher::IOnPatchComplete, this, std::placeholders::_1, std::placeholders::_2)); |
|
patcher->OnSelfPatch([&](const plFileName& file) { fClientExecutable = file; }); |
|
patcher->OnRedistUpdate([&](const plFileName& file) { fInstallerThread->fRedistQueue.push_back(file); }); |
|
|
|
// If this is a repair, we need to approve the downloads... |
|
if (hsCheckBits(fFlags, kGameDataOnly)) |
|
patcher->OnFileDownloadDesired(std::bind(&plClientLauncher::IApproveDownload, this, std::placeholders::_1)); |
|
|
|
// Let's get 'er done. |
|
if (hsCheckBits(fFlags, kHaveSelfPatched)) { |
|
if (hsCheckBits(fFlags, kClientImage)) |
|
patcher->RequestManifest(plManifest::ClientImageManifest()); |
|
else |
|
patcher->RequestManifest(plManifest::ClientManifest()); |
|
} else |
|
patcher->RequestManifest(plManifest::PatcherManifest()); |
|
patcher->Start(); |
|
} |
|
|
|
bool plClientLauncher::CompleteSelfPatch(std::function<void(void)> waitProc) const |
|
{ |
|
if (hsCheckBits(fFlags, kHaveSelfPatched)) |
|
return false; |
|
|
|
plString myExe = plFileSystem::GetCurrentAppPath().GetFileName(); |
|
if (myExe.CompareI(plManifest::PatcherExecutable().AsString()) != 0) { |
|
waitProc(); |
|
|
|
// so now we need to unlink the old patcher, and move ME into that fool's place... |
|
// then we can continue on our merry way! |
|
if (!plFileSystem::Unlink(plManifest::PatcherExecutable())) { |
|
hsMessageBox("Failed to delete old patcher executable!", "Error", hsMessageBoxNormal, hsMessageBoxIconError); |
|
return true; |
|
} |
|
if (!plFileSystem::Move(plFileSystem::GetCurrentAppPath(), plManifest::PatcherExecutable())) { |
|
hsMessageBox("Failed to move patcher executable!", "Error", hsMessageBoxNormal, hsMessageBoxIconError); |
|
return true; |
|
} |
|
|
|
// Now, execute the new patcher... |
|
fLaunchClientFunc(plManifest::PatcherExecutable(), GetAppArgs() + " -NoSelfPatch"); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
// =================================================== |
|
|
|
static void IGotFileServIPs(ENetError result, void* param, const plString& addr) |
|
{ |
|
plClientLauncher* launcher = static_cast<plClientLauncher*>(param); |
|
NetCliGateKeeperDisconnect(); |
|
|
|
if (IS_NET_SUCCESS(result)) { |
|
// bah... why do I even bother |
|
plString eapSucks[] = { addr }; |
|
NetCliFileStartConnect(eapSucks, 1, true); |
|
|
|
// Who knows if we will actually connect. So let's start updating. |
|
launcher->PatchClient(); |
|
} else if (s_errorProc) |
|
s_errorProc(result, "Failed to get FileServ addresses"); |
|
} |
|
|
|
static void IEapSucksErrorProc(ENetProtocol protocol, ENetError error) |
|
{ |
|
if (s_errorProc) { |
|
plString msg = plFormat("Protocol: {}", NetProtocolToString(protocol)); |
|
s_errorProc(error, msg); |
|
} |
|
} |
|
|
|
void plClientLauncher::InitializeNetCore() |
|
{ |
|
// initialize shard status |
|
hsTimer::SetRealTime(true); |
|
fStatusThread->Start(); |
|
|
|
// init eap... |
|
AsyncCoreInitialize(); |
|
|
|
NetClientInitialize(); |
|
NetClientSetErrorHandler(IEapSucksErrorProc); |
|
NetClientSetTransTimeoutMs(kNetTransTimeout); |
|
|
|
// Gotta grab the filesrvs from the gate |
|
const plString* addrs; |
|
uint32_t num = GetGateKeeperSrvHostnames(addrs); |
|
|
|
NetCliGateKeeperStartConnect(addrs, num); |
|
NetCliGateKeeperFileSrvIpAddressRequest(IGotFileServIPs, this, true); |
|
} |
|
|
|
// =================================================== |
|
|
|
void plClientLauncher::PumpNetCore() const |
|
{ |
|
// this ain't net core, but it needs to be pumped :( |
|
hsTimer::IncSysSeconds(); |
|
|
|
// pump eap |
|
NetClientUpdate(); |
|
|
|
// pump shard status |
|
fStatusThread->Update(); |
|
|
|
// don't nom all the CPU... kthx |
|
hsSleep::Sleep(kNetCoreUpdateSleepTime); |
|
} |
|
|
|
void plClientLauncher::ShutdownNetCore() const |
|
{ |
|
// shutdown shard status |
|
fStatusThread->Shutdown(); |
|
|
|
// unhook the neterr callback at this point because all transactions |
|
// will fail when we call NetClientDestroy |
|
s_errorProc = nullptr; |
|
|
|
// shutdown eap |
|
NetCliGateKeeperDisconnect(); |
|
NetCliFileDisconnect(); |
|
NetClientDestroy(); |
|
|
|
// shutdown eap (part deux) |
|
AsyncCoreDestroy(kAsyncCoreShutdownTime); |
|
} |
|
|
|
// =================================================== |
|
|
|
bool plClientLauncher::LoadServerIni() const |
|
{ |
|
PF_CONSOLE_INITIALIZE(Core); |
|
|
|
pfConsoleEngine console; |
|
return console.ExecuteFile(fServerIni); |
|
} |
|
|
|
void plClientLauncher::ParseArguments() |
|
{ |
|
#define APPLY_FLAG(arg, flag) \ |
|
if (cmdParser.GetBool(arg)) \ |
|
fFlags |= flag; |
|
|
|
enum { kArgServerIni, kArgNoSelfPatch, kArgImage, kArgRepairGame, kArgPatchOnly, |
|
kArgSkipLoginDialog }; |
|
const plCmdArgDef cmdLineArgs[] = { |
|
{ kCmdArgFlagged | kCmdTypeString, "ServerIni", kArgServerIni }, |
|
{ kCmdArgFlagged | kCmdTypeBool, "NoSelfPatch", kArgNoSelfPatch }, |
|
{ kCmdArgFlagged | kCmdTypeBool, "Image", kArgImage }, |
|
{ kCmdArgFlagged | kCmdTypeBool, "Repair", kArgRepairGame }, |
|
{ kCmdArgFlagged | kCmdTypeBool, "PatchOnly", kArgPatchOnly }, |
|
{ kCmdArgFlagged | kCmdTypeBool, "SkipLoginDialog", kArgSkipLoginDialog } |
|
}; |
|
|
|
std::vector<plString> args; |
|
args.reserve(__argc); |
|
for (size_t i = 0; i < __argc; i++) { |
|
args.push_back(plString::FromUtf8(__argv[i])); |
|
} |
|
|
|
plCmdParser cmdParser(cmdLineArgs, arrsize(cmdLineArgs)); |
|
cmdParser.Parse(args); |
|
|
|
// cache 'em |
|
if (cmdParser.IsSpecified(kArgServerIni)) |
|
fServerIni = cmdParser.GetString(kArgServerIni); |
|
APPLY_FLAG(kArgNoSelfPatch, kHaveSelfPatched); |
|
APPLY_FLAG(kArgImage, kClientImage); |
|
APPLY_FLAG(kArgRepairGame, kRepairGame); |
|
APPLY_FLAG(kArgPatchOnly, kPatchOnly); |
|
APPLY_FLAG(kArgSkipLoginDialog, kSkipLoginDialog); |
|
|
|
// last chance setup |
|
if (hsCheckBits(fFlags, kPatchOnly)) |
|
fClientExecutable = ""; |
|
else if (hsCheckBits(fFlags, kRepairGame)) |
|
fClientExecutable = plManifest::PatcherExecutable(); |
|
|
|
#undef APPLY_FLAG |
|
} |
|
|
|
void plClientLauncher::SetErrorProc(ErrorFunc proc) |
|
{ |
|
s_errorProc = proc; |
|
} |
|
|
|
void plClientLauncher::SetInstallerProc(InstallRedistFunc proc) |
|
{ |
|
fInstallerThread->fInstallProc = proc; |
|
} |
|
|
|
void plClientLauncher::SetShardProc(StatusFunc proc) |
|
{ |
|
fStatusThread->fShardFunc = proc; |
|
}
|
|
|