Files
watrbx-game-engine/iOS/SharedCode/PlaceLauncher.mm
T
2025-09-18 17:55:52 -04:00

1288 lines
48 KiB
Plaintext

//
// PlaceLauncher.m
// RobloxMobile
//
// Created by David York on 9/25/12.
// Copyright (c) 2012 ROBLOX. All rights reserved.
//
#include <boost/filesystem.hpp>
#include <boost/iostreams/copy.hpp>
#import "PlaceLauncher.h"
#import "AppDelegate.h"
#ifdef STANDALONE_PURCHASING
#import "StandaloneAppStore.h"
#endif
#import "StoreManager.h"
#import "Reachability.h"
#import "GameViewController.h"
#import "MainViewController.h"
#import "ControlView.h"
#import "RobloxWebUtility.h"
#import "UIScreen+PortraitBounds.h"
#import "RobloxNotifications.h"
#import "RobloxInfo.h"
#import "RobloxGoogleAnalytics.h"
#import "RobloxAlert.h"
#import "ObjectiveCUtilities.h"
#import "MarshallerInterface.h"
#import "RobloxMemoryManager.h"
#import "UserInfo.h"
#import "SessionReporter.h"
#import "NotificationHelper.h"
#import "CrashReporter.h"
#include "RobloxView.h"
#include "FunctionMarshaller.h"
#include "Teleporter.h"
#include "RbxFormat.h"
#include "script/ScriptContext.h"
#include "util/Http.h"
#include "util/Statistics.h"
#include "util/MemoryStats.h"
#include "util/RobloxGoogleAnalytics.h"
#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/MarketplaceService.h"
#include "v8datamodel/AdService.h"
#include "v8datamodel/NotificationService.h"
#include "V8DataModel/GameBasicSettings.h"
#include "GfxBase/ViewBase.h"
#include "util/Http.h"
#include "rbx/Profiler.h"
#include "iOSSettingsService.h"
#include <rapidjson/document.h>
const std::string kStartGameStatusURL = "%sGame/PlaceLauncher.ashx?request=CheckGameJobStatus&jobId=%s";
FASTFLAG(GoogleAnalyticsTrackingEnabled)
DYNAMIC_LOGGROUP(GoogleAnalyticsTracking)
LOGGROUP(Network)
LOGGROUP(PlayerShutdownLuaTimeoutSeconds)
@implementation RBXGameLaunchParams
+(RBXGameLaunchParams*) InitDefaults
{
RBXGameLaunchParams* params = [[RBXGameLaunchParams alloc] init];
params.joinRequestType = JOIN_GAME_REQUEST_PLACEID;
params.joinRequestString = @"RequestGame";
params.targetId = -1;
params.accessCode = @"";
params.gameInstanceId = @"";
return params;
}
+(RBXGameLaunchParams*) InitParamsForFollowUser:(int)userID
{
RBXGameLaunchParams* params = [RBXGameLaunchParams InitDefaults];
params.joinRequestType = JOIN_GAME_REQUEST_USERID;
params.joinRequestString = @"RequestFollowUser";
params.targetId = userID;
return params;
}
+(RBXGameLaunchParams*) InitParamsForJoinPlace:(int)placeID
{
RBXGameLaunchParams* params = [RBXGameLaunchParams InitDefaults];
params.targetId = placeID;
return params;
}
+(RBXGameLaunchParams*) InitParamsForJoinPrivateServer:(int)placeID withAccessCode:(NSString*)vpsAccessCode
{
RBXGameLaunchParams* params = [RBXGameLaunchParams InitDefaults];
params.joinRequestType = JOIN_GAME_REQUEST_PRIVATE_SERVER;
params.joinRequestString = @"RequestPrivateGame";
params.targetId = placeID;
params.accessCode = vpsAccessCode;
return params;
}
+(RBXGameLaunchParams*) InitParamsForJoinGameInstance:(int)placeID withInstanceID:(NSString*)instanceID
{
RBXGameLaunchParams* params = [RBXGameLaunchParams InitDefaults];
params.joinRequestType = JOIN_GAME_REQUEST_GAME_INSTANCE;
params.joinRequestString = @"RequestGameJob";
params.targetId = placeID;
params.gameInstanceId = instanceID;
return params;
}
-(std::string) getStringJoinURL
{
const std::string kStartGameURL = "%sGame/PlaceLauncher.ashx?%s&isPartyLeader=false&gender=&isTeleport=false";
//base - Game / PlaceLauncher.ashx ? (request=%s and other parameters) & isPartyLeader=false & gender= & isTeleport=false
const char* base = [[RobloxInfo getBaseUrl] UTF8String];
std::string url, formattedParameters;
switch (self.joinRequestType)
{
case JOIN_GAME_REQUEST_USERID:
{
formattedParameters = RBX::format("request=%s&userId=%d", self.joinRequestString.UTF8String, self.targetId);
} break;
case JOIN_GAME_REQUEST_GAME_INSTANCE:
{
//note the inconsistency
formattedParameters = RBX::format("request=%s&placeId=%d&gameId=%s", self.joinRequestString.UTF8String, self.targetId, self.gameInstanceId.UTF8String);
} break;
case JOIN_GAME_REQUEST_PRIVATE_SERVER:
{
formattedParameters = RBX::format("request=%s&placeId=%d&accessCode=%s", self.joinRequestString.UTF8String, self.targetId, self.accessCode.UTF8String);
} break;
default: //JOIN_GAME_REQUEST_PLACEID
{
formattedParameters = RBX::format("request=%s&placeId=%d", self.joinRequestString.UTF8String, self.targetId);
} break;
}
url = RBX::format(kStartGameURL.c_str(), base, formattedParameters.c_str());
return url;
}
@end
static bool bPlaceViewPresented = false;
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;
}
// binds ogre views to our iOS views (so we can control segues and such)
static void bindOgreViews(RobloxView* rbxView, GameViewController* gameController)
{
if (MainViewController* mainViewController = [MainViewController sharedInstance])
{
UIView* mUIView = gameController.view;
UIWindow* mUIWindow = mUIView.window;
[mainViewController setOgreView:mUIView];
mUIView.multipleTouchEnabled = YES;
ControlView* controlView = [[ControlView alloc] init:[[UIScreen mainScreen] portraitBounds] withGame:rbxView->getGame()];
[gameController addControlView:controlView];
[mainViewController setOgreWindow:mUIWindow];
}
}
// if we have all of our view controls set up properly, this function will present
// our current game view over our current view
static void presentGameView()
{
// make sure we are in main thread to show controller
dispatch_async(dispatch_get_main_queue(), ^{
MainViewController* mainController = [MainViewController sharedInstance];
if(!mainController)
return;
UIViewController* ogreViewController = [mainController getOgreViewController];
if(!ogreViewController)
return;
UIViewController* lastNonGameController = [mainController getLastNonGameController];
if(!lastNonGameController)
return;
if ([lastNonGameController respondsToSelector:@selector(presentedViewController)]) {
if(lastNonGameController.presentedViewController != ogreViewController) {
// present the game view on top of whatever non game controller we are using
// this way, when we leave the game we will go back to what we were doing
[lastNonGameController presentViewController:ogreViewController animated:NO completion:^{
if(lastNonGameController && [lastNonGameController respondsToSelector:@selector(handleStartGameSuccess)])
[lastNonGameController performSelector:@selector(handleStartGameSuccess)];
bPlaceViewPresented = true;
}];
}
}
});
}
static void initControlViewHelper(RobloxView* rbxView, BOOL presentGameAutomatically, GameViewController* gameController)
{
bindOgreViews(rbxView, gameController);
if(presentGameAutomatically)
presentGameView();
dispatch_async(dispatch_get_main_queue(),^{
PlaceLauncher* placeLauncher = [PlaceLauncher sharedInstance];
[[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_GAME_BOUND_VIEWS object:placeLauncher userInfo:nil];
});
}
static void initControlView(RobloxView* rbxView, BOOL presentGameAutomatically, GameViewController* gameController, RBX::FunctionMarshaller* marshaller)
{
// This needs to be execute, because this task is holding the write lock needed
// to complete the work in initControlViewHelper
marshaller->Execute(boost::bind(&initControlViewHelper, rbxView, presentGameAutomatically, gameController), NULL);
}
static void finishTeleportHelper(RobloxView* rbxView, shared_ptr<RBX::Game> game)
{
if(MainViewController* mainViewController = [MainViewController sharedInstance])
{
GameViewController* gameController = [mainViewController getOgreViewController];
for(ControlView* control in gameController.view.subviews)
{
[control setGame:game];
break;
}
}
}
static void finishTeleport(RobloxView* rbxView, shared_ptr<RBX::Game> game, RBX::FunctionMarshaller* marshaller)
{
// This needs to be execute, because this task is holding the write lock needed
// to complete the work in finishTeleportImpl
marshaller->Execute(boost::bind(&finishTeleportHelper, rbxView, game), NULL);
}
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] == '{')
{
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 joinLocalGame(int portID, const std::string& ipAddress, shared_ptr<RBX::Game> game, int userId)
{
try
{
const char* base = [[RobloxInfo getBaseUrl] UTF8String];
std::string scriptString = RBX::format("%sGame/Join.ashx?userID=%i&serverPort=%i&server=%s",
base, userId, portID, ipAddress.c_str());
executeUrlJoinScript(game, scriptString);
}
catch (const RBX::base_exception& e)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Exception thrown: Can't join local place (%s,%i), because %s\n", ipAddress.c_str(), portID, e.what());
[[PlaceLauncher sharedInstance] handleStartGameFailure];
}
}
static void joinGameWithJoinScript(const std::string& joinScript, shared_ptr<RBX::Game> game)
{
try
{
executeUrlJoinScript(game, joinScript);
}
catch (const RBX::base_exception& e)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Exception thrown: Can't join game with joinScript because %s\n", e.what());
[[PlaceLauncher sharedInstance] handleStartGameFailure];
}
}
static void joinGameWithParameters(shared_ptr<RBX::Game> game, RBXGameLaunchParams* params)
{
try
{
int retrys = 5;
// const char* base = [[RobloxInfo getBaseUrl] UTF8String];
NSDictionary *dictionary;
dictionary = [NSDictionary dictionaryWithObjectsAndKeys: [RobloxInfo getUserAgentString], @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
std::string response;
bool found = false;
int status = -1;
std::string url = [params getStringJoinURL];
std::string jobId;
while (retrys >= 0)
{
bool retryUsed = true;
response = "";
RBX::Http(url).get(response);
rapidjson::Document doc;
doc.Parse<rapidjson::kParseDefaultFlags>(response.c_str());
RBXASSERT(doc.HasMember("status"));
status = doc["status"].GetInt();
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();
}
int sleepTime = 250*1000;
if (retryUsed)
{
--retrys;
sleepTime = 999*1000;
}
::usleep(sleepTime);
}
bool bAppActive = [[PlaceLauncher sharedInstance] appActive];
if (found && bAppActive)
{
NSString *label = [NSString stringWithFormat:@"%s - %d", params.joinRequestString.UTF8String, params.targetId];
[RobloxGoogleAnalytics setEventTracking:@"PlaySuccess" withAction:@"Success" withLabel:label withValue:1];
[[CrashReporter sharedInstance] logIntKeyValue:label withValue: params.targetId];
executeUrlJoinScript(game, ReadStringValue(response, "joinScriptUrl"));
[[PlaceLauncher sharedInstance] setLastPlaceId:params.targetId];
[[SessionReporter sharedInstance] reportSessionFor:ENTER_GAME PlaceId:params.targetId];
[RobloxGoogleAnalytics setPageViewTracking:@"Visit/Success/Join"];
dispatch_async(dispatch_get_main_queue(), ^{
[[RobloxMemoryManager sharedInstance] startMaxMemoryTracker];
});
}
else
{
if (!bAppActive)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Cannot connect to place %d. App is not active.", params.targetId);
if (params.joinRequestType == JOIN_GAME_REQUEST_PLACEID)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Cannot connect to place %d, return from GetPlaceLauncher.ashx = %s", params.targetId, response.c_str());
else if (params.joinRequestType == JOIN_GAME_REQUEST_GAME_INSTANCE)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Cannot connect to place %d with game instance id - %s", params.targetId, params.gameInstanceId.UTF8String);
else if (params.joinRequestType == JOIN_GAME_REQUEST_PRIVATE_SERVER)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Cannot connect to private server %d with access code - %s", params.targetId, params.accessCode.UTF8String);
else
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Cannot follow user %d, return from GetPlaceLauncher.ashx = %s", params.targetId, 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
*/
NSString *statusMessage = @"UnknownError";
if (5 == status)
statusMessage = @"ConnectionErrorGameEnded";
else if (6 == status)
statusMessage = @"ConnectionErrorGameFull";
else if (3 == status)
statusMessage = @"ConnectionErrorGameDisabled";
else if (4 == status)
statusMessage = @"GeneralGameStartError";
else if (10 == status)
statusMessage = @"ConnectionErrorGameUserLeft";
else if (11 == status)
statusMessage = @"ConnectionErrorGameRestricted";
NSString *label = [NSString stringWithFormat:@"%s - %d", params.joinRequestString.UTF8String, params.targetId];
[RobloxGoogleAnalytics setEventTracking:@"PlayErrors" withAction:statusMessage withLabel:label withValue:1];
if (bAppActive)
[RobloxAlert RobloxAlertWithMessage:NSLocalizedString(statusMessage, nil)];
[[PlaceLauncher sharedInstance] handleStartGameFailure];
}
}
catch (const RBX::base_exception& e)
{
if (params.joinRequestType == JOIN_GAME_REQUEST_PLACEID)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Exception thrown: Can't join place %i, because %s\n", params.targetId, e.what());
else if (params.joinRequestType == JOIN_GAME_REQUEST_GAME_INSTANCE)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Exception thrown: Can't join place %i with game instance %s, because %s\n", params.targetId, params.gameInstanceId.UTF8String, e.what());
else if (params.joinRequestType == JOIN_GAME_REQUEST_PRIVATE_SERVER)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Exception thrown: Can't join private server of place %i with access code %s, because %s\n", params.targetId, params.accessCode.UTF8String, e.what());
else
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Exception thrown: Can't follow user %i, because %s\n", params.targetId, e.what());
[[PlaceLauncher sharedInstance] handleStartGameFailure];
}
}
static void joinGameTeleport(std::string url, std::string ticket, std::string script, NSObject* controller, 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);
if ([controller respondsToSelector:@selector(handleStartGameSuccess)])
[controller performSelector:@selector(handleStartGameSuccess)];
}
catch (const RBX::base_exception& e)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR,"PlaceLauncher: Teleport failed: %s\n", e.what());
[[PlaceLauncher sharedInstance] handleStartGameFailure];
}
}
/////////////////////////////////////////////////////////////////////////////////
//
// PlaceLauncher implementation
//
/////////////////////////////////////////////////////////////////////////////////
// private methods
@interface PlaceLauncher()
-(BOOL) startGame:(boost::function0<void>) scriptFunction controller:(UIViewController*) lastNonGameController preloadedGame:(shared_ptr<RBX::Game>) preloadedGame presentGameAutomatically:(BOOL) presentGameAutomatically;
-(shared_ptr<RBX::Game>) setupPreloadedGameWithNonGameController:(UIViewController*) lastNonGameController needsOnline:(BOOL) needsOnline;
-(shared_ptr<RBX::Game>) setupPreloadedGameWithNonGameController:(UIViewController*) lastNonGameController unsecuredGame:(BOOL) unsecuredGame needsOnline:(BOOL) needsOnline;
-(shared_ptr<RBX::Game>) setupGame:(UIViewController*)lastNonGameController needsOnline:(BOOL) needsOnline;
-(shared_ptr<RBX::Game>) setupGame:(UIViewController*)lastNonGameController unsecuredGame:(BOOL) unsecuredGame needsOnline:(BOOL) needsOnline;
-(void) setupDatamodelConnections:(shared_ptr<RBX::DataModel>) dataModel;
@end
@implementation PlaceLauncher
-(id)init
{
if(self = [super init])
{
rbxView = NULL;
hasReceivedMemoryWarning = NO;
isCurrentlyPlayingGame = NO;
lastPlaceId = 0;
teleporter.reset(new Teleporter(self, RBX::FunctionMarshaller::GetWindow()));
RBX::TeleportService::SetCallback(teleporter.get());
}
return self;
}
-(void)dealloc
{
RBX::TeleportService::SetCallback(NULL);
teleporter.reset();
}
+ (id)sharedInstance
{
static dispatch_once_t placeLauncherPred = 0;
__strong static id _sharedObject = nil;
dispatch_once(&placeLauncherPred, ^{ // Need to use GCD for thread-safe allocation
_sharedObject = [[self alloc] init];
});
return _sharedObject;
}
-(BOOL) getIsCurrentlyPlayingGame
{
return isCurrentlyPlayingGame;
}
-(void) handleStartGameFailure
{
[[PlaceLauncher sharedInstance] leaveGame];
if (![[MainViewController sharedInstance] getLastNonGameController])
return;
dispatch_async(dispatch_get_main_queue(), ^
{
UIViewController* lastNonGameController = [[MainViewController sharedInstance] getLastNonGameController];
if (lastNonGameController && [lastNonGameController respondsToSelector:@selector(handleStartGameFailure)])
[lastNonGameController performSelector:@selector(handleStartGameFailure)];
isCurrentlyPlayingGame = NO;
});
}
-(bool)prepareGame:(BOOL) needsOnline
{
// Global static variable setup
NSString* exePath = [[NSBundle mainBundle] resourcePath];
exePath = [exePath stringByAppendingString:@"/content"];
RBX::ContentProvider::setAssetFolder([exePath UTF8String]);
RBX::Http::rbxUserAgent = [[RobloxInfo getUserAgentString] UTF8String];
{
bool useCurl = rand() % 100 < RBX::ClientAppSettings::singleton().GetValueHttpUseCurlPercentageMacClient();
FASTLOG1(FLog::Network, "Use CURL = %d", useCurl);
RBX::Http::SetUseCurl(useCurl);
RBX::Http::SetUseStatistics(true);
}
{
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());
}
}
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());
return true;
}
-(void) setLastPlaceId:(int) lastId;
{
lastPlaceId = lastId;
}
-(void) checkPlacePartCount
{
// only show these alerts if the user preference says to
NSUserDefaults *userDefaults =[NSUserDefaults standardUserDefaults];
if(![[userDefaults stringForKey:@"warnings_preference"] boolValue])
return;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
// check num of parts in the level, warn user if over our limits
iOSSettingsService * iosSettings = [[RobloxWebUtility sharedInstance] getCachediOSSettings];
int maxIdealParts = iosSettings->GetValueiPad2_MaximumIdealParts();
if (maxIdealParts > 0)
{
RBX::World* world = NULL;
RBX::Workspace* workspace = NULL;
if (rbxView && rbxView->getDataModel() != NULL)
workspace = rbxView->getDataModel()->getWorkspace();
if (workspace != NULL)
world = workspace->getWorld();
if (world != NULL)
{
int numPrimitives = world->getNumPrimitives();
if (maxIdealParts < numPrimitives)
{
NSString* warningMsg = NSLocalizedString(@"WarnPlaceIsNotIdeal", nil);
NSString* detailMsg = [NSString stringWithFormat:NSLocalizedString(@"WarnTooManyParts", nil), numPrimitives, maxIdealParts];
NSString* fullMsg = [NSString stringWithFormat:@"%@\n\n%@", warningMsg, detailMsg];
NSString* placeId = [NSString stringWithFormat:@"%d", lastPlaceId];
[RobloxGoogleAnalytics setEventTracking:@"PlayErrors" withAction:@"TooManyParts" withLabel:placeId withValue:0];
[RobloxAlert RobloxAlertWithMessage:fullMsg];
}
}
}
});
}
-(void)placeDidFinishLoading
{
[[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_GAME_FINISHED_LOADING object:self userInfo:nil];
[self checkPlacePartCount];
}
-(void) deleteRobloxView:(BOOL) resetCurrentGame
{
if(resetCurrentGame)
{
// remove all shared ptr to games so it can properly be destroyed
currentGame.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;
[[RobloxMemoryManager sharedInstance] stopFreeMemoryChecker];
}
}
-(void) finishGameSetup: (boost::shared_ptr<RBX::Game>)game gameViewController:(GameViewController*) gameController
{
CGSize screenSize = [[UIScreen mainScreen] portraitBounds].size;
// View dimensions are rotated since we're rendering in landscape
int viewWidth = screenSize.height;
int viewHeight = screenSize.width;
RBX::GameBasicSettings::singleton().setFullScreen(true);
rbxView = RobloxView::create_view(game, (__bridge void*)gameController.view, viewWidth, viewHeight);
rbxView->getDataModel()->submitTask(boost::bind(boostFuncFromSelector(@selector(finishGameSetupWithLock), self)), RBX::DataModelJob::Write);
}
-(void) finishGameSetupWithLock
{
if(rbxView->getDataModel()->getIsGameLoaded())
{
[self placeDidFinishLoading];
}
else
{
rbxView->getDataModel()->gameLoadedSignal.connect(boostFuncFromSelector(@selector(placeDidFinishLoading),self));
}
[self setupDatamodelConnections:rbxView->getDataModel()];
}
-(void) setupDatamodelConnections:(shared_ptr<RBX::DataModel>) dataModel
{
// connect to events to show a web page in game
if(RBX::GuiService* guiService = dataModel->find<RBX::GuiService>())
guiService->openUrlWindow.connect( boostFuncFromSelector_1<std::string>(@selector(openUrlWindow:), [[MainViewController sharedInstance] getOgreViewController]) );
if(RBX::AdService* adService = dataModel->find<RBX::AdService>())
adService->playVideoAdSignal.connect( boostFuncFromSelector(@selector(playVideoAd), [[MainViewController sharedInstance] getOgreViewController]) );
#ifdef STANDALONE_NOTIFICATION
id notificationHelper = [NotificationHelper sharedInstance];
if(RBX::NotificationService* notificationService = dataModel->find<RBX::NotificationService>())
{
notificationService->scheduleNotificationSignal.connect( boostFuncFromSelector_4<int, int, std::string, int>(@selector(scheduleLocalNotification:), notificationHelper) );
notificationService->cancelNotificationSignal.connect( boostFuncFromSelector_2<int, int>(@selector(cancelLocalNotification:), notificationHelper) );
notificationService->cancelAllNotificationSignal.connect( boostFuncFromSelector_1<int>(@selector(cancelAllNotification:), notificationHelper) );
notificationService->getScheduledNotificationsSignal.connect( boostFuncFromSelector_3<int, boost::function<void(shared_ptr<const RBX::Reflection::ValueArray>)>, boost::function<void(std::string)> >(@selector(getScheduledNotifications:), notificationHelper) );
}
#endif
dispatch_async(dispatch_get_main_queue(), ^{
[[RobloxMemoryManager sharedInstance] startFreeMemoryChecker];
});
RBX::Network::Players* players = rbxView->getDataModel()->find<RBX::Network::Players>();
childConnection = players->onDemandWrite()->childAddedSignal.connect(boostFuncFromSelector_1< shared_ptr<RBX::Instance> >(@selector(childAdded:), self));
// connect to events to show signup in game
if( RBX::LoginService* loginService = RBX::ServiceProvider::create<RBX::LoginService>(dataModel.get()) )
loginService->promptLoginSignal.connect(boostFuncFromSelector(@selector(handlePromptLoginSignal), [[MainViewController sharedInstance] getOgreViewController]));
if (RBX::MarketplaceService* marketplaceService = RBX::ServiceProvider::create<RBX::MarketplaceService>(dataModel.get()) )
{
id storeManager = GetStoreMgr;
#ifdef STANDALONE_PURCHASING
if ([storeManager isKindOfClass:[StandaloneAppStore class]])
{
StandaloneAppStore* appStore = (StandaloneAppStore*) storeManager;
marketplaceService->promptThirdPartyPurchaseRequested.connect(boostFuncFromSelector_2<shared_ptr<RBX::Instance>, std::string>(@selector(promptThirdPartyPurchase:productId:), appStore));
}
#else
if([storeManager isKindOfClass:[StoreManager class]])
{
StoreManager* appStore = (StoreManager*) storeManager;
marketplaceService->promptNativePurchaseRequested.connect(boostFuncFromSelector_2<shared_ptr<RBX::Instance>, std::string>(@selector(promptNativePurchase:productId:), appStore));
}
#endif
}
}
-(void) setLastNonGameController:(UIViewController*)lastNonGameController needsOnline:(BOOL) needsOnline
{
[[MainViewController sharedInstance] setLastNonGameController:lastNonGameController];
if (lastNonGameController != nil && ![self prepareGame:needsOnline])
[self handleStartGameFailure];
}
-(void) createGame:(shared_ptr<RBX::Game>)game presentGameAutomatically:(BOOL) presentGameAutomatically
{
hasReceivedMemoryWarning = NO;
// make sure we have no other views going
[self deleteRobloxView:NO];
UIViewController* lastController = [[MainViewController sharedInstance] getLastNonGameController];
if(lastController)
{
GameViewController* gameController = [[GameViewController alloc] init];
gameController.modalPresentationStyle = UIModalPresentationFullScreen;
[[MainViewController sharedInstance] setOgreViewController:gameController];
[self finishGameSetup:game gameViewController:gameController];
rbxView->getDataModel()->submitTask(boost::bind(initControlView, rbxView, presentGameAutomatically, gameController, RBX::FunctionMarshaller::GetWindow()), RBX::DataModelJob::Write);
}
}
-(shared_ptr<RBX::Game>) setupGame:(UIViewController*)lastNonGameController needsOnline:(BOOL) needsOnline
{
return [self setupGame:lastNonGameController unsecuredGame:NO needsOnline:needsOnline];
}
-(shared_ptr<RBX::Game>) setupGame:(UIViewController*)lastNonGameController unsecuredGame:(BOOL) unsecuredGame needsOnline:(BOOL) needsOnline
{
if(isCurrentlyPlayingGame)
return shared_ptr<RBX::Game>();
[[RobloxWebUtility sharedInstance] updateAllClientSettingsWithCompletion:nil];
// don't allow screen to dim while playing game
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
isCurrentlyPlayingGame = YES;
[self setLastNonGameController:lastNonGameController needsOnline:needsOnline];
currentGame = unsecuredGame ? shared_ptr<RBX::Game>( new RBX::UnsecuredStudioGame(NULL, GetBaseURL().c_str()) ) :
shared_ptr<RBX::Game>( new RBX::SecurePlayerGame(NULL, GetBaseURL().c_str()) );
if (FLog::PlayerShutdownLuaTimeoutSeconds > 0)
currentGame->getDataModel()->create<RBX::ScriptContext>();
return currentGame;
}
-(void) presentGameViewController
{
presentGameView();
}
-(shared_ptr<RBX::Game>) setupPreloadedGameWithNonGameController:(UIViewController*) lastNonGameController unsecuredGame:(BOOL) unsecuredGame needsOnline:(BOOL) needsOnline
{
return [self setupGame:lastNonGameController unsecuredGame:unsecuredGame needsOnline:needsOnline];
}
-(shared_ptr<RBX::Game>) setupPreloadedGameWithNonGameController:(UIViewController*) lastNonGameController needsOnline:(BOOL) needsOnline
{
return [self setupGame:lastNonGameController needsOnline:needsOnline];
}
-(void) injectJoinScript:(NSString*)joinUrlScript
{
// Start the script thread!
boost::thread(RBX::thread_wrapper(boost::bind(&joinGameWithJoinScript, [joinUrlScript UTF8String], rbxView->getGame()), "InjectStartScript"));
}
-(BOOL) startGameLocal:(int)portId ipAddress:(NSString*)ipAddress controller:(UIViewController*)lastNonGameController presentGameAutomatically:(BOOL)presentGameAutomatically userId:(int)userId
{
shared_ptr<RBX::Game> game = [self setupPreloadedGameWithNonGameController:lastNonGameController unsecuredGame:YES needsOnline:YES];
if( game != shared_ptr<RBX::Game>())
return [self startGame:boost::bind(&joinLocalGame, portId, [ipAddress UTF8String], game, userId) controller:lastNonGameController preloadedGame:game presentGameAutomatically:presentGameAutomatically];
return false;
}
-(BOOL) startGame:(RBXGameLaunchParams*)params controller:(UIViewController*)lastNonGameController presentGameAutomatically:(BOOL)presentGameAutomatically
{
shared_ptr<RBX::Game> game = [self setupPreloadedGameWithNonGameController:lastNonGameController needsOnline:YES];
if( game != shared_ptr<RBX::Game>())
return [self startGame:boost::bind(&joinGameWithParameters, game, params)
controller:lastNonGameController
preloadedGame:game
presentGameAutomatically:presentGameAutomatically];
return false;
}
-(BOOL) startGameWithJoinScript:(NSString*)joinScript controller:(UIViewController*)lastNonGameController presentGameAutomatically:(BOOL)presentGameAutomatically
{
shared_ptr<RBX::Game> game = [self setupPreloadedGameWithNonGameController:lastNonGameController needsOnline:YES];
if( game != shared_ptr<RBX::Game>())
return [self startGame:boost::bind(&joinGameWithJoinScript, [joinScript UTF8String], game) controller:lastNonGameController preloadedGame:game presentGameAutomatically:presentGameAutomatically];
return false;
}
-(BOOL) startGame:(boost::function0<void>)scriptFunction controller:(UIViewController*)lastNonGameController preloadedGame:(shared_ptr<RBX::Game>)preloadedGame presentGameAutomatically:(BOOL)presentGameAutomatically
{
// Start the script thread!
boost::thread(RBX::thread_wrapper(scriptFunction, "GameStartScript"));
[self createGame:preloadedGame presentGameAutomatically:presentGameAutomatically];
return YES;
}
-(BOOL) appActive
{
NSString * appState = [[NSUserDefaults standardUserDefaults] stringForKey:@"RobloxAppState"];
NSLog(@"PlacerLauncher::appActive - %@", appState);
if (appState)
{
if ([appState isEqualToString:@"inApp"] || [appState isEqualToString:@"tryForeground"])
{
return TRUE;
}
else
{
return FALSE;
}
}
else
{
return FALSE;
}
}
-(void) leaveGameShutdownWithObject:(NSObject*) object
{
if( [object isKindOfClass:[NSNumber class]] )
{
NSNumber* numberVal = (NSNumber*)object;
[self leaveGameShutdown:[numberVal boolValue]];
}
}
-(void)leaveGameShutdown:(BOOL) userRequestedLeave
{
if (!bPlaceViewPresented)
{
[self performSelectorOnMainThread:@selector(leaveGameShutdownWithObject:) withObject:[NSNumber numberWithBool:userRequestedLeave] waitUntilDone:FALSE];
return;
}
bPlaceViewPresented = false;
MainViewController* mainViewController = [MainViewController sharedInstance];
NSString* didUserRequestLeave = userRequestedLeave ? @"TRUE" : @"FALSE";
NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:didUserRequestLeave,@"UserRequestedLeave", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_GAME_START_LEAVING object:self userInfo:dict];
[[mainViewController getOgreViewController] dismissViewControllerAnimated:false completion:^(void)
{
// these will change, so we should remove them from being used
[mainViewController setOgreViewController:nil];
[mainViewController setOgreView:nil];
[mainViewController setOgreWindow:nil];
[self deleteRobloxView:YES];
hasReceivedMemoryWarning = NO;
isCurrentlyPlayingGame = NO;
[[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_GAME_DID_LEAVE object:self userInfo:dict];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"RobloxGameState"];
[[NSUserDefaults standardUserDefaults] synchronize];
isLeavingGame = false;
dispatch_async(dispatch_get_main_queue(), ^{
[[RobloxWebUtility sharedInstance] writeUpdatedSettings];
});
// end backgroundTask since we are done
AppDelegate * appDelegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
//NSLog(@"Task [%d] Time remaining: %f", appDelegate.bgTask, [[UIApplication sharedApplication] backgroundTimeRemaining]);
[[UIApplication sharedApplication] endBackgroundTask:appDelegate.bgTask];
appDelegate.bgTask = UIBackgroundTaskInvalid;
}];
}
-(void)leaveGame
{
[self leaveGame:NO];
}
-(void)leaveGame:(BOOL) userRequestedLeave
{
GameViewController *gcv = [[MainViewController sharedInstance] getOgreViewController];
NSLog(@"isCurrentlyPlayingGame: %d", isCurrentlyPlayingGame);
NSLog(@"isLeavingGame: %d", isLeavingGame);
NSLog(@"[[MainViewController sharedInstance] getOgreViewController]: %p", gcv);
if(!isCurrentlyPlayingGame || isLeavingGame || ([[MainViewController sharedInstance] getOgreViewController] == nil))
return;
isLeavingGame = true;
NSLog(@"PlaceLauncher::leaveGame");
// allow screen to dim
[[UIApplication sharedApplication] setIdleTimerDisabled:NO];
[[NSUserDefaults standardUserDefaults] setObject:@"leaveGame" forKey:@"RobloxGameState"];
[[NSUserDefaults standardUserDefaults] synchronize];
[self closeChildConnections];
[[SessionReporter sharedInstance] reportSessionFor:EXIT_GAME];
[RobloxGoogleAnalytics setPageViewTracking:@"Visit/Success/LeaveGame"];
// set up a background task to give time to finish in application has gone into background to prevent termination
AppDelegate * appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
appDelegate.bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^
{
isLeavingGame = false;
// stopped or ending the task outright.
[[UIApplication sharedApplication] endBackgroundTask:appDelegate.bgTask];
appDelegate.bgTask = UIBackgroundTaskInvalid;
}];
[[RobloxMemoryManager sharedInstance] stopMaxMemoryTracker];
dispatch_async(dispatch_get_main_queue(),^{
[self leaveGameShutdown:userRequestedLeave];
});
}
-(void)disableViewBecauseGoingToBackground
{
if (rbxView)
rbxView->requestStopRenderingForBackgroundMode();
}
-(void)enableViewBecauseGoingToForeground
{
if (rbxView)
rbxView->requestResumeRendering();
}
-(void)teleport:(NSString*)ticket withAuthentication:(NSString*)url withScript:(NSString*)script
{
if (isLeavingGame) return;
if (!rbxView) return;
//RobloxPageViewController* lastController = (RobloxPageViewController*)[[MainViewController sharedInstance] getLastNonGameController];
//RBXASSERT([lastController isKindOfClass:[RobloxPageViewController class]]);
UIViewController* lastController = [[MainViewController sharedInstance] getLastNonGameController];
[[PlaceLauncher sharedInstance] setLastNonGameController:lastController needsOnline:YES];
[self deleteRobloxView:NO];
// remove reference to game from controlView (otherwise won't shut down)
if(MainViewController* mainViewController = [MainViewController sharedInstance])
{
GameViewController* gameController = [mainViewController getOgreViewController];
for(ControlView* control in gameController.view.subviews)
{
[control setGame:boost::shared_ptr<RBX::Game>()];
break;
}
}
// first do an empty reset so serviceprovider oldProvider is called first
currentGame.reset();
currentGame.reset(new RBX::SecurePlayerGame(NULL, GetBaseURL().c_str()));
// 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, std::string([url UTF8String]), std::string([ticket UTF8String]), std::string([script UTF8String]), lastController, currentGame));
if (isLeavingGame) return;
if (!isCurrentlyPlayingGame) return;
[self finishGameSetup:currentGame gameViewController:[[MainViewController sharedInstance] getOgreViewController]];
rbxView->getDataModel()->submitTask(boost::bind(&finishTeleport, rbxView, currentGame, RBX::FunctionMarshaller::GetWindow()), RBX::DataModelJob::Write);
}
-(BOOL)isCurrentlyPlayingGame
{
return isCurrentlyPlayingGame;
}
-(void) applicationDidReceiveMemoryWarning
{
if ([self isCurrentlyPlayingGame] == YES)
{
NSLog(@"applicationDidReceiveMemoryWarning - FREE mem: %ld", (long)RBX::MemoryStats::freeMemoryBytes());
print_free_memory();
// log out of memory
bool bJoining = (childConnection.connected() || playerConnection.connected());
NSString* placeId = [NSString stringWithFormat:@"%d", lastPlaceId];
if (bJoining)
{
[RobloxGoogleAnalytics setEventTracking:@"PlayErrors" withAction:@"OutOfMemory_EarlyExit" withLabel:placeId withValue:0];
[[SessionReporter sharedInstance] reportSessionFor:OUT_MEMORY_ON_LOAD PlaceId:lastPlaceId];
}
else
{
[RobloxGoogleAnalytics setEventTracking:@"PlayErrors" withAction:@"OutOfMemory" withLabel:placeId withValue:0];
[[SessionReporter sharedInstance] reportSessionFor:OUT_MEMORY_IN_GAME PlaceId:lastPlaceId];
}
[self clearCachedContent];
}
else
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING,"PlaceLauncher: applicationDidReceiveMemoryWarning receive while out of game, ignoring");
}
-(void) clearCachedContent
{
if ([self isCurrentlyPlayingGame] == YES)
{
if (rbxView == 0)
{
return;
}
uint64_t preClearBytes = RBX::MemoryStats::usedMemoryBytes();
rbxView->getView()->garbageCollect();
uint64_t postClearBytes = RBX::MemoryStats::usedMemoryBytes();
NSLog(@"PlaceLauncher::clearCachedContent: %.02fMB", (preClearBytes - postClearBytes)/(1048576.0));
}
}
-(void)childAdded:(shared_ptr<RBX::Instance>) child
{
NSLog(@"PlaceLauncher::childAdded");
// see if we are the local player
if (rbxView == 0)
{
NSLog(@"robloxView == 0");
[self closeChildConnections];
return;
}
boost::shared_ptr<RBX::DataModel> dataModel = rbxView->getDataModel();
if (dataModel == 0)
{
NSLog(@"dataModel == 0");
[self closeChildConnections];
return;
}
RBX::Network::Players* players = dataModel->find<RBX::Network::Players>();
if (players == 0)
{
NSLog(@"players == 0");
[self closeChildConnections];
return;
}
RBX::Network::Player * player = players->getLocalPlayer();
if (player == 0)
{
NSLog(@"player == 0");
[self closeChildConnections];
return;
}
if (player == child.get())
{
// HOOK INTO OnCharacterAdded
NSLog(@"player == child");
playerConnection = player->characterAddedSignal.connect(boostFuncFromSelector_1< shared_ptr<RBX::Instance> >(@selector(playerLoaded:),self));
childConnection.disconnect();
}
else
{
NSLog(@"player not child but valid");
playerConnection = player->characterAddedSignal.connect(boostFuncFromSelector_1< shared_ptr<RBX::Instance> >(@selector(playerLoaded:),self));
childConnection.disconnect();
}
}
-(void) playerLoaded:(shared_ptr<RBX::Instance>) character
{
NSLog(@"PlaceLauncher::playerLoaded - disabling heartbeat");
playerConnection.disconnect();
[self closeChildConnections];
[[NSUserDefaults standardUserDefaults] setObject:@"inGame" forKey:@"RobloxGameState"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
-(void) closeChildConnections
{
if (childConnection.connected())
{
childConnection.disconnect();
}
if (playerConnection.connected())
{
playerConnection.disconnect();
}
[[RobloxMemoryManager sharedInstance] stopFreeMemoryChecker];
}
-(shared_ptr<RBX::Game>) getCurrentGame
{
return currentGame;
}
@end