Files
2025-09-18 17:55:52 -04:00

617 lines
21 KiB
C++

//
// PlaceLauncher.m
// RobloxMobile
//
// Created by David York on 9/25/12.
// Copyright (c) 2012 ROBLOX. All rights reserved.
//
#include "PlaceLauncher.h"
#include "util/Statistics.h"
#include "RobloxView.h"
#include "RobloxInfo.h"
#include "JNIMain.h"
#include "RobloxUtilities.h"
#include <iostream> // std::cout
#include <fstream> // std::ifstream
#include "v8datamodel/DebugSettings.h"
#include "v8datamodel/Workspace.h"
#include "v8datamodel/GuiService.h"
#include "v8datamodel/TeleportService.h"
#include "v8datamodel/LoginService.h"
#include "V8DataModel/ContentProvider.h"
#include "v8datamodel/MeshContentProvider.h"
#include "v8datamodel/TextureContentProvider.h"
#include "v8datamodel/FastLogSettings.h"
#include "V8DataModel/UserInputService.h"
#include "script/ScriptContext.h"
#include "util/Statistics.h"
#include "util/standardout.h"
#include "util/MemoryStats.h"
#include "v8datamodel/Stats.h"
#include "util/RobloxGoogleAnalytics.h"
#include "util/Http.h"
#include "LogManager.h"
#include "rbx/Profiler.h"
#include <rapidjson/document.h>
DYNAMIC_LOGVARIABLE(PlaceLauncher, 0)
DYNAMIC_LOGGROUP(NetworkTrace)
LOGGROUP(PlayerShutdownLuaTimeoutSeconds)
DYNAMIC_FASTINT(AndroidInfluxHundredthsPercentage)
using namespace RBX;
using namespace RBX::JNI;
namespace
{
static const std::string kAndroidClientAppSettings = "AndroidAppSettings";
static const std::string kAndroidClientSettingsAPIKey = "D6925E56-BFB9-4908-AAA2-A5B1EC4B2D79";
static const std::string kStartGameURL = "%sGame/PlaceLauncher.ashx?request=%s&%s&isPartyLeader=false&gender=&isTeleport=false";
static const std::string kStartGameStatusURL = "%sGame/PlaceLauncher.ashx?request=CheckGameJobStatus&jobId=%s";
} // namespace
namespace RBX
{
namespace JNI
{
extern std::string platformUserAgent; // Comes from LogManager
}
}
PlaceLauncher::PlaceLauncher()
{
teleporter.reset(new Teleporter(RBX::FunctionMarshaller::GetWindow()));
RBX::TeleportService::SetCallback(teleporter.get());
};
PlaceLauncher& PlaceLauncher::getPlaceLauncher()
{
static PlaceLauncher placeLauncher;
return placeLauncher;
}
static std::string ReadStringValue(std::string &data, std::string name)
{
std::string result;
int pos = data.find(name);
if (pos != -1)
{
int start = pos + name.length() + 3;
int end = data.find(",", start);
result = data.substr(start, end - start - 1);
}
pos = result.find("\\/"); // find first space
while (pos != std::string::npos)
{
result.replace(pos, 2, "/");
pos = result.find("\\/", pos + 1);
}
return result;
}
static RBX::ProtectedString fetchAndValidateScript(RBX::DataModel* dataModel, const std::string& urlScript)
{
RBX::Security::Impersonator impersonate(RBX::Security::COM);
std::ostringstream data;
if (RBX::ContentProvider::isUrl(urlScript))
{
RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write);
std::auto_ptr<std::istream> stream(RBX::ServiceProvider::create<RBX::ContentProvider>(dataModel)->getContent(RBX::ContentId(urlScript.c_str())));
boost::iostreams::copy(*stream, data);
}
else
return RBX::ProtectedString();
RBX::ProtectedString verifiedSource;
try
{
verifiedSource = RBX::ProtectedString::fromTrustedSource(data.str());
RBX::ContentProvider::verifyScriptSignature(verifiedSource, true);
}
catch(std::bad_alloc& e)
{
throw;
}
catch(RBX::base_exception& e)
{
return RBX::ProtectedString();
}
return verifiedSource;
}
static void executeUrlJoinScript(boost::shared_ptr<RBX::Game> game, const std::string& urlScript)
{
RBXASSERT(urlScript.find("join.ashx"));
boost::shared_ptr<RBX::DataModel> dataModel = game->getDataModel();
RBX::ProtectedString verifiedSource = fetchAndValidateScript(dataModel.get(), urlScript);
if (verifiedSource.empty())
return;
RBX::DataModel::LegacyLock lock(dataModel, RBX::DataModelJob::Write);
if (dataModel->isClosed())
return;
// new join script
std::string dataString = verifiedSource.getSource();
int firstNewLineIndex = dataString.find("\r\n");
if (dataString[firstNewLineIndex+2] == '{')
{
RobloxUtilities::getRobloxUtilities().getGameTimer().reset();
game->configurePlayer(RBX::Security::COM, dataString.substr(firstNewLineIndex+2));
return;
}
// old join script
RBX::ScriptContext* context = dataModel->create<RBX::ScriptContext>();
context->executeInNewThread(RBX::Security::COM, verifiedSource, "Start Script");
}
static void joinGamePlaceId(StartGameParams sgp, shared_ptr<RBX::Game> game)
{
RobloxUtilities::getRobloxUtilities().getGameTimer().reset();
try
{
int retrys = 5;
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"PlaceLauncher::joinGamePlaceId placeId = %i, userId = %d, accessCode = %s, gameId = %s, joinRequestType: %d\n", sgp.placeId, sgp.userId, sgp.accessCode.c_str(), sgp.gameId.c_str(), sgp.joinRequestType);
const char* base = GetBaseURL().c_str();
std::string requestParamsString, requestType;
std::string formattedParams;
switch (sgp.joinRequestType)
{
case JOIN_GAME_REQUEST_PLACEID:
requestParamsString = "placeId=%d";
formattedParams = RBX::format(requestParamsString.c_str(), sgp.placeId);
requestType = "RequestGame";
break;
case JOIN_GAME_REQUEST_USERID:
requestParamsString = "userId=%d";
formattedParams = RBX::format(requestParamsString.c_str(), sgp.userId);
requestType = "RequestFollowUser";
break;
case JOIN_GAME_REQUEST_PRIVATE_SERVER:
requestParamsString = "placeId=%d&accessCode=%s";
formattedParams = RBX::format(requestParamsString.c_str(), sgp.placeId, sgp.accessCode.c_str());
requestType = "RequestPrivateGame";
break;
case JOIN_GAME_REQUEST_GAME_INSTANCE:
requestParamsString = "placeId=%d&gameId=%s";
formattedParams = RBX::format(requestParamsString.c_str(), sgp.placeId, sgp.gameId.c_str());
requestType = "RequestGameJob";
break;
}
std::string response;
bool found = false;
int status = -1;
std::string url = RBX::format(kStartGameURL.c_str(), base, requestType.c_str(), formattedParams.c_str());
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"PlaceLauncher URL : %s\n", url.c_str());
std::string jobId;
while (retrys >= 0)
{
bool retryUsed = true;
response = "";
RBX::Http(url).get(response);
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,"PlaceLauncher Response : %s\n", response.c_str());
rapidjson::Document doc;
doc.Parse<rapidjson::kParseDefaultFlags>(response.c_str());
RBXASSERT(doc.HasMember("status"));
status = doc["status"].GetInt();
// Place join status results
// Waiting = 0,
// Loading = 1,
// Joining = 2,
// Disabled = 3,
// Error = 4,
// GameEnded = 5,
// GameFull = 6
// UserLeft = 10
// Restricted = 11
if (2 == status)
{
// not most efficient way to check status,
// but we have web calls so strstr is not a big deal
found = true;
break;
}
else if (0 == status || 1 == status)
{
// 0 or 1 is not an error - it is a sign that we should wait
retryUsed = false;
RBXASSERT(doc.HasMember("jobId"));
jobId = doc["jobId"].GetString();
// Check game status no matter what type of join we're attempting
url = RBX::format(kStartGameStatusURL.c_str(), base, Http::urlEncode(jobId).c_str());
}
else
break;
int sleepTime = 250*1000;
if (retryUsed)
{
--retrys;
sleepTime = 999*1000;
}
::usleep(sleepTime);
}
if (found)
{
executeUrlJoinScript(game, ReadStringValue(response, "joinScriptUrl"));
}
else
{
if (sgp.joinRequestType == JOIN_GAME_REQUEST_PLACEID)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "PlaceLauncher: Cannot connect to place %d, return from GetPlaceLauncher.ashx = %s", sgp.placeId, response.c_str());
else if (sgp.joinRequestType == JOIN_GAME_REQUEST_USERID)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "PlaceLauncher: Cannot follow user %d, return from GetPlaceLauncher.ashx = %s", sgp.userId, response.c_str());
else if (sgp.joinRequestType == JOIN_GAME_REQUEST_PRIVATE_SERVER)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "PlaceLauncher: Cannot join private server, accessCode = %s, return from GetPlaceLauncher.ashx = %s", sgp.accessCode.c_str(), response.c_str());
else if (sgp.joinRequestType)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "PlaceLauncher: Cannot join game instance, gameId = %s, return from GetPlaceLauncher.ashx = %s", sgp.gameId.c_str(), response.c_str());
// Place join status results
// Waiting = 0,
// Loading = 1,
// Joining = 2,
// Disabled = 3,
// Error = 4,
// GameEnded = 5,
// GameFull = 6
// UserLeft = 10
// Restricted = 11
PlaceLauncher::handleStartGameFailure(status);
}
}
catch (const RBX::base_exception& e)
{
if (sgp.joinRequestType == JOIN_GAME_REQUEST_PLACEID)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Exception thrown: Can't join place %d, because %s\n", sgp.placeId, e.what());
else if (sgp.joinRequestType == JOIN_GAME_REQUEST_USERID)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Exception thrown: Can't follow user %d, because %s\n", sgp.userId, e.what());
else if (sgp.joinRequestType == JOIN_GAME_REQUEST_PRIVATE_SERVER)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "PlaceLauncher: Exception thrown: Cannot join private server, accessCode = %s, because %s", sgp.accessCode.c_str(), e.what());
else if (sgp.joinRequestType)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "PlaceLauncher: Exception thrown: Cannot join game instance, gameId = %s, because %s", sgp.gameId.c_str(), e.what());
//[[PlaceLauncher sharedInstance] handleStartGameFailure];
}
}
void PlaceLauncher::handleStartGameFailure(int status = 99)
{
// Place join status results
// 0, 1, or 2 is handled in joinGamePlaceId above
// Disabled = 3,
// Error = 4,
// GameEnded = 5,
// GameFull = 6
// UserLeft = 10
// Restricted = 11
std::string errorMsgName; // this is the name of the error message string in res/values/strings.xml
std::string fastLogMsg; // only used for logging; not displayed to the user
switch (status)
{
case 3:
errorMsgName = "GameStartFailureDisabled";
fastLogMsg = "PlaceLauncher failure - game was disabled.";
break;
case 4:
errorMsgName = "GameStartFailureError";
fastLogMsg = "PlaceLauncher failure - game failed to start.";
break;
case 5:
errorMsgName = "GameStartFailureGameEnded";
fastLogMsg = "PlaceLauncher failure - game has ended.";
break;
case 6:
errorMsgName = "GameStartFailureGameFull";
fastLogMsg = "PlaceLauncher failure - game is full.";
break;
case 10:
errorMsgName = "GameStartFailureUserLeft";
fastLogMsg = "PlaceLauncher failure - the user you were following has left the game.";
break;
case 11:
errorMsgName = "GameStartFailureRestricted";
fastLogMsg = "PlaceLauncher failure - game not available for this platform.";
break;
case 99:
default:
errorMsgName = "GameStartFailureUnknown";
fastLogMsg = "PlaceLauncher failure - unknown game start failure.";
break;
}
RBX::StandardOut::singleton()->printf(MESSAGE_INFO, "in handleGameStartFailure, msg is %s", errorMsgName.c_str());
FASTLOG1(DFLog::PlaceLauncher, "%s", fastLogMsg.c_str());
JNI::exitGameWithError(errorMsgName); // from JNIMain
}
bool PlaceLauncher::prepareGame( const StartGameParams& sgp )
{
StandardOut::singleton()->printf(MESSAGE_INFO, "Asset Path in Place Launch: %s", RobloxInfo::getRobloxInfo().getAssetFolderPath().c_str());
FASTLOG(DFLog::PlaceLauncher, "PlaceLauncher prepareGame - START");
RBX::ContentProvider::setAssetFolder(RobloxInfo::getRobloxInfo().getAssetFolderPath().c_str());
Http::rbxUserAgent = platformUserAgent;
RBX::Game::globalInit(false);
RBX::TeleportService::SetBaseUrl(GetBaseURL().c_str());
/*
if(needsOnline)
{
Reachability* reachability = [Reachability reachabilityForInternetConnection];
NetworkStatus remoteStatus = [reachability currentReachabilityStatus];
if(remoteStatus == NotReachable)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: No Network Connection available");
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"ConnectionError", nil)];
return false;
}
else if(remoteStatus == ReachableViaWWAN)
{
// if user preference is for wifi only, don't allow this
NSUserDefaults *userDefaults =[NSUserDefaults standardUserDefaults];
if([[userDefaults stringForKey:@"wifionly_preference"] boolValue])
{
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(@"WiFiOnlyError", nil)];
return false;
}
}
}*/
std::string ios = "i";
ios += "o";
ios += "s";
ios = ios + "," + ios; // slight obfuscation
RBX::DataModel::hash = ios;
{
RBX::Security::Impersonator impersonate(RBX::Security::RobloxGameScript_);
RBX::GlobalBasicSettings::singleton()->loadState("");
}
RBX::Profiler::onThreadCreate("Main");
RBX::TaskScheduler::singleton().setThreadCount(RBX::TaskSchedulerSettings::singleton().getThreadPoolConfig());
FASTLOG(DFLog::PlaceLauncher, "PlaceLauncher prepareGame - END");
return true;
}
void initClientSettings()
{
// This needs to be put in a manager and cached
std::string clientSettingsData;
std::string androidAppSettingsData;
FetchClientSettingsData(CLIENT_APP_SETTINGS_STRING, CLIENT_SETTINGS_API_KEY, &clientSettingsData);
FetchClientSettingsData(kAndroidClientAppSettings.c_str(), kAndroidClientSettingsAPIKey.c_str(), &androidAppSettingsData);
RBX::ClientAppSettings settings = RBX::ClientAppSettings::singleton();
LoadClientSettingsFromString(CLIENT_APP_SETTINGS_STRING, clientSettingsData, &RBX::ClientAppSettings::singleton());
LoadClientSettingsFromString(kAndroidClientAppSettings.c_str(), androidAppSettingsData, &RBX::ClientAppSettings::singleton());
// Reset synchronized flags, they should be set by the server
FLog::ResetSynchronizedVariablesState();
}
static void initControlView(RobloxView* rbxView, bool isTouchDevice)
{
if(DataModel* dm = rbxView->getDataModel().get())
{
if(RBX::UserInputService* inputService = RBX::ServiceProvider::create<RBX::UserInputService>(dm))
{
inputService->setTouchEnabled(isTouchDevice);
}
}
}
void PlaceLauncher::finishGameSetup( boost::shared_ptr<RBX::Game> game)
{
rbxView = RobloxView::create_view(currentGame, gameParams.view, gameParams.viewWidth, gameParams.viewHeight);
rbxView->getDataModel()->submitTask(boost::bind(initControlView, rbxView, gameParams.isTouchDevice), RBX::DataModelJob::Write);
}
shared_ptr<RBX::Game> PlaceLauncher::setupGame( const StartGameParams& sgp )
{
FASTLOG(DFLog::PlaceLauncher, "PlaceLauncher setUpGame");
if(isCurrentlyPlayingGame)
return shared_ptr<RBX::Game>();
isCurrentlyPlayingGame = true;
initClientSettings();
RobloxGoogleAnalytics::lotteryInit(
ClientAppSettings::singleton().GetValueGoogleAnalyticsAccountPropertyIDPlayer(),
ClientAppSettings::singleton().GetValueGoogleAnalyticsThreadPoolMaxScheduleSize(),
ClientAppSettings::singleton().GetValueGoogleAnalyticsLoadPlayer());
StandardOut::singleton()->printf(MESSAGE_INFO, "account = %s, schedule = %d",
ClientAppSettings::singleton().GetValueGoogleAnalyticsAccountPropertyIDPlayer(),
ClientAppSettings::singleton().GetValueGoogleAnalyticsThreadPoolMaxScheduleSize());
RBX::Http::SetUseStatistics(true);
if(prepareGame( sgp ))
{
FASTLOGS(DFLog::PlaceLauncher, "Game's base URL: %s", GetBaseURL().c_str());
currentGame = shared_ptr<RBX::Game>( new RBX::SecurePlayerGame(NULL, GetBaseURL().c_str()) );
rbxView = RobloxView::create_view(currentGame, sgp.view, sgp.viewWidth, sgp.viewHeight);
rbxView->getDataModel()->submitTask(boost::bind(initControlView, rbxView, gameParams.isTouchDevice), RBX::DataModelJob::Write);
if (FLog::PlayerShutdownLuaTimeoutSeconds > 0)
currentGame->getDataModel()->create<RBX::ScriptContext>();
return currentGame;
}
return shared_ptr<RBX::Game>();
}
shared_ptr<RBX::Game> PlaceLauncher::setupPreloadedGame( const StartGameParams& sgp )
{
FASTLOG(DFLog::PlaceLauncher, "PlaceLauncher SetupPreloadedGame");
return setupGame( sgp );
}
bool PlaceLauncher::startGame( boost::function0 <void> scriptFunction , shared_ptr<RBX::Game> preloadedGame)
{
// Start the script thread!
boost::thread(RBX::thread_wrapper(scriptFunction, "GameStartScript"));
return true;
}
bool PlaceLauncher::startGame( const StartGameParams& sgp )
{
gameParams = sgp;
FASTLOG(DFLog::PlaceLauncher, "PlaceLauncher StartGame");
shared_ptr<RBX::Game> game = setupPreloadedGame( sgp );
if( game != shared_ptr<RBX::Game>())
{
currentGame = game;
return startGame(boost::bind(&joinGamePlaceId, sgp, game), game);
}
return true;
}
void PlaceLauncher::leaveGame ( bool userRequestedLeave)
{
RBX::Time::Interval gamePlayInterval = RobloxUtilities::getRobloxUtilities().getGameTimer().reset();
RBX::Analytics::InfluxDb::Points points;
points.addPoint("SessionReport" , "AndroidSuccess");
points.addPoint("FreeMemoryKB" , RBX::MemoryStats::freeMemoryBytes());
points.addPoint("UsedMemoryKB" , RBX::MemoryStats::usedMemoryBytes());
points.addPoint("PlayTime" , gamePlayInterval.seconds());
points.report("Android-RobloxPlayer-SessionReport", DFInt::AndroidInfluxHundredthsPercentage);
deleteRobloxView(true);
}
void PlaceLauncher::deleteRobloxView( bool resetCurrentGame )
{
if(resetCurrentGame)
{
// remove all shared ptr to games so it can properly be destroyed
currentGame.reset();
}
if( teleporter )
{
teleporter.reset();
}
// do actual destruction of the Roblox View, only if it isn't NULL
if (rbxView)
{
// Destroy the view synchronously
// Take care to zero-out view pointer beforehand to support reentrancy
RobloxView* view = rbxView;
rbxView = NULL;
delete view;
}
}
static void joinGameTeleport(std::string url, std::string ticket, std::string script, shared_ptr<RBX::Game> game)
{
try
{
// get authentication URL
std::string compound = url;
if (!ticket.empty())
{
compound += "?suggest=";
compound += ticket;
}
// issue an authentication request
std::string result;
try
{
RBX::Http http(compound.c_str());
http.setAuthDomain(GetBaseURL().c_str());
http.get(result);
}
catch (const RBX::base_exception& e)
{
}
// run game
executeUrlJoinScript(game, script);
}
catch (const RBX::base_exception& e)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Teleport failed: %s\n", e.what());
}
}
void PlaceLauncher::teleport( std::string ticket, std::string authUrl, std::string script)
{
if (!rbxView) return;
// first do an empty reset so serviceprovider oldProvider is called first
currentGame.reset();
currentGame.reset(new RBX::SecurePlayerGame(NULL, GetBaseURL().c_str()));
rbxView->replaceGame(currentGame);
rbxView->getDataModel()->submitTask(boost::bind(initControlView, rbxView, gameParams.isTouchDevice), RBX::DataModelJob::Write);
// Don't use submit task on data model for this thread, the script fetched in
// the spawned thread does data model setup.
// joinGameTeleport will take ownership of the game.
boost::thread joinScriptThread(boost::bind(&joinGameTeleport, authUrl, ticket, script, currentGame));
}
void PlaceLauncher::shutDownGraphics()
{
if (!rbxView) return;
rbxView->shutDownGraphics(currentGame);
}
void PlaceLauncher::startUpGraphics(void *wnd, unsigned int width, unsigned int height)
{
if (!rbxView) return;
rbxView->startUpGraphics(currentGame, wnd, width, height);
}