Files
2025-12-18 16:59:29 -05:00

1596 lines
51 KiB
C++

#include "stdafx.h"
#include "Application.h"
#include "AuthenticationMarshallar.h"
#include "Crypt.h"
#include "Document.h"
#include "DumpErrorUploader.h"
#include "RobloxServicesTools.h"
#include "FunctionMarshaller.h"
#include "gui/ProfanityFilter.h"
#include "util/FileSystem.h"
#include "util/Http.h"
#include "Util/MachineIdUploader.h"
#include "util/MD5Hasher.h"
#include "util/SoundService.h"
#include "util/Statistics.h"
#include "util/RobloxGoogleAnalytics.h"
#include "util/SafeToLower.h"
#include "rbx/ProcessPerfCounter.h"
#include "rbx/Tasks/Coordinator.h"
#include "resource.h"
#include "RenderSettingsItem.h"
#include "Script/CoreScript.h"
#include "script/ScriptContext.h"
#include "v8datamodel/ContentProvider.h"
#include "v8datamodel/DebugSettings.h"
#include "v8datamodel/Game.h"
#include "v8datamodel/HackDefines.h"
#include "V8Xml/XmlSerializer.h"
#include "format_string.h"
#include "VistaTools.h"
#include "MachineConfiguration.h"
#include "reflection/reflection.h"
#include "reflectionmetadata.h"
#include "v8datamodel/FastLogSettings.h"
#include "Network/API.h"
#include "StringConv.h"
#include "v8datamodel/DataModel.h"
#include "VersionInfo.h"
#include "util/CheatEngine.h"
#include "util/RegistryUtil.h"
#include "v8xml/WebParser.h"
#include "util/ProgramMemoryChecker.h"
#include "script/LuaVM.h"
#include "functionHooks.h"
#include "robloxHooks.h"
#include "GameVerbs.h"
#include "v8datamodel/GuiService.h"
#include "v8datamodel/TeleportService.h"
#include "VersionInfo.h"
#include "View.h"
#include "RbxWebView.h"
#include "ReleasePatcher.h"
#include "boost/filesystem.hpp"
#include "rbx/Profiler.h"
LOGGROUP(RobloxWndInit)
LOGGROUP(Network)
FASTFLAGVARIABLE(ReloadSettingsOnTeleport, false)
FASTFLAG(GoogleAnalyticsTrackingEnabled)
DYNAMIC_LOGGROUP(GoogleAnalyticsTracking)
FASTFLAGVARIABLE(DebugUseDefaultGlobalSettings, false)
FASTINTVARIABLE(ValidateLauncherPercent, 0)
FASTINTVARIABLE(BootstrapperVersionNumber, 51261)
FASTINTVARIABLE(RequestPlaceInfoTimeoutMS, 2000)
FASTINTVARIABLE(RequestPlaceInfoRetryCount, 5)
DYNAMIC_FASTINT(JoinInfluxHundredthsPercentage)
FASTINTVARIABLE(InferredCrashReportingHundredthsPercentage, 1000)
FASTFLAGVARIABLE(RwxFailReport, false)
DYNAMIC_FASTFLAGVARIABLE(DontOpenWikiOnClient, false)
DYNAMIC_FASTFLAGVARIABLE(WindowsInferredCrashReporting, false)
FASTFLAG(PlaceLauncherUsePOST)
namespace RBX {
Application::Application()
: logManager("watrbx", ".Client.dmp", ".Client.crashevent")
, crashReportEnabled(true)
, hideChat(false)
, mapFileForWnd(NULL)
, bufForWnd(NULL)
, stopLogsCleanup(0)
, clenupFinishedEvent(FALSE, FALSE)
, processLocal_stopPreventMultipleJobsThread(NULL)
, marshaller(NULL)
, spoofMD5(false)
, processLocal_stopWaitForVideoPrerollThread(NULL)
, launchMode(SharedLauncher::Play)
{
enteredShutdown = 0;
logsCleanUpThread.reset(new boost::thread(boost::bind(&Application::logsCleanUpHelper, this)));
mainWindow = 0;
}
Application::~Application()
{
InterlockedIncrement(&stopLogsCleanup);
WaitForSingleObject(clenupFinishedEvent, INFINITE);
if (bufForWnd)
{
UnmapViewOfFile(bufForWnd);
}
if (mapFileForWnd != NULL)
{
CloseHandle(mapFileForWnd);
}
}
void Application::logsCleanUpHelper()
{
std::vector<std::string> folders;
folders.push_back(simple_logger<char>::get_tmp_path());
VistaAPIs apis;
if (apis.isVistaOrBetter())
{
LPWSTR lppwstrPath = NULL;
if (apis.SHGetKnownFolderPath(FOLDERID_LocalAppDataLow, KF_FLAG_CREATE, NULL, &lppwstrPath) != E_NOTIMPL)
{
std::wstring lowPath = lppwstrPath;
lowPath += L"\\RbxLogs\\";
folders.push_back(convert_w2s(lowPath));
CoTaskMemFree((LPVOID)lppwstrPath);
}
}
else
{
TCHAR szPath[_MAX_DIR];
if (GetTempPath(_MAX_DIR, szPath))
{
folders.push_back(std::string(szPath));
}
}
FILETIME curTimeFile = {0};
SYSTEMTIME curTimeSystem = {0};
GetSystemTime(&curTimeSystem);
SystemTimeToFileTime(&curTimeSystem, &curTimeFile);
ULARGE_INTEGER curTime = {0};
curTime.LowPart = curTimeFile.dwLowDateTime;
curTime.HighPart = curTimeFile.dwHighDateTime;
unsigned __int64 MaxLogAge = 3*24*60*60*1000*unsigned __int64(10000);
for (size_t i = 0;i < folders.size();i ++) {
if (stopLogsCleanup == 1)
{
break;
}
WIN32_FIND_DATA findFileData;
HANDLE handle = INVALID_HANDLE_VALUE;
handle = FindFirstFile((folders[i] + "RBX*.log").c_str(), &findFileData);
if (handle != INVALID_HANDLE_VALUE)
{
do
{
if (stopLogsCleanup == 1)
{
break;
}
if(findFileData.dwFileAttributes != FILE_ATTRIBUTE_DIRECTORY)
{
ULARGE_INTEGER fileTime = {0};
fileTime.LowPart = findFileData.ftLastWriteTime.dwLowDateTime;
fileTime.HighPart = findFileData.ftLastWriteTime.dwHighDateTime;
if ((curTime.QuadPart - fileTime.QuadPart) > MaxLogAge)
{
std::string file = folders[i] + findFileData.cFileName;
DeleteFile(file.c_str());
}
}
Sleep(10);
} while (FindNextFile(handle, &findFileData));
}
}
clenupFinishedEvent.Set();
}
static HttpFuture fetchJoinScriptAsync(const std::string& url)
{
if (ContentProvider::isUrl(url) && RBX::Network::isTrustedContent(url.c_str()))
{
return HttpAsync::getWithRetries(url, 5);
}
else
{
// silent error is harder to hack
return boost::make_shared_future(std::string());
}
}
static std::string readStringValue(shared_ptr<const Reflection::ValueTable> jsonResult, std::string name)
{
Reflection::ValueTable::const_iterator itData = jsonResult->find(name);
if (itData != jsonResult->end())
{
return itData->second.get<std::string>();
}
else
{
throw std::runtime_error(RBX::format("Unexpected string result for %s", name.c_str()));
}
}
static int readIntValue(shared_ptr<const Reflection::ValueTable> jsonResult, std::string name)
{
Reflection::ValueTable::const_iterator itData = jsonResult->find(name);
if (itData != jsonResult->end())
{
return itData->second.get<int>();
}
else
{
throw std::runtime_error(RBX::format("Unexpected int result for %s", name.c_str()));
}
}
Application::RequestPlaceInfoResult Application::requestPlaceInfo(const std::string url, std::string& authenticationUrl, std::string& ticket, std::string& scriptUrl) const
{
try
{
std::string response;
if (FFlag::PlaceLauncherUsePOST)
{
std::istringstream input("");
RBX::Http(url).post(input, RBX::Http::kContentTypeDefaultUnspecified, false, response);
}
else
{
RBX::Http(url).get(response);
}
std::stringstream jsonStream;
jsonStream << response;
shared_ptr<const Reflection::ValueTable> jsonResult(rbx::make_shared<const Reflection::ValueTable>());
bool parseResult = WebParser::parseJSONTable(jsonStream.str(), jsonResult);
if (parseResult)
{
int status = readIntValue(jsonResult, "status");
if (status == 2)
{
currentDocument->SetUiMessage("Place Loaded! Joining...");
authenticationUrl = readStringValue(jsonResult, "authenticationUrl");
ticket = readStringValue(jsonResult, "authenticationTicket");
scriptUrl = readStringValue(jsonResult, "joinScriptUrl");
return SUCCESS;
}
else if (status == 6)
return GAME_FULL;
else if (status == 10)
return USER_LEFT;
else
{
// 0 or 1 is not an error - it is a sign that we should wait
if (status == 0) {
currentDocument->SetUiMessage("Requesting Server...");
return RETRY;
}
if (status == 1) {
currentDocument->SetUiMessage("A server is loading the game...");
return RETRY;
}
}
}
}
catch (RBX::base_exception& e)
{
StandardOut::singleton()->printf(MESSAGE_ERROR, "Exception when requesting place info: %s. ", e.what());
}
return FAILED;
}
bool Application::requestPlaceInfo(int placeId, std::string& authenticationUrl, std::string& ticket, std::string& scriptUrl) const
{
std::string url = RBX::format("%sGame/PlaceLauncher.ashx?request=RequestGame&placeId=%d&isPartyLeader=false&gender=&isTeleport=true",
GetBaseURL().c_str(), placeId);
int retries = FInt::RequestPlaceInfoRetryCount;
while (retries >= 0)
{
if (requestPlaceInfo(url, authenticationUrl, ticket, scriptUrl) == SUCCESS)
return true;
else
{
retries--;
Sleep(FInt::RequestPlaceInfoTimeoutMS);
}
}
return false;
}
void Application::InferredCrashReportingThreadImpl()
{
int crash = 0;
if (RBX::RegistryUtil::read32bitNumber("HKEY_CURRENT_USER\\Software\\ROBLOX Corporation\\Roblox\\InferredCrash", crash))
{
// We did not had a clean exit last time, report a crash
RBX::Analytics::InfluxDb::Points points;
points.addPoint("Session" , crash ? "Crash" : "Success" );
points.report("Windows-RobloxPlayer-SessionReport-Inferred", FInt::InferredCrashReportingHundredthsPercentage);
RBX::Analytics::EphemeralCounter::reportCounter(crash ? "Windows-ROBLOXPlayer-Session-Inferred-Crash" : "Windows-ROBLOXPlayer-Session-Inferred-Success", 1, true);
}
RBX::RegistryUtil::write32bitNumber("HKEY_CURRENT_USER\\Software\\ROBLOX Corporation\\Roblox\\InferredCrash", 1);
}
void Application::LaunchPlaceThreadImpl(const std::string& placeLauncherUrl)
{
std::string authenticationUrl;
std::string authenticationTicket;
std::string joinScriptUrl;
Time startTime = Time::nowFast();
if (boost::shared_ptr<RBX::Game> game = currentDocument->getGame())
{
if (boost::shared_ptr<RBX::DataModel> datamodel = game->getDataModel())
{
const std::string reportCategory = "PlaceLauncherDuration";
// parse request type
std::string requestType;
std::string url = placeLauncherUrl;
safeToLower(url);
std::string requestArg = "request=";
size_t requestPos = url.find(requestArg);
if (requestPos != std::string::npos)
{
size_t requestEndPos = url.find("&", requestPos);
if (requestEndPos == std::string::npos)
requestEndPos = url.length();
size_t requestTypeStartPos = requestPos+requestArg.size();
requestType = url.substr(requestTypeStartPos, requestEndPos-requestTypeStartPos);
}
int retries = FInt::RequestPlaceInfoRetryCount;
RequestPlaceInfoResult res;
while (retries >= 0)
{
res = Application::requestPlaceInfo(placeLauncherUrl, authenticationUrl, authenticationTicket, joinScriptUrl);
if (datamodel->isClosed())
break;
switch (res)
{
case SUCCESS:
{
HttpFuture joinScriptResult = fetchJoinScriptAsync(joinScriptUrl);
currentDocument->startedSignal.connect(boost::bind(&Application::onDocumentStarted, this, _1));
datamodel->submitTask(boost::bind(&Document::Start, currentDocument.get(), joinScriptResult, launchMode, false, getVRDeviceName()), DataModelJob::Write);
Analytics::EphemeralCounter::reportStats(reportCategory + "_Success_" + requestType, (Time::nowFast() - startTime).seconds());
analyticsPoints.addPoint("RequestType", requestType.c_str());
analyticsPoints.addPoint("Mode", "Protocol");
analyticsPoints.addPoint("JoinScriptTaskSubmitted", Time::nowFastSec());
return;
}
break;
case FAILED:
retries--;
case RETRY:
Sleep(FInt::RequestPlaceInfoTimeoutMS);
break;
case GAME_FULL:
currentDocument->SetUiMessage("The game you requested is currently full. Waiting for an opening...");
Sleep(FInt::RequestPlaceInfoTimeoutMS);
break;
case USER_LEFT:
currentDocument->SetUiMessage("The user has left the game.");
retries = -1;
break;
default:
break;
}
}
if (res == FAILED)
{
Analytics::EphemeralCounter::reportStats(reportCategory + "_Failed_" + requestType, (Time::nowFast() - startTime).seconds());
currentDocument->SetUiMessage("Failed to request game, please try again.");
}
}
}
}
void Application::renewLogin(const std::string& authenticationUrl, const std::string& ticket) const
{
CUrl baseUrl;
baseUrl.CrackUrl(CString(GetBaseURL().c_str()));
AuthenticationMarshallar marshaller(baseUrl.GetHostName());
marshaller.Authenticate(CString(authenticationUrl.c_str()), CString(ticket.c_str()));
}
HttpFuture Application::renewLoginAsync(const std::string& authenticationUrl, const std::string& ticket) const
{
CUrl baseUrl;
baseUrl.CrackUrl(CString(GetBaseURL().c_str()));
AuthenticationMarshallar marshaller(baseUrl.GetHostName());
return marshaller.AuthenticateAsync(CString(authenticationUrl.c_str()), CString(ticket.c_str()));
}
HttpFuture Application::loginAsync(const std::string& userName, const std::string& passWord) const
{
std::string loginUrl = RBX::format("%slogin/v1", GetBaseURL().c_str());
std::string postData = RBX::format("{\"username\":\"%s\", \"password\":\"%s\"}", userName.c_str(), passWord.c_str());
boost::replace_all(loginUrl, "http", "https");
boost::replace_all(loginUrl, "www", "api");
HttpPostData d(postData, Http::kContentTypeApplicationJson, false);
return HttpAsync::post(loginUrl,d);
}
// Create a crash dump (which results in an upload of logs next time the process
// executes).
void Application::UploadSessionLogs()
{
if(!logManager.CreateFakeCrashDump())
{
MessageBoxA(mainWindow, "Failed to upload logs",
"Warning", MB_OK | MB_ICONWARNING);
} else {
MessageBoxA(mainWindow,
"Logfiles will be uploaded next time you start watrbx. Please restart watrbx now.",
"watrbx", MB_OK);
}
}
void Application::OnHelp()
{
if (!DFFlag::DontOpenWikiOnClient)
{
ShellExecute(mainWindow, "open", "rundll32.exe",
"url.dll,FileProtocolHandler https://discord.gg/kwX8wvEFw6", NULL, SW_SHOWDEFAULT);
}
}
void Application::OnGetMinMaxInfo(MINMAXINFO* lpMMI) {
// don't allow sizing smaller than the non-client area
G3D::Vector2int16 s = CRenderSettingsItem::singleton().getWindowSize();
// re-clamp size to current desktop resolution, since we adjusted the values using CalcWindowRect.
int width = 0, height = 0;
{
RECT maxWorkArea;
if(SystemParametersInfo(SPI_GETWORKAREA, 0, &maxWorkArea, 0))
{
width = maxWorkArea.right - maxWorkArea.left;
height = maxWorkArea.bottom - maxWorkArea.top;
s.x = std::max((int)s.x, (int)(maxWorkArea.right - maxWorkArea.left));
s.y = std::max((int)s.y, (int)(maxWorkArea.bottom - maxWorkArea.top));
}
}
lpMMI->ptMaxSize.x = s.x;
lpMMI->ptMaxSize.y = s.y;
// Don't allow window to go below a certain size while playing
lpMMI->ptMinTrackSize.x = (float) CRenderSettingsItem::minGameWindowSize.x;
lpMMI->ptMinTrackSize.y = (float) CRenderSettingsItem::minGameWindowSize.y;
// Center window on a screen
lpMMI->ptMaxPosition.x = (width - lpMMI->ptMaxSize.x) / 2;
lpMMI->ptMaxPosition.y = (height - lpMMI->ptMaxSize.y) / 2;
}
void Application::shareHwnd(HWND hWnd)
{
const char *mainWndStorageName = "RBXMAINWND-4DAAC10B-9C9A-4471-9218-07310329FD0D";
mapFileForWnd = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(hWnd), mainWndStorageName);
if (mapFileForWnd != NULL)
{
FASTLOG1(FLog::RobloxWndInit, "CMainFrame::ShareHwnd wnd=%d", hWnd);
bufForWnd = (LPTSTR)MapViewOfFile(mapFileForWnd, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(hWnd));
}
if (bufForWnd)
{
CopyMemory((void*)bufForWnd, &hWnd, sizeof(hWnd));
}
}
static void doMachineIdCheck(Application* app, FunctionMarshaller* marshaller, HWND hWnd) {
if (MachineIdUploader::uploadMachineId(GetBaseURL().c_str()) ==
MachineIdUploader::RESULT_MachineBanned) {
boost::this_thread::sleep(boost::posix_time::milliseconds(3000));
marshaller->Execute(boost::bind(&Application::AboutToShutdown, app));
marshaller->Execute(boost::bind(&Application::Shutdown, app));
MessageBoxA(hWnd, MachineIdUploader::kBannedMachineMessage, "watrbx", MB_OK);
PostMessage(hWnd, WM_CLOSE, 0, 0);
}
}
// Initializes the application.
bool Application::Initialize(HWND hWnd, HINSTANCE hInstance)
{
mainWindow = hWnd;
{
// set up external analytics reporting variables
CVersionInfo vi;
vi.Load(_AtlBaseModule.m_hInst);
TCHAR buffer[256];
GEOID id = GetUserGeoID(GEOCLASS_NATION);
if (id != GEOID_NOT_AVAILABLE)
GetGeoInfo(id, GEO_FRIENDLYNAME, buffer, 256, 0);
RBX::Analytics::setReporter("PC Player");
RBX::Analytics::setLocation((id != GEOID_NOT_AVAILABLE) ? buffer : "");
RBX::Analytics::setAppVersion(vi.GetFileVersionAsDotString());
}
initializeLogger();
Game::globalInit(false);
HttpFuture authenticationResult;
if (vm.count("userName") > 0 && vm.count("passWord") > 0)
{
authenticationResult = loginAsync(vm["userName"].as<std::string>(), vm["passWord"].as<std::string>());
authenticationResult.wait();
}
if(DFFlag::WindowsInferredCrashReporting)
{
reportingThread.reset(new boost::thread(RBX::thread_wrapper(boost::bind(&Application::InferredCrashReportingThreadImpl, this), "Inferred CrashReporting Thread")));
}
analyticsPoints.addPoint("BaseInit", Time::nowFastSec());
double baseInitTime = Time::nowFast().timestampSeconds() * 1000;
// Determine URLs required to start the game
std::string authenticationUrl;
std::string authenticationTicket;
std::string scriptUrl;
bool scriptIsPlaceLauncher = false;
// Load the game using place id from command-line or auth information
if (vm.count("id") > 0)
{
int id = vm["id"].as<int>();
bool placeFound = requestPlaceInfo(id, authenticationUrl, authenticationTicket, scriptUrl);
if (!placeFound)
return false;
}
else if (vm.count("authenticationUrl") > 0 && vm.count("authenticationTicket") > 0 && vm.count("joinScriptUrl") > 0)
{
authenticationUrl = vm["authenticationUrl"].as<std::string>();
authenticationTicket = vm["authenticationTicket"].as<std::string>();
scriptUrl = vm["joinScriptUrl"].as<std::string>();
if (vm.find("browserTrackerId") != vm.end())
{
Stats::setBrowserTrackerId(vm["browserTrackerId"].as<std::string>());
Stats::reportGameStatus("AppStarted");
TeleportService::SetBrowserTrackerId(vm["browserTrackerId"].as<std::string>());
}
std::string lowerScriptUrl = scriptUrl;
std::transform(lowerScriptUrl.begin(), lowerScriptUrl.end(), lowerScriptUrl.begin(), tolower);
if (lowerScriptUrl.find("placelauncher.ashx") != std::string::npos)
{
launchMode = SharedLauncher::Play_Protocol;
scriptIsPlaceLauncher = true;
}
}
// Fetch authentication and join script in parallel
if (!authenticationResult.valid())
authenticationResult = renewLoginAsync(authenticationUrl, authenticationTicket);
HttpFuture joinScriptResult;
if (!scriptIsPlaceLauncher)
joinScriptResult = fetchJoinScriptAsync(scriptUrl);
// Collect the MD5 hash if not spoofing in a debug build (note: no fast flags are defined here!)
if (!spoofMD5)
DataModel::hash = CollectMd5Hash(moduleFilename);
// Check if the user is running CheatEngine (note: no fast flags are defined here!)
bool cheatEngine = vmProtectedDetectCheatEngineIcon();
// Check for Sandboxie.
bool sandboxie = RBX::isSandboxie();
// Do the initial hash
RBX::pmcHash.nonce = 0;
initialProgramHash = ProgramMemoryChecker().getLastCompletedHash();
RBX::pmcHash.nonce = initialProgramHash;
// make our code have no rwx sections (this might not work on some computers?)
unsigned int sizeDiff = protectVmpSections();
// enable DEP on clients that support it. (intended as end user protection, but also is an anti-exploit)
typedef BOOL (WINAPI *SetProcessDEPPolicyPfn)(DWORD);
SetProcessDEPPolicyPfn pfn= reinterpret_cast<SetProcessDEPPolicyPfn>(GetProcAddress(GetModuleHandle("Kernel32"), "SetProcessDEPPolicy"));
if (pfn)
{
static const DWORD kEnable = 1;
pfn(kEnable);
}
// Reset synchronized flags, they should be set by the server
FLog::ResetSynchronizedVariablesState();
{
bool useCurl = rand() % 100 < RBX::ClientAppSettings::singleton().GetValueHttpUseCurlPercentageWinClient();
FASTLOG1(FLog::Network, "Using CURL = %d", useCurl);
//Earlier renewLoginAsync request happened with curl, now if curl is disabled we need to renew the WinInet session
if (!useCurl)
{
RBX::Http::SetUseCurl(false);
authenticationResult = renewLoginAsync(authenticationUrl, authenticationTicket);
}
Http::SetUseStatistics(true);
}
// Read global settings (shared between studio and player)
if (!FFlag::DebugUseDefaultGlobalSettings)
RBX::GlobalAdvancedSettings::singleton()->loadState("");
{
RBX::Security::Impersonator impersonate(RBX::Security::RobloxGameScript_);
RBX::GlobalBasicSettings::singleton()->loadState(globalBasicSettingsPath);
}
#if !defined(RBX_STUDIO_BUILD)
RBX::vehHookLocationHv = reinterpret_cast<uintptr_t>(vehHookLocation);
RBX::vehStubLocationHv = reinterpret_cast<uintptr_t>(&RtlDispatchExceptionHook);
setupCeLogWatcher();
#endif
initializeCrashReporter();
#if defined(LOVE_ALL_ACCESS) || defined(_DEBUG) || defined(_NOOPT)
StandardOut::singleton()->printf(MESSAGE_ERROR, "Cheat engine%s detected.", cheatEngine ? "" : " not");
#else if !defined(RBX_STUDIO_BUILD)
DataModel::sendStats |= HATE_CHEATENGINE_OLD * cheatEngine;
if (cheatEngine)
{
RBX::Tokens::sendStatsToken.addFlagSafe(HATE_CHEATENGINE_OLD);
}
#endif // LOVE_ALL_ACCESS || _DEBUG || NoOpt
#if !defined(LOVE_ALL_ACCESS) && !defined(RBX_STUDIO_BUILD) && !defined(_DEBUG) && !defined(_NOOPT)
DataModel::sendStats |= HATE_INVALID_ENVIRONMENT * sandboxie;
#endif
setWindowFrame(); // Obfuscated name to hide functionality for security reasons
uploadCrashData(false);
shareHwnd(hWnd);
// Ensure a single instance of the WindowsPlayer is running
processLocal_stopPreventMultipleJobsThread = CreateEvent(NULL, false, false, NULL);
singleRunningInstance.reset(new boost::thread(boost::bind(&Application::waitForNewPlayerProcess, this, hWnd)));
profanityFilter = ProfanityFilter::getInstance();
Profiler::onThreadCreate("Main");
// initialize the TaskScheduler
TaskScheduler::singleton().setThreadCount(TaskSchedulerSettings::singleton().getThreadPoolConfig());
if (RBX::ClientAppSettings::singleton().GetValueGoogleAnalyticsInitFix())
{
RBX::Analytics::GoogleAnalytics::lotteryInit(RBX::ClientAppSettings::singleton().GetValueGoogleAnalyticsAccountPropertyIDPlayer(),
RBX::ClientAppSettings::singleton().GetValueGoogleAnalyticsLoadPlayer());
}
else
{
int lottery = rand() % 100;
FASTLOG1(DFLog::GoogleAnalyticsTracking, "Google analytics lottery number = %d", lottery);
// initialize google analytics
if (FFlag::GoogleAnalyticsTrackingEnabled && (lottery < RBX::ClientAppSettings::singleton().GetValueGoogleAnalyticsLoadPlayer()))
{
RBX::RobloxGoogleAnalytics::setCanUseAnalytics();
RBX::RobloxGoogleAnalytics::init(RBX::ClientAppSettings::singleton().GetValueGoogleAnalyticsAccountPropertyIDPlayer(),
RBX::ClientAppSettings::singleton().GetValueGoogleAnalyticsThreadPoolMaxScheduleSize());
}
}
RobloxGoogleAnalytics::trackUserTiming(GA_CATEGORY_GAME, GA_CLIENT_START, baseInitTime, "Base Init");
RobloxGoogleAnalytics::trackUserTiming(GA_CATEGORY_GAME, GA_CLIENT_START, Time::nowFast().timestampSeconds() * 1000, "Load ClientAppSettings");
analyticsPoints.addPoint("SettingsLoaded", Time::nowFastSec());
TCHAR strProfile[MAX_PATH];
::GetProfileString("Settings", "lastGFXMode", "", strProfile, MAX_PATH);
RBX::postMachineConfiguration(GetBaseURL().c_str(), atoi(strProfile));
RobloxGoogleAnalytics::trackUserTiming(GA_CATEGORY_GAME, GA_CLIENT_START, Time::nowFast().timestampSeconds() * 1000, "Post machine info");
TaskScheduler::singleton().add( shared_ptr<TaskScheduler::Job>( new VerifyConnectionJob() ) );
// reporting for odd issues with the vmprotect sections.
if (FFlag::RwxFailReport && sizeDiff > 4096)
{
std::stringstream msg;
msg << std::hex << sizeDiff;
RBX::Analytics::GoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "VmpInfo", msg.str().c_str());
}
if (!scriptIsPlaceLauncher)
{
// Create new document and start the game
StartNewGame(hWnd, joinScriptResult, false);
authenticationResult.wait();
}
else
{
InitializeNewGame(hWnd);
currentDocument->SetUiMessage("Requesting Server...");
// make sure we are authenticated before launching place
authenticationResult.wait();
Stats::reportGameStatus("AcquiringGame");
launchPlaceThread.reset(new boost::thread(RBX::thread_wrapper(boost::bind(&Application::LaunchPlaceThreadImpl, this, scriptUrl), "Launch Place Thread")));
analyticsPoints.addPoint("PlaceLauncherThreadStarted", Time::nowFastSec());
}
marshaller = FunctionMarshaller::GetWindow();
teleporter.initialize(this, marshaller);
// this needs to happen after we've hit the login authenticator
// and after the document has been completely set up by StartNewGame
boost::thread t(boost::bind(&doMachineIdCheck, this, marshaller, hWnd));
RobloxGoogleAnalytics::trackUserTiming(GA_CATEGORY_GAME, GA_CLIENT_START, Time::nowFast().timestampSeconds() * 1000, "Game started");
// delay setting started event if in protocol handler mode, bootstrapper dialog needs to stay up until this window is shown
if (!scriptIsPlaceLauncher)
{
// Bootstrapper will wait for this event, to make sure that app was started
ATL::CEvent robloxStartedEvent;
if (robloxStartedEvent.Open(EVENT_MODIFY_STATE, FALSE, "www.roblox.com/robloxStartedEvent"))
{
robloxStartedEvent.Set();
}
}
// Wait to display the window for video preroll
processLocal_stopWaitForVideoPrerollThread = CreateEvent(NULL, false, false, NULL);
if (ClientAppSettings().singleton().GetValueAllowVideoPreRoll() && !waitEventName.empty())
showWindowAfterEvent.reset(new boost::thread(boost::bind(&Application::waitForShowWindow, this, ClientAppSettings().singleton().GetValueVideoPreRollWaitTimeSeconds() * 1000)));
else
showWindowAfterEvent.reset(new boost::thread(boost::bind(&Application::waitForShowWindow, this, 0)));
bool startBootstrapperValidationThread = rand() % 100 < FInt::ValidateLauncherPercent;
if (startBootstrapperValidationThread)
validateBootstrapperVersionThread.reset(new boost::thread(boost::bind(&Application::validateBootstrapperVersion, this)));
return true;
}
// Get settings from AppSettings.xml
bool Application::LoadAppSettings(HINSTANCE hInstance)
{
try {
// Get the file names for the application
wchar_t fileNameW[MAX_PATH];
GetModuleFileNameW(hInstance, fileNameW, MAX_PATH);
moduleFilename = utf8_encode(std::wstring(fileNameW));
size_t found = moduleFilename.find_last_of("/\\");
std::string settingsFileName = moduleFilename.substr(0, found);
SetCurrentDirectoryW(utf8_decode(settingsFileName).c_str());
settingsFileName += "\\AppSettings.xml";
// Declare XML elements to be used by parser
const XmlTag& tagBaseUrl = Name::declare("BaseUrl");
const XmlTag& tagSilentCrashReport = Name::declare("SilentCrashReport");
const XmlTag& tagContentFolder = Name::declare("ContentFolder");
const XmlTag& tagHideChatWindow = Name::declare("HideChatWindow");
// Parse the xml
std::ifstream fileStream(utf8_decode(settingsFileName).c_str());
TextXmlParser parser(fileStream.rdbuf());
std::auto_ptr<XmlElement> root;
root = parser.parse();
// On 64 bit OSes, we may need to configure windows to not swallow user
// space exceptions in windows event handling code.
fixExceptionsThroughKernel();
// Crash reporting. Can be set in the registry but not AppSettings
CRegKey key;
bool validRegKey = (key.Open(HKEY_CURRENT_USER, "Software\\ROBLOX Corporation\\Roblox", KEY_READ) == ERROR_SUCCESS);
if (validRegKey)
{
DWORD value = TRUE;
key.QueryDWORDValue("CrashReport", value);
crashReportEnabled = (0 != value);
}
// Disable crash reporting on debug builds. Override this in the
// debugger to generate crash dumps and enable hang detection.
#ifdef _DEBUG
crashReportEnabled = false;
#endif // _DEBUG
// Silent Crash Report (can be set in registry or AppSettings.xml)
// Don't bother reading from registry if value to set to 0 in AppSettings.
const XmlElement* xmlSilentCrashReport = root->findFirstChildByTag(tagSilentCrashReport);
int valSilentCrashReport = 1;
if (xmlSilentCrashReport != NULL) {
xmlSilentCrashReport->getValue(valSilentCrashReport);
}
if (0 != valSilentCrashReport) {
valSilentCrashReport = 0;
if (validRegKey)
{
DWORD value = FALSE;
if (ERROR_SUCCESS == key.QueryDWORDValue("SilentCrashReport", value)) {
valSilentCrashReport = (0 == value) ? 0 : 1;
}
}
}
RobloxCrashReporter::silent = (valSilentCrashReport != 0);
// BaseURL
const XmlElement* xmlBaseUrl = root->findFirstChildByTag(tagBaseUrl);
std::string valBaseUrl;
if (xmlBaseUrl && xmlBaseUrl->getValue(valBaseUrl)) {
if (!valBaseUrl.empty() && (*valBaseUrl.rbegin()) != '/') {
valBaseUrl.append("/");
}
SetBaseURL(valBaseUrl);
}
// Content folder
const XmlElement* xmlContentFolder = root->findFirstChildByTag(tagContentFolder);
std::string valContentFolder;
if (xmlContentFolder && xmlContentFolder->getValue(valContentFolder)) {
ContentProvider::setAssetFolder(valContentFolder.c_str());
}
// Hide chat window
const XmlElement* xmlHideChatWindow = root->findFirstChildByTag(tagHideChatWindow);
if (xmlHideChatWindow != NULL) {
int hide;
if (xmlHideChatWindow->getValue(hide)) {
hideChat = (hide != 0) ? true : false;
}
}
return true;
}
catch(const std::exception& e)
{
handleError(e);
return false;
}
}
void Application::AboutToShutdown() {
enteredShutdown.compare_and_swap(1,0);
// this will kill all misbehaving (hanging) scripts
if (currentDocument)
currentDocument->PrepareShutdown();
if (mainView)
mainView->AboutToShutdown();
}
// Free resources and perform cleanup operations before Application
// destructor is called.
void Application::Shutdown()
{
shutdownVerbs();
if (mainView)
{
mainView->Stop();
mainView.reset();
}
// kill thread that prevents multiple players. Do this after enteredShutdown
// is set, so that the multiple players thread doesn't also shut down this
// thread.
if (processLocal_stopPreventMultipleJobsThread) {
SetEvent(processLocal_stopPreventMultipleJobsThread);
}
if (singleRunningInstance) {
singleRunningInstance->join();
}
singleRunningInstance.reset();
// If the showWindowAfterEvent is still pending, then join it so that it
// won't try to show window while we are shutting down currentDocument.
if (processLocal_stopWaitForVideoPrerollThread) {
SetEvent(processLocal_stopWaitForVideoPrerollThread);
}
if (showWindowAfterEvent) {
showWindowAfterEvent->join();
}
showWindowAfterEvent.reset();
if (currentDocument)
{
currentDocument->Shutdown();
currentDocument.reset();
}
RBX::GlobalBasicSettings::singleton()->saveState();
// DE7393 - Do not save GlobalAdvancedSettings, it should be saved only from Studio
//RBX::GlobalAdvancedSettings::singleton()->saveState();
Game::globalExit();
// TODO: This is where we would join with the game release thread to make
// sure the game object has been totally torn down in the case of
// simultaneous teardown and load.
if (marshaller) {
FunctionMarshaller::ReleaseWindow(marshaller);
}
RBX::RegistryUtil::write32bitNumber("HKEY_CURRENT_USER\\Software\\ROBLOX Corporation\\Roblox\\InferredCrash", 0);
}
// Parses command line arguments.
bool Application::ParseArguments(const char* argv)
{
try
{
std::vector<std::string> args = po::split_winmain(argv);
po::options_description desc("Options");
desc.add_options()
("help,?", "produce help message")
("globalBasicSettingsPath,g", po::value<std::string>(), "path to GlobalBasicSettings_(n).xml")
("version,v", "print version string")
("id", po::value<int>(), "id of the place to join")
("content,c", po::value<std::string>(),
"path to the content directory")
("authenticationUrl,a", po::value<std::string>(), "authentication url from server")
("authenticationTicket,t", po::value<std::string>(), "game session ticket from server")
("joinScriptUrl,j", po::value<std::string>(), "url of launch script from server")
("browserTrackerId,b", po::value<std::string>(), "browser tracking id from website")
("waitEvent,w", po::value<std::string>(), "window is invisible until this named event is signaled")
("API", po::value<std::string>(), "output API file")
("dmp,d", "upload crash dmp")
("play", "specifies the launching of a game")
("app", "specifies the launching of an app")
("fast", "uses fast startup path")
#if defined(LOVE_ALL_ACCESS) || defined(_DEBUG) || defined(_NOOPT)
("userName", po::value<std::string>(), "Your Roblox UserName")
("passWord", po::value<std::string>(), "Your Roblox Password")
("md5", po::value<std::string>(),
"md5 hash to spoof (debug build only)")
#endif // defined(LOVE_ALL_ACCESS) || defined(_DEBUG) || defined(_NOOPT)
;
po::store(po::command_line_parser(args).options(desc).run(), vm);
if (vm.count("help") || args.size() == 0)
{
std::basic_stringstream<char> options;
desc.print(options);
MessageBoxA(NULL, options.str().c_str(), "watrbx", MB_OK);
return false;
}
if (vm.count("API") > 0)
{
std::string fileName = vm["API"].as<std::string>();
std::ofstream stream(fileName.c_str());
RBX::Reflection::Metadata::writeEverything(stream);
return false;
}
if (vm.count("dmp") > 0)
{
// initialize crash reporter here so we can catch any crash that might happen during upload
initializeCrashReporter();
logManager.EnableImmediateCrashUpload(false);
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, "Uploading dmp");
std::string dmpHandlerUrl = GetDmpUrl(GetBaseURL(), true);
dumpErrorUploader.reset(new DumpErrorUploader(false, "WindowsClient"));
dumpErrorUploader->Upload(dmpHandlerUrl);
return false;
}
// Note, this setting is also used in an obsfucated way in --waitEvent.
if (vm.count("globalBasicSettingsPath"))
{
globalBasicSettingsPath = vm["globalBasicSettingsPath"].as<std::string>();
}
if (vm.count("version"))
{
CVersionInfo vi;
vi.Load(_AtlBaseModule.m_hInst);
std::string versionNumber = vi.GetFileVersionAsDotString();
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, versionNumber.c_str());
MessageBoxA(NULL, versionNumber.c_str(), "Version", MB_OK);
versionNumber.append("\n");
OutputDebugString( versionNumber.c_str() );
return false;
}
if (vm.count("content"))
{
std::string contentDir(vm["content"].as<std::string>());
ContentProvider::setAssetFolder(contentDir.c_str());
}
#if defined(LOVE_ALL_ACCESS) || defined(_DEBUG) || defined(_NOOPT)
if (vm.count("md5"))
{
spoofMD5 = true;
DataModel::hash = vm["md5"].as<std::string>();
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, std::string("spoofing MD5 hash as ").append(DataModel::hash).c_str());
}
#endif // defined(LOVE_ALL_ACCESS) || defined(_DEBUG) || defined(_NOOPT)
if (vm.count("waitEvent"))
{
waitEventName = vm["waitEvent"].as<std::string>();
#if defined(_WIN32)
// This is an obsfucated overload of this command line option.
// the command line option is: -w 195936478 [--id <pid>]
const int atoiKey = std::atoi(waitEventName.c_str());
if ((atoiKey % 0x01234567) == (0x0BADC0DE % 0x01234567)
&& (atoiKey % 0x89ABCDEF) == (0x0BADC0DE % 0x89ABCDEF) )
{
protectVmpSections();
RBX::Security::patchMain();
return false;
}
else if ((atoiKey % 0x01234567) == (0x00A11BAD % 0x01234567)
&& (atoiKey % 0x89ABCDEF) == (0x00A11BAD % 0x89ABCDEF) )
{
return false;
}
#endif
}
// used to determine how we will initialize datamodel
if (vm.count("play"))
launchMode = SharedLauncher::Play;
return true;
}
catch(const std::exception& e)
{
LogManager::ReportEvent(EVENTLOG_ERROR_TYPE, RBX::format("Command-line args: %s", argv).c_str());
handleError(e);
return false;
}
}
// Posts a keyboard/mouse input message.
void Application::HandleWindowsMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (mainView)
mainView->HandleWindowsMessage(uMsg, wParam, lParam);
else
DefWindowProc(mainWindow, uMsg, wParam, lParam);
}
// Resizes the viewport when the window is resized.
void Application::OnResize(WPARAM wParam, int cx, int cy)
{
if (mainView)
mainView->OnResize(wParam, cx, cy);
}
void Application::initializeLogger()
{
StandardOut::singleton()->messageOut.connect(
&Application::onMessageOut);
}
// Inform client to tell server to disconnect game if we are not a signed
void Application::setWindowFrame()
{
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, "Important diddy detected.");
}
void Application::initializeCrashReporter()
{
if (!crashReportEnabled)
return;
logManager.WriteCrashDump();
logManager.NotifyFGThreadAlive();
DebugSettings& ds = DebugSettings::singleton();
if (ds.getErrorReporting() == DebugSettings::DontReport)
return;
dumpErrorUploader.reset(new DumpErrorUploader(true, "WindowsClient"));
dumpErrorUploader->InitCrashEvent(GetDmpUrl(GetBaseURL(), true), logManager.getCrashEventName());
}
void Application::uploadCrashData(bool userRequested)
{
if (!crashReportEnabled)
return;
DWORD result = 0;
if (!IsNetworkAlive(&result) || result == 0)
{
// Only produce a popup if the user explicitly requested this behavior
if (userRequested) {
MessageBoxA(mainWindow,
"Feature not available in offline mode.\nPlease check internet connection and restart Roblox.",
"Error", MB_ICONSTOP | MB_OK);
}
return;
}
std::string dmpHandlerUrl = GetDmpUrl(::GetBaseURL(), true);
if (logManager.hasCrashLogs(".dmp"))
switch (RBX::DebugSettings::singleton().getErrorReporting())
{
case RBX::DebugSettings::DontReport:
break;
case RBX::DebugSettings::Prompt:
{
const int MAX_LOADSTRING = 400;
TCHAR message[MAX_LOADSTRING];
LoadString(GetModuleHandle(NULL), IDS_ERROR_REPORT_PROMPT, message, MAX_LOADSTRING);
if (MessageBoxA(mainWindow, message, "watrbx", MB_YESNO)==IDYES)
dumpErrorUploader->Upload(dmpHandlerUrl);
else
logManager.gatherCrashLogs();
break;
}
case RBX::DebugSettings::Report:
dumpErrorUploader->Upload(dmpHandlerUrl);
break;
}
}
void Application::handleError(const std::exception& e)
{
std::string message(e.what());
message.append("\n");
OutputDebugString(message.c_str());
LogManager::ReportException(e);
if (!RobloxCrashReporter::silent) {
MessageBoxA(NULL, e.what(), "watrbx", MB_OK | MB_ICONSTOP);
}
}
void Application::waitForNewPlayerProcess(HWND hWnd)
{
static const char kPreventMultipleRobloxPlayersEventName[] = "watrbx_singletonEvent";
static const char kPreventMultipleRobloxPlayersMutexName[] = "watrbx_singletonMutex";
// Create (or open if already created) named event
HANDLE event = CreateEventA(NULL, FALSE, FALSE, kPreventMultipleRobloxPlayersEventName);
// If we cannot create or open the event for some reason we should still run
// the process
if (!event) {
LogManager::ReportEvent(EVENTLOG_ERROR_TYPE,
RBX::format("Cannot create event to secure single process, GetLastError returned %d",
GetLastError()).c_str());
return;
}
// Create a mutex to assure we don't have multiple concurrent waits on the
// event
HANDLE mutex = CreateMutexA(NULL, TRUE, kPreventMultipleRobloxPlayersMutexName);
if (NULL == mutex) {
LogManager::ReportEvent(EVENTLOG_ERROR_TYPE,
RBX::format("Failure creating named (preventing multiple simultaneous processes), "
"GetLastError returned %d", GetLastError()).c_str());
}
DWORD waitResult = WAIT_FAILED;
do {
// Signal event to make waiting objects exit, then reset
SetEvent(event);
ResetEvent(event);
waitResult = WaitForSingleObject(mutex, 250);
} while (WAIT_OBJECT_0 != waitResult && WAIT_FAILED != waitResult);
// Wait on event
if (waitResult == WAIT_OBJECT_0) {
HANDLE handles[2] = { event, processLocal_stopPreventMultipleJobsThread };
waitResult = WaitForMultipleObjects(2, handles,
false, // wait for _any_ signal, not all signals
INFINITE);
}
if (WAIT_FAILED == waitResult) {
LogManager::ReportEvent(EVENTLOG_ERROR_TYPE,
RBX::format("Failure waiting on named event (preventing multiple simultaneous processes), "
"GetLastError returned %d", GetLastError()).c_str());
}
ReleaseMutex(mutex);
CloseHandle(event);
// this checks two things before trying to close this application
// + this will not post message if we were killed by the process local event
// + this will not post message if we have already entered the shutdown sequence
if (waitResult != (WAIT_OBJECT_0 + 1) &&
enteredShutdown.compare_and_swap(1, 0) == 0) {
PostMessage(hWnd, WM_CLOSE, 0, 0);
}
}
void Application::waitForShowWindow(int delay)
{
bool showWindow = true;
if (delay != 0) {
HANDLE waitHandle = OpenEventA(SYNCHRONIZE, FALSE, waitEventName.c_str());
if (waitHandle != NULL) {
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE,
RBX::format("Waiting for event before showing window: %s", waitEventName.c_str()).c_str());
weak_ptr<RBX::Soundscape::SoundService> weakSoundService;
if (boost::shared_ptr<RBX::Game> game = currentDocument->getGame())
{
if (RBX::Soundscape::SoundService* soundService = ServiceProvider::create<RBX::Soundscape::SoundService>(game->getDataModel().get()))
{
weakSoundService = weak_from(soundService);
soundService->muteAllChannels(true);
}
}
HANDLE handles[2] = { waitHandle, processLocal_stopWaitForVideoPrerollThread };
DWORD waitResult = WaitForMultipleObjects(2, handles,
false, // wait for _any_ signal, not all signals
delay);
// if the processLocal event was fired, don't bother showing window
// because this app is closing.
if (waitResult == WAIT_OBJECT_0 + 1) {
showWindow = false;
}
if (waitResult == WAIT_FAILED) {
FASTLOG2(FLog::RobloxWndInit, "Waiting for show window (video preroll) is done with ERROR, "
"wait result is = 0x%X, ERROR = 0x%X", waitResult, GetLastError());
} else {
FASTLOG1(FLog::RobloxWndInit,
"Waiting for show window (video preroll) is done and wait result is = 0x%X", waitResult);
}
if (shared_ptr<RBX::Soundscape::SoundService> soundService = weakSoundService.lock())
{
RBX::DataModel::LegacyLock lock(RBX::DataModel::get(soundService.get()), DataModelJob::Write);
soundService->muteAllChannels(false);
}
} else {
LogManager::ReportEvent(EVENTLOG_ERROR_TYPE,
RBX::format("Cannot create listening event (for video preroll): %s error: %d",
waitEventName.c_str(), GetLastError()).c_str());
}
}
if (showWindow) {
if (mainView)
mainView->ShowWindow();
// Bootstrapper will wait for this event, to make sure that app was started
ATL::CEvent robloxStartedEvent;
if (robloxStartedEvent.Open(EVENT_MODIFY_STATE, FALSE, "www.roblox.com/robloxStartedEvent"))
{
robloxStartedEvent.Set();
}
}
}
void Application::validateBootstrapperVersion()
{
boost::filesystem::path dir = RBX::FileSystem::getUserDirectory(false, RBX::DirExe);
boost::filesystem::path launcherName = "RobloxPlayerLauncher.exe";
boost::filesystem::path launcherPath = dir / launcherName;
CVersionInfo vi;
if (vi.Load(launcherPath.native()))
{
bool needUpdate = false;
std::string v = vi.GetFileVersionAsDotString();
int pos = v.rfind(".");
if (pos != std::string::npos)
{
std::string n = v.substr(pos+1);
if (atoi(n.c_str()) < FInt::BootstrapperVersionNumber)
needUpdate = true;
}
if (!needUpdate)
return;
try
{
std::string version;
std::string installHost;
std::string baseUrl = GetBaseURL();
pos = baseUrl.find("www");
if (pos == std::string::npos)
return;
// from www.roblox.com or www.gametest1.watrbx.local to setup.roblox.com or setup.gametest1.watrbx.local, etc...
installHost = "http://setup" + baseUrl.substr(pos+3);
{
Http http(installHost + "version");
http.get(version);
}
if (version.length() > 0)
{
std::string downloadUrl = RBX::format("%s%s-%s", installHost.c_str(), version.c_str(), launcherName.c_str());
Http http(downloadUrl);
std::string file;
http.get(file);
if (!file.length())
throw std::runtime_error("Invalid download");
// write file to tmp directory
boost::filesystem::path tempPath = boost::filesystem::temp_directory_path();
tempPath /= "rbxtmp";
std::ofstream outStream(tempPath.c_str(), std::ios_base::out | std::ios::binary);
outStream.write(file.c_str(), file.length());
outStream.close();
// copy file to current directory
boost::filesystem::path tempDestPath = dir / tempPath.filename();
boost::filesystem::copy_file(tempPath, tempDestPath, boost::filesystem::copy_option::overwrite_if_exists);
// remove previous launcher and rename new one
boost::system::error_code ec;
boost::filesystem::remove(launcherPath, ec);
// try again if failed to remove
if (ec.value() == boost::system::errc::io_error)
{
StandardOut::singleton()->print(MESSAGE_INFO, "validateBootstrapperVersion file remove failed, retrying in 10 seconds");
boost::this_thread::sleep(boost::posix_time::seconds(10));
boost::filesystem::remove(launcherPath);
}
boost::filesystem::rename(tempDestPath, launcherPath);
}
}
catch (boost::filesystem::filesystem_error& e)
{
StandardOut::singleton()->printf(MESSAGE_INFO, "validateBootstrapperVersion File Error: %s (%d)", e.what(), e.code().value());
RobloxGoogleAnalytics::trackEventWithoutThrottling(GA_CATEGORY_ERROR, "ValidateBootstrapperVersion File Error", e.what());
}
catch (RBX::base_exception& e)
{
StandardOut::singleton()->printf(MESSAGE_INFO, "validateBootstrapperVersion Error: %s", e.what());
RobloxGoogleAnalytics::trackEventWithoutThrottling(GA_CATEGORY_ERROR, "ValidateBootstrapperVersion Error", e.what());
}
}
else
RobloxGoogleAnalytics::trackEventWithoutThrottling(GA_CATEGORY_ERROR, "ValidateBootstrapperVersion Fail Loading Version Info", launcherPath.string().c_str());
}
void Application::onMessageOut(const StandardOutMessage& message)
{
std::string level;
switch (message.type)
{
case MESSAGE_INFO:
level = "INFO";
break;
case MESSAGE_WARNING:
level = "WARNING";
break;
case MESSAGE_ERROR:
level = "ERROR";
break;
default:
level.clear();
break;
}
std::ostringstream outputString;
outputString << level << ": " << message.message << std::endl;
OutputDebugString(outputString.str().c_str());
}
void Application::Teleport(const std::string& authenticationUrl,
const std::string& ticket,
const std::string& scriptUrl)
{
currentDocument->PrepareShutdown();
mainView->Stop();
shutdownVerbs();
currentDocument->Shutdown();
currentDocument.reset();
if (FFlag::ReloadSettingsOnTeleport)
{
// remove invalid children from settings()
RBX::GlobalAdvancedSettings::singleton()->removeInvalidChildren();
RBX::GlobalBasicSettings::singleton()->removeInvalidChildren();
}
{
HttpFuture authenticationResult = renewLoginAsync(authenticationUrl, ticket);
HttpFuture joinScriptResult = fetchJoinScriptAsync(scriptUrl);
// Create new document and start the game
StartNewGame(mainWindow, joinScriptResult, true);
authenticationResult.wait();
}
}
void Application::InitializeNewGame(HWND hWnd)
{
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, "Initializing new game");
currentDocument.reset(new Document());
currentDocument->Initialize( hWnd, !hideChat );
mainView.reset(new View(hWnd));
mainView->Start(currentDocument->getGame());
if (boost::shared_ptr<RBX::DataModel> datamodel = currentDocument->getGame()->getDataModel())
{
// Connect our handlers to webview requests from the DM
if (GuiService* guiService = datamodel->find<GuiService>())
{
guiService->openUrlWindow.connect( boost::bind(&Application::openUrlInBrowserApp, this, _1) );
// needed to clean up window when user closes it
guiService->urlWindowClosed.connect( boost::bind(&Application::closeBrowser,this) );
}
}
initVerbs();
}
void Application::StartNewGame(HWND hWnd, HttpFuture& scriptResult, bool isTeleport)
{
LogManager::ReportEvent(EVENTLOG_INFORMATION_TYPE, "Starting or teleporting to game");
currentDocument.reset(new Document());
currentDocument->Initialize( hWnd, !hideChat );
if (isTeleport)
{
RBXASSERT(mainView); // must not kill the view!
}
else
{
mainView.reset(new View(hWnd));
}
mainView->Start(currentDocument->getGame());
if (boost::shared_ptr<RBX::DataModel> datamodel = currentDocument->getGame()->getDataModel())
{
currentDocument->startedSignal.connect(boost::bind(&Application::onDocumentStarted, this, _1));
datamodel->submitTask(boost::bind(&Document::Start, currentDocument.get(), scriptResult, launchMode, isTeleport, getVRDeviceName()), DataModelJob::Write);
if (!isTeleport)
{
analyticsPoints.addPoint("Mode", "PlugIn");
analyticsPoints.addPoint("JoinScriptTaskSubmitted", Time::nowFastSec());
}
// Connect our handlers to webview requests from the DM
if (GuiService* guiService = datamodel->find<GuiService>())
{
guiService->openUrlWindow.connect( boost::bind(&Application::openUrlInBrowserApp, this, _1) );
// needed to clean up window when user closes it
guiService->urlWindowClosed.connect( boost::bind(&Application::closeBrowser,this) );
}
}
initVerbs();
}
void Application::initVerbs()
{
RBXASSERT(currentDocument);
DataModel* dm = currentDocument->getGame()->getDataModel().get();
leaveGameVerb.reset(new LeaveGameVerb(*mainView, dm));
RBX::ViewBase* gfx = mainView->GetGfxView();
recordToggleVerb.reset(new RecordToggleVerb(*currentDocument, mainView.get(), currentDocument->getGame().get()));
screenshotVerb.reset(new ScreenshotVerb(*currentDocument, gfx, currentDocument->getGame().get()));
toggleFullscreenVerb.reset(new ToggleFullscreenVerb(*mainView, dm, recordToggleVerb->GetVideoControl()));
}
void Application::shutdownVerbs()
{
toggleFullscreenVerb.reset();
leaveGameVerb.reset();
screenshotVerb.reset();
recordToggleVerb.reset();
}
void Application::doOpenUrl(const std::string url)
{
if(webView && webView->GetWindow(NULL))
return;
webView.reset( new RbxWebView(url, currentDocument->getGame()) );
if (HWND dialog = webView->Create( mainWindow, SW_SHOWNORMAL))
::ShowWindowAsync(dialog, SW_SHOWNORMAL);
else
MessageBox( mainWindow, "There was a problem opening the in-game browser.", "watrbx", MB_OK | MB_ICONSTOP);
}
void Application::openUrlInBrowserApp(const std::string url)
{
if(marshaller)
marshaller->Submit(boost::bind(&Application::doOpenUrl,this,url));
}
void Application::doCloseBrowser()
{
if(!webView)
return;
if(!webView->GetWindow(NULL))
return;
webView->closeDialog();
webView->DestroyWindow();
webView.reset();
}
void Application::closeBrowser()
{
if(marshaller)
marshaller->Submit(boost::bind(&Application::doCloseBrowser,this));
}
void Application::onDocumentStarted(bool isTeleport)
{
if (!isTeleport)
{
analyticsPoints.addPoint("JoinScriptTaskStarted", Time::nowFastSec());
analyticsPoints.report("ClientLaunch", DFInt::JoinInfluxHundredthsPercentage);
}
}
const char* Application::getVRDeviceName()
{
return mainView ? mainView->GetGfxView()->getVRDeviceName() : 0;
}
} // namespace RBX