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.
430 lines
14 KiB
430 lines
14 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 "plFileSystem.h" |
|
#include "plProduct.h" |
|
|
|
#include "pfPatcher/plManifests.h" |
|
#include "pfPatcher/pfPatcher.h" |
|
|
|
#include "plClientLauncher.h" |
|
|
|
#include "hsWindows.h" |
|
#include "resource.h" |
|
#include <commctrl.h> |
|
#include <shellapi.h> |
|
#include <shlobj.h> |
|
|
|
// =================================================== |
|
|
|
#define PLASMA_PHAILURE 1 |
|
#define PLASMA_OK 0 |
|
|
|
static HWND s_dialog; |
|
static plString s_error; // This is highly unfortunate. |
|
static plClientLauncher s_launcher; |
|
static UINT s_taskbarCreated = RegisterWindowMessageW(L"TaskbarButtonCreated"); |
|
static ITaskbarList3* s_taskbar = nullptr; |
|
|
|
typedef std::unique_ptr<void, std::function<BOOL(HANDLE)>> handleptr_t; |
|
|
|
// =================================================== |
|
|
|
/** Create a global patcher mutex that is backwards compatible with eap's */ |
|
static handleptr_t CreatePatcherMutex() |
|
{ |
|
return handleptr_t(CreateMutexW(nullptr, TRUE, plManifest::PatcherExecutable().AsString().ToWchar()), |
|
CloseHandle); |
|
} |
|
|
|
static bool IsPatcherRunning() |
|
{ |
|
handleptr_t mut = CreatePatcherMutex(); |
|
return WaitForSingleObject(mut.get(), 0) != WAIT_OBJECT_0; |
|
} |
|
|
|
static void WaitForOldPatcher() |
|
{ |
|
handleptr_t mut = CreatePatcherMutex(); |
|
DWORD wait = WaitForSingleObject(mut.get(), 0); |
|
while (wait != WAIT_OBJECT_0) // :( :( :( |
|
wait = WaitForSingleObject(mut.get(), 100); |
|
Sleep(1000); // :( |
|
} |
|
|
|
// =================================================== |
|
|
|
static inline void IShowErrorDialog(const wchar_t* msg) |
|
{ |
|
// This bypasses all that hsClientMinimizeGuard crap we have in CoreLib. |
|
MessageBoxW(nullptr, msg, L"Error", MB_ICONERROR | MB_OK); |
|
} |
|
|
|
static inline void IQuit(int exitCode=PLASMA_OK) |
|
{ |
|
// hey, guess what? |
|
// PostQuitMessage doesn't work if you're not on the main thread... |
|
PostMessageW(s_dialog, WM_QUIT, exitCode, 0); |
|
} |
|
|
|
static inline void IShowMarquee(bool marquee=true) |
|
{ |
|
// NOTE: This is a HACK to workaround a bug that causes progress bars that were ever |
|
// marquees to reanimate when changing the range or position |
|
ShowWindow(GetDlgItem(s_dialog, IDC_MARQUEE), marquee ? SW_SHOW : SW_HIDE); |
|
ShowWindow(GetDlgItem(s_dialog, IDC_PROGRESS), marquee ? SW_HIDE : SW_SHOW); |
|
PostMessageW(GetDlgItem(s_dialog, IDC_MARQUEE), PBM_SETMARQUEE, static_cast<WPARAM>(marquee), 0); |
|
} |
|
|
|
BOOL CALLBACK PatcherDialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) |
|
{ |
|
// NT6 Taskbar Majick |
|
if (uMsg == s_taskbarCreated) { |
|
if (s_taskbar) |
|
s_taskbar->Release(); |
|
HRESULT result = CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_ALL, IID_ITaskbarList3, (void**)&s_taskbar); |
|
if (FAILED(result)) |
|
s_taskbar = nullptr; |
|
} |
|
|
|
switch (uMsg) { |
|
case WM_COMMAND: |
|
// Did they press cancel? |
|
if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDCANCEL) { |
|
EnableWindow(GetDlgItem(s_dialog, IDCANCEL), false); |
|
SetWindowTextW(GetDlgItem(s_dialog, IDC_TEXT), L"Shutting Down..."); |
|
IQuit(); |
|
} |
|
break; |
|
case WM_DESTROY: |
|
if (s_taskbar) |
|
s_taskbar->Release(); |
|
PostQuitMessage(PLASMA_OK); |
|
s_dialog = nullptr; |
|
break; |
|
case WM_NCHITTEST: |
|
SetWindowLongW(hwndDlg, DWL_MSGRESULT, (LONG_PTR)HTCAPTION); |
|
return TRUE; |
|
case WM_QUIT: |
|
s_launcher.ShutdownNetCore(); |
|
DestroyWindow(hwndDlg); |
|
break; |
|
} |
|
|
|
return DefWindowProcW(hwndDlg, uMsg, wParam, lParam); |
|
} |
|
|
|
static void ShowPatcherDialog(HINSTANCE hInstance) |
|
{ |
|
s_dialog = ::CreateDialogW(hInstance, MAKEINTRESOURCEW(IDD_DIALOG), nullptr, PatcherDialogProc); |
|
SetDlgItemTextW(s_dialog, IDC_TEXT, L"Connecting..."); |
|
SetDlgItemTextW(s_dialog, IDC_PRODUCTSTRING, plProduct::ProductString().ToWchar()); |
|
SetDlgItemTextW(s_dialog, IDC_DLSIZE, L""); |
|
SetDlgItemTextW(s_dialog, IDC_DLSPEED, L""); |
|
IShowMarquee(); |
|
} |
|
|
|
static void PumpMessages() |
|
{ |
|
MSG msg; |
|
do { |
|
// Pump all Win32 messages |
|
while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE)) { |
|
if (!IsDialogMessageW(s_dialog, &msg)) { |
|
TranslateMessage(&msg); |
|
DispatchMessageW(&msg); |
|
} |
|
} |
|
|
|
// Now we need to pump the netcore while we have some spare time... |
|
s_launcher.PumpNetCore(); |
|
} while (msg.message != WM_QUIT); |
|
} |
|
|
|
// =================================================== |
|
|
|
static void IOnDownloadBegin(const plFileName& file) |
|
{ |
|
plString msg = plFormat("Downloading... {}", file); |
|
SetDlgItemTextW(s_dialog, IDC_TEXT, msg.ToWchar()); |
|
} |
|
|
|
static void IOnProgressTick(uint64_t curBytes, uint64_t totalBytes, const plString& status) |
|
{ |
|
// Swap marquee/real progress |
|
IShowMarquee(false); |
|
|
|
// DL size |
|
plString size = plFormat("{} / {}", plFileSystem::ConvertFileSize(curBytes), |
|
plFileSystem::ConvertFileSize(totalBytes)); |
|
SetDlgItemTextW(s_dialog, IDC_DLSIZE, size.ToWchar()); |
|
|
|
// DL speed |
|
SetDlgItemTextW(s_dialog, IDC_DLSPEED, status.ToWchar()); |
|
HWND progress = GetDlgItem(s_dialog, IDC_PROGRESS); |
|
|
|
// hey look... ULONGLONG. that's exactly what we need >.< |
|
if (s_taskbar) |
|
s_taskbar->SetProgressValue(s_dialog, curBytes, totalBytes); |
|
|
|
// Windows can only do signed 32-bit int progress bars. |
|
// So, chop it into smaller chunks until we get something we can represent. |
|
while (totalBytes > INT32_MAX) { |
|
totalBytes /= 1024; |
|
curBytes /= 1024; |
|
} |
|
|
|
PostMessageW(progress, PBM_SETRANGE32, 0, static_cast<int32_t>(totalBytes)); |
|
PostMessageW(progress, PBM_SETPOS, static_cast<int32_t>(curBytes), 0); |
|
} |
|
|
|
// =================================================== |
|
|
|
static void ISetDownloadStatus(const plString& status) |
|
{ |
|
SetDlgItemTextW(s_dialog, IDC_TEXT, status.ToWchar()); |
|
|
|
// consider this a reset of the download status... |
|
IShowMarquee(); |
|
SetDlgItemTextW(s_dialog, IDC_DLSIZE, L""); |
|
SetDlgItemTextW(s_dialog, IDC_DLSPEED, L""); |
|
|
|
if (s_taskbar) |
|
s_taskbar->SetProgressState(s_dialog, TBPF_INDETERMINATE); |
|
} |
|
|
|
|
|
static handleptr_t ICreateProcess(const plFileName& exe, const plString& args) |
|
{ |
|
STARTUPINFOW si; |
|
PROCESS_INFORMATION pi; |
|
memset(&si, 0, sizeof(si)); |
|
memset(&pi, 0, sizeof(pi)); |
|
si.cb = sizeof(si); |
|
|
|
// Create wchar things and stuff :/ |
|
plString cmd = plFormat("{} {}", exe, args); |
|
plStringBuffer<wchar_t> file = exe.AsString().ToWchar(); |
|
plStringBuffer<wchar_t> params = cmd.ToWchar(); |
|
|
|
// Guess what? CreateProcess isn't smart enough to throw up an elevation dialog... We need ShellExecute for that. |
|
// But guess what? ShellExecute won't run ".exe.tmp" files. GAAAAAAAAHHHHHHHHH!!!!!!! |
|
BOOL result = CreateProcessW( |
|
file, |
|
const_cast<wchar_t*>(params.GetData()), |
|
nullptr, |
|
nullptr, |
|
FALSE, |
|
DETACHED_PROCESS, |
|
nullptr, |
|
nullptr, |
|
&si, |
|
&pi |
|
); |
|
|
|
// So maybe it needs elevation... Or maybe everything arseploded. |
|
if (result != FALSE) { |
|
CloseHandle(pi.hThread); |
|
return handleptr_t(pi.hProcess, CloseHandle); |
|
} else if (GetLastError() == ERROR_ELEVATION_REQUIRED) { |
|
SHELLEXECUTEINFOW info; |
|
memset(&info, 0, sizeof(info)); |
|
info.cbSize = sizeof(info); |
|
info.lpFile = file.GetData(); |
|
info.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC; |
|
info.lpParameters = args.ToWchar(); |
|
ShellExecuteExW(&info); |
|
|
|
return handleptr_t(info.hProcess, CloseHandle); |
|
} else { |
|
wchar_t* msg = nullptr; |
|
FormatMessageW( |
|
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, |
|
nullptr, |
|
GetLastError(), |
|
LANG_USER_DEFAULT, |
|
msg, |
|
0, |
|
nullptr |
|
); |
|
s_error = plString::FromWchar(msg); |
|
LocalFree(msg); |
|
} |
|
|
|
return nullptr; |
|
} |
|
|
|
static bool IInstallRedist(const plFileName& exe) |
|
{ |
|
ISetDownloadStatus(plFormat("Installing... {}", exe)); |
|
Sleep(2500); // let's Sleep for a bit so the user can see that we're doing something before the UAC dialog pops up! |
|
|
|
// Try to guess some arguments... Unfortunately, the file manifest format is fairly immutable. |
|
plStringStream ss; |
|
if (exe.AsString().CompareI("oalinst.exe") == 0) |
|
ss << "/s"; // rarg nonstandard |
|
else |
|
ss << "/q"; |
|
if (exe.AsString().Find("vcredist", plString::kCaseInsensitive) != -1) |
|
ss << " /norestart"; // I don't want to image the accusations of viruses and hacking if this happened... |
|
|
|
// Now fire up the process... |
|
handleptr_t process = ICreateProcess(exe, ss.GetString()); |
|
if (process) { |
|
WaitForSingleObject(process.get(), INFINITE); |
|
|
|
// Get the exit code so we can indicate success/failure to the redist thread |
|
DWORD code = PLASMA_OK; |
|
GetExitCodeProcess(process.get(), &code); |
|
|
|
return code != PLASMA_PHAILURE; |
|
} |
|
return PLASMA_PHAILURE; |
|
} |
|
|
|
static void ILaunchClientExecutable(const plFileName& exe, const plString& args) |
|
{ |
|
// Once we start launching something, we no longer need to trumpet any taskbar status |
|
if (s_taskbar) |
|
s_taskbar->SetProgressState(s_dialog, TBPF_NOPROGRESS); |
|
|
|
// Only launch a client executable if we're given one. If not, that's probably a cue that we're |
|
// done with some service operation and need to go away. |
|
if (!exe.AsString().IsEmpty()) { |
|
handleptr_t hEvent = handleptr_t(CreateEventW(nullptr, TRUE, FALSE, L"UruPatcherEvent"), CloseHandle); |
|
handleptr_t process = ICreateProcess(exe, args); |
|
|
|
// if this is the real game client, then we need to make sure it gets this event... |
|
if (plManifest::ClientExecutable().AsString().CompareI(exe.AsString()) == 0) { |
|
WaitForInputIdle(process.get(), 1000); |
|
WaitForSingleObject(hEvent.get(), INFINITE); |
|
} |
|
} |
|
|
|
// time to hara-kiri... |
|
IQuit(); |
|
} |
|
|
|
static void IOnNetError(ENetError result, const plString& msg) |
|
{ |
|
if (s_taskbar) |
|
s_taskbar->SetProgressState(s_dialog, TBPF_ERROR); |
|
|
|
s_error = plFormat("Error: {}\r\n{}", NetErrorAsString(result), msg); |
|
IQuit(PLASMA_PHAILURE); |
|
} |
|
|
|
static void ISetShardStatus(const plString& status) |
|
{ |
|
SetDlgItemTextW(s_dialog, IDC_STATUS_TEXT, status.ToWchar()); |
|
} |
|
|
|
static pfPatcher* IPatcherFactory() |
|
{ |
|
pfPatcher* patcher = new pfPatcher(); |
|
patcher->OnFileDownloadBegin(IOnDownloadBegin); |
|
patcher->OnProgressTick(IOnProgressTick); |
|
|
|
return patcher; |
|
} |
|
|
|
// =================================================== |
|
|
|
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLink, int nCmdShow) |
|
{ |
|
// Let's initialize our plClientLauncher friend |
|
s_launcher.ParseArguments(); |
|
s_launcher.SetErrorProc(IOnNetError); |
|
s_launcher.SetInstallerProc(IInstallRedist); |
|
s_launcher.SetLaunchClientProc(ILaunchClientExecutable); |
|
s_launcher.SetPatcherFactory(IPatcherFactory); |
|
s_launcher.SetShardProc(ISetShardStatus); |
|
s_launcher.SetStatusProc(ISetDownloadStatus); |
|
|
|
// If we're newly updated, then our filename will be something we don't expect! |
|
// Let's go ahead and take care of that nao. |
|
if (s_launcher.CompleteSelfPatch(WaitForOldPatcher)) |
|
return PLASMA_OK; // see you on the other side... |
|
|
|
// Load the doggone server.ini |
|
if (!s_launcher.LoadServerIni()) { |
|
IShowErrorDialog(L"No server.ini file found. Please check your URU installation."); |
|
return PLASMA_PHAILURE; |
|
} |
|
|
|
// Ensure there is only ever one patcher running... |
|
if (IsPatcherRunning()) { |
|
plString text = plFormat("{} is already running", plProduct::LongName()); |
|
IShowErrorDialog(text.ToWchar()); |
|
return PLASMA_OK; |
|
} |
|
HANDLE _onePatcherMut = CreatePatcherMutex().release(); |
|
|
|
// Initialize the network core |
|
s_launcher.InitializeNetCore(); |
|
|
|
// Welp, now that we know we're (basically) sane, let's create our client window |
|
// and pump window messages until we're through. |
|
ShowPatcherDialog(hInstance); |
|
PumpMessages(); |
|
|
|
// So there appears to be some sort of issue with calling MessageBox once we've set up our dialog... |
|
// WTF?!?! So, to hack around that, we'll wait until everything shuts down to display any error. |
|
if (!s_error.IsEmpty()) |
|
IShowErrorDialog(s_error.ToWchar()); |
|
|
|
// Alrighty now we just need to clean up behind ourselves! |
|
// NOTE: We shut down the netcore in the WM_QUIT handler so |
|
// we don't have a windowless, zombie process if that takes |
|
// awhile (it can... dang eap...) |
|
ReleaseMutex(_onePatcherMut); |
|
CloseHandle(_onePatcherMut); |
|
|
|
// kthxbai |
|
return s_error.IsEmpty() ? PLASMA_OK : PLASMA_PHAILURE; |
|
} |
|
|
|
/* Enable themes in Windows XP and later */ |
|
#pragma comment(linker,"\"/manifestdependency:type='win32' \ |
|
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \ |
|
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
|
|
|