This commit is contained in:
watrabi
2025-09-18 17:55:52 -04:00
commit 977f1ff4b8
15030 changed files with 17324420 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
//
// AppDelegate.h
// RobloxMobile
//
// Created by Ben Tkacheff on 10/9/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "AppDelegate.h"
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
#define HAS_SHOWN_KEY "HasShownBefore"
@interface AppDelegate : UIResponder <UIApplicationDelegate>
{
UIBackgroundTaskIdentifier bgTask;
BOOL leavingGame;
}
@property (strong, nonatomic) UIWindow *window;
@property (assign) UIBackgroundTaskIdentifier bgTask;
@end
+156
View File
@@ -0,0 +1,156 @@
//
// AppDelegate.m
// RobloxMobile
//
// Created by Ben Tkacheff on 10/9/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "AppDelegate.h"
#import "PlaceLauncher.h"
#import "UserInfo.h"
#import "RobloxInfo.h"
#import "RobloxMemoryManager.h"
#import "UpgradeCheckHelper.h"
#import "CrashReporter.h"
#import "SessionReporter.h"
#import "KeychainItemWrapper.h"
#import "LoginManager.h"
#import "ABTestManager.h"
#import "RobloxNotifications.h"
#include "v8datamodel/GuiBuilder.h"
#include "util/standardout.h"
#include "util/http.h"
@implementation AppDelegate
@synthesize bgTask;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
leavingGame = NO;
[LoginManager sharedInstance];
//we need to call this on start up, so save a browser tracker
//NSString* browserTracker = [RobloxData initializeBrowserTracker];
//[[NSUserDefaults standardUserDefaults] setObject:browserTracker forKey:BROWSER_TRACKER_KEY];
//[[NSUserDefaults standardUserDefaults] synchronize];
// this makes sure everyone is on the right minimum version of the app
[UpgradeCheckHelper checkForUpdate:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]];
// Register the preference defaults early.
NSString * defaultKeys[2] = { @"warnings_preference", @"wifionly_preference" };
NSNumber * defaultValues[2] = { [NSNumber numberWithBool:YES], [NSNumber numberWithBool:NO] };
NSDictionary *appDefaults = [NSDictionary dictionaryWithObjects:(id *)defaultValues forKeys:(id *)defaultKeys count:2];
[[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
[CrashReporter sharedInstance];
[[SessionReporter sharedInstance] reportSessionFor:APPLICATION_FRESH_START];
KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithIdentifier:[[[NSBundle mainBundle] bundleIdentifier] stringByAppendingString:@"RobloxLogin"] accessGroup:nil];
if (keychainItem)
{
NSString *password = [keychainItem objectForKey:(__bridge id)kSecValueData];
NSString* username = [keychainItem objectForKey:(__bridge id)kSecAttrAccount];
[UserInfo CurrentPlayer].username = username;
[UserInfo CurrentPlayer].password = password;
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gotDidLeaveGameNotification:) name:RBX_NOTIFY_GAME_DID_LEAVE object:nil ];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gotStartLeaveGameNotification:) name:RBX_NOTIFY_GAME_START_LEAVING object:nil ];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@HAS_SHOWN_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];
[[LoginManager sharedInstance] setRememberPassword:true];
[[ABTestManager sharedInstance] fetchExperimentsForBrowserTracker];
return YES;
}
-(void) gotDidLeaveGameNotification:(NSNotification *)aNotification
{
leavingGame = NO;
[[UIApplication sharedApplication] setStatusBarHidden:NO];
}
-(void) gotStartLeaveGameNotification:(NSNotification *)aNotification
{
leavingGame = YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
[[PlaceLauncher sharedInstance] disableViewBecauseGoingToBackground];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[[NSUserDefaults standardUserDefaults] setObject:@"tryBackground" forKey:@"RobloxAppState"];
[[NSUserDefaults standardUserDefaults] synchronize];
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
[[PlaceLauncher sharedInstance] leaveGame];
[[SessionReporter sharedInstance] reportSessionFor:APPLICATION_BACKGROUND];
[[LoginManager sharedInstance] processBackground];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"RobloxAppState"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"RobloxGameState"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
[[PlaceLauncher sharedInstance] clearCachedContent];
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
// this makes sure everyone is on the right minimum version of the app
[UpgradeCheckHelper checkForUpdate:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[[NSUserDefaults standardUserDefaults] setObject:@"tryForeground" forKey:@"RobloxAppState"];
[[NSUserDefaults standardUserDefaults] synchronize];
RobloxWebUtility * robloxWebUtility = [RobloxWebUtility sharedInstance];
if (![robloxWebUtility bAppSettingsInitialized])
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0), ^{
[robloxWebUtility updateAllClientSettingsWithCompletion:nil];
});
}
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
[[PlaceLauncher sharedInstance] enableViewBecauseGoingToForeground];
[[SessionReporter sharedInstance] reportSessionFor:APPLICATION_ACTIVE];
[[NSUserDefaults standardUserDefaults] setObject:@"inApp" forKey:@"RobloxAppState"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskLandscape;
}
@end
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+994
View File
@@ -0,0 +1,994 @@
//
// GCDAsyncUdpSocket
//
// This class is in the public domain.
// Originally created by Robbie Hanson of Deusty LLC.
// Updated and maintained by Deusty LLC and the Apple development community.
//
// https://github.com/robbiehanson/CocoaAsyncSocket
//
#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>
extern NSString *const GCDAsyncUdpSocketException;
extern NSString *const GCDAsyncUdpSocketErrorDomain;
extern NSString *const GCDAsyncUdpSocketQueueName;
extern NSString *const GCDAsyncUdpSocketThreadName;
enum GCDAsyncUdpSocketError
{
GCDAsyncUdpSocketNoError = 0, // Never used
GCDAsyncUdpSocketBadConfigError, // Invalid configuration
GCDAsyncUdpSocketBadParamError, // Invalid parameter was passed
GCDAsyncUdpSocketSendTimeoutError, // A send operation timed out
GCDAsyncUdpSocketClosedError, // The socket was closed
GCDAsyncUdpSocketOtherError, // Description provided in userInfo
};
typedef enum GCDAsyncUdpSocketError GCDAsyncUdpSocketError;
/**
* You may optionally set a receive filter for the socket.
* A filter can provide several useful features:
*
* 1. Many times udp packets need to be parsed.
* Since the filter can run in its own independent queue, you can parallelize this parsing quite easily.
* The end result is a parallel socket io, datagram parsing, and packet processing.
*
* 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited.
* The filter can prevent such packets from arriving at the delegate.
* And because the filter can run in its own independent queue, this doesn't slow down the delegate.
*
* - Since the udp protocol does not guarantee delivery, udp packets may be lost.
* Many protocols built atop udp thus provide various resend/re-request algorithms.
* This sometimes results in duplicate packets arriving.
* A filter may allow you to architect the duplicate detection code to run in parallel to normal processing.
*
* - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive.
* Such packets need to be ignored.
*
* 3. Sometimes traffic shapers are needed to simulate real world environments.
* A filter allows you to write custom code to simulate such environments.
* The ability to code this yourself is especially helpful when your simulated environment
* is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router),
* or the system tools to handle this aren't available (e.g. on a mobile device).
*
* @param data - The packet that was received.
* @param address - The address the data was received from.
* See utilities section for methods to extract info from address.
* @param context - Out parameter you may optionally set, which will then be passed to the delegate method.
* For example, filter block can parse the data and then,
* pass the parsed data to the delegate.
*
* @returns - YES if the received packet should be passed onto the delegate.
* NO if the received packet should be discarded, and not reported to the delegete.
*
* Example:
*
* GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) {
*
* MyProtocolMessage *msg = [MyProtocol parseMessage:data];
*
* *context = response;
* return (response != nil);
* };
* [udpSocket setReceiveFilter:filter withQueue:myParsingQueue];
*
**/
typedef BOOL (^GCDAsyncUdpSocketReceiveFilterBlock)(NSData *data, NSData *address, id *context);
/**
* You may optionally set a send filter for the socket.
* A filter can provide several interesting possibilities:
*
* 1. Optional caching of resolved addresses for domain names.
* The cache could later be consulted, resulting in fewer system calls to getaddrinfo.
*
* 2. Reusable modules of code for bandwidth monitoring.
*
* 3. Sometimes traffic shapers are needed to simulate real world environments.
* A filter allows you to write custom code to simulate such environments.
* The ability to code this yourself is especially helpful when your simulated environment
* is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router),
* or the system tools to handle this aren't available (e.g. on a mobile device).
*
* @param data - The packet that was received.
* @param address - The address the data was received from.
* See utilities section for methods to extract info from address.
* @param tag - The tag that was passed in the send method.
*
* @returns - YES if the packet should actually be sent over the socket.
* NO if the packet should be silently dropped (not sent over the socket).
*
* Regardless of the return value, the delegate will be informed that the packet was successfully sent.
*
**/
typedef BOOL (^GCDAsyncUdpSocketSendFilterBlock)(NSData *data, NSData *address, long tag);
@interface GCDAsyncUdpSocket : NSObject
/**
* GCDAsyncUdpSocket uses the standard delegate paradigm,
* but executes all delegate callbacks on a given delegate dispatch queue.
* This allows for maximum concurrency, while at the same time providing easy thread safety.
*
* You MUST set a delegate AND delegate dispatch queue before attempting to
* use the socket, or you will get an error.
*
* The socket queue is optional.
* If you pass NULL, GCDAsyncSocket will automatically create its own socket queue.
* If you choose to provide a socket queue, the socket queue must not be a concurrent queue,
* then please see the discussion for the method markSocketQueueTargetQueue.
*
* The delegate queue and socket queue can optionally be the same.
**/
- (id)init;
- (id)initWithSocketQueue:(dispatch_queue_t)sq;
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq;
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq;
#pragma mark Configuration
- (id)delegate;
- (void)setDelegate:(id)delegate;
- (void)synchronouslySetDelegate:(id)delegate;
- (dispatch_queue_t)delegateQueue;
- (void)setDelegateQueue:(dispatch_queue_t)delegateQueue;
- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue;
- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr;
- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
- (void)synchronouslySetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
/**
* By default, both IPv4 and IPv6 are enabled.
*
* This means GCDAsyncUdpSocket automatically supports both protocols,
* and can send to IPv4 or IPv6 addresses,
* as well as receive over IPv4 and IPv6.
*
* For operations that require DNS resolution, GCDAsyncUdpSocket supports both IPv4 and IPv6.
* If a DNS lookup returns only IPv4 results, GCDAsyncUdpSocket will automatically use IPv4.
* If a DNS lookup returns only IPv6 results, GCDAsyncUdpSocket will automatically use IPv6.
* If a DNS lookup returns both IPv4 and IPv6 results, then the protocol used depends on the configured preference.
* If IPv4 is preferred, then IPv4 is used.
* If IPv6 is preferred, then IPv6 is used.
* If neutral, then the first IP version in the resolved array will be used.
*
* Starting with Mac OS X 10.7 Lion and iOS 5, the default IP preference is neutral.
* On prior systems the default IP preference is IPv4.
**/
- (BOOL)isIPv4Enabled;
- (void)setIPv4Enabled:(BOOL)flag;
- (BOOL)isIPv6Enabled;
- (void)setIPv6Enabled:(BOOL)flag;
- (BOOL)isIPv4Preferred;
- (BOOL)isIPv6Preferred;
- (BOOL)isIPVersionNeutral;
- (void)setPreferIPv4;
- (void)setPreferIPv6;
- (void)setIPVersionNeutral;
/**
* Gets/Sets the maximum size of the buffer that will be allocated for receive operations.
* The default maximum size is 9216 bytes.
*
* The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535.
* The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295.
*
* Since the OS/GCD notifies us of the size of each received UDP packet,
* the actual allocated buffer size for each packet is exact.
* And in practice the size of UDP packets is generally much smaller than the max.
* Indeed most protocols will send and receive packets of only a few bytes,
* or will set a limit on the size of packets to prevent fragmentation in the IP layer.
*
* If you set the buffer size too small, the sockets API in the OS will silently discard
* any extra data, and you will not be notified of the error.
**/
- (uint16_t)maxReceiveIPv4BufferSize;
- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max;
- (uint32_t)maxReceiveIPv6BufferSize;
- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max;
/**
* User data allows you to associate arbitrary information with the socket.
* This data is not used internally in any way.
**/
- (id)userData;
- (void)setUserData:(id)arbitraryUserData;
#pragma mark Diagnostics
/**
* Returns the local address info for the socket.
*
* The localAddress method returns a sockaddr structure wrapped in a NSData object.
* The localHost method returns the human readable IP address as a string.
*
* Note: Address info may not be available until after the socket has been binded, connected
* or until after data has been sent.
**/
- (NSData *)localAddress;
- (NSString *)localHost;
- (uint16_t)localPort;
- (NSData *)localAddress_IPv4;
- (NSString *)localHost_IPv4;
- (uint16_t)localPort_IPv4;
- (NSData *)localAddress_IPv6;
- (NSString *)localHost_IPv6;
- (uint16_t)localPort_IPv6;
/**
* Returns the remote address info for the socket.
*
* The connectedAddress method returns a sockaddr structure wrapped in a NSData object.
* The connectedHost method returns the human readable IP address as a string.
*
* Note: Since UDP is connectionless by design, connected address info
* will not be available unless the socket is explicitly connected to a remote host/port.
* If the socket is not connected, these methods will return nil / 0.
**/
- (NSData *)connectedAddress;
- (NSString *)connectedHost;
- (uint16_t)connectedPort;
/**
* Returns whether or not this socket has been connected to a single host.
* By design, UDP is a connectionless protocol, and connecting is not needed.
* If connected, the socket will only be able to send/receive data to/from the connected host.
**/
- (BOOL)isConnected;
/**
* Returns whether or not this socket has been closed.
* The only way a socket can be closed is if you explicitly call one of the close methods.
**/
- (BOOL)isClosed;
/**
* Returns whether or not this socket is IPv4.
*
* By default this will be true, unless:
* - IPv4 is disabled (via setIPv4Enabled:)
* - The socket is explicitly bound to an IPv6 address
* - The socket is connected to an IPv6 address
**/
- (BOOL)isIPv4;
/**
* Returns whether or not this socket is IPv6.
*
* By default this will be true, unless:
* - IPv6 is disabled (via setIPv6Enabled:)
* - The socket is explicitly bound to an IPv4 address
* _ The socket is connected to an IPv4 address
*
* This method will also return false on platforms that do not support IPv6.
* Note: The iPhone does not currently support IPv6.
**/
- (BOOL)isIPv6;
#pragma mark Binding
/**
* Binds the UDP socket to the given port.
* Binding should be done for server sockets that receive data prior to sending it.
* Client sockets can skip binding,
* as the OS will automatically assign the socket an available port when it starts sending data.
*
* You may optionally pass a port number of zero to immediately bind the socket,
* yet still allow the OS to automatically assign an available port.
*
* You cannot bind a socket after its been connected.
* You can only bind a socket once.
* You can still connect a socket (if desired) after binding.
*
* On success, returns YES.
* Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr.
**/
- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr;
/**
* Binds the UDP socket to the given port and optional interface.
* Binding should be done for server sockets that receive data prior to sending it.
* Client sockets can skip binding,
* as the OS will automatically assign the socket an available port when it starts sending data.
*
* You may optionally pass a port number of zero to immediately bind the socket,
* yet still allow the OS to automatically assign an available port.
*
* The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35").
* You may also use the special strings "localhost" or "loopback" to specify that
* the socket only accept packets from the local machine.
*
* You cannot bind a socket after its been connected.
* You can only bind a socket once.
* You can still connect a socket (if desired) after binding.
*
* On success, returns YES.
* Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr.
**/
- (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError **)errPtr;
/**
* Binds the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object.
*
* If you have an existing struct sockaddr you can convert it to a NSData object like so:
* struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len];
* struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len];
*
* Binding should be done for server sockets that receive data prior to sending it.
* Client sockets can skip binding,
* as the OS will automatically assign the socket an available port when it starts sending data.
*
* You cannot bind a socket after its been connected.
* You can only bind a socket once.
* You can still connect a socket (if desired) after binding.
*
* On success, returns YES.
* Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr.
**/
- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr;
#pragma mark Connecting
/**
* Connects the UDP socket to the given host and port.
* By design, UDP is a connectionless protocol, and connecting is not needed.
*
* Choosing to connect to a specific host/port has the following effect:
* - You will only be able to send data to the connected host/port.
* - You will only be able to receive data from the connected host/port.
* - You will receive ICMP messages that come from the connected host/port, such as "connection refused".
*
* The actual process of connecting a UDP socket does not result in any communication on the socket.
* It simply changes the internal state of the socket.
*
* You cannot bind a socket after it has been connected.
* You can only connect a socket once.
*
* The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2").
*
* This method is asynchronous as it requires a DNS lookup to resolve the given host name.
* If an obvious error is detected, this method immediately returns NO and sets errPtr.
* If you don't care about the error, you can pass nil for errPtr.
* Otherwise, this method returns YES and begins the asynchronous connection process.
* The result of the asynchronous connection process will be reported via the delegate methods.
**/
- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr;
/**
* Connects the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object.
*
* If you have an existing struct sockaddr you can convert it to a NSData object like so:
* struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len];
* struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len];
*
* By design, UDP is a connectionless protocol, and connecting is not needed.
*
* Choosing to connect to a specific address has the following effect:
* - You will only be able to send data to the connected address.
* - You will only be able to receive data from the connected address.
* - You will receive ICMP messages that come from the connected address, such as "connection refused".
*
* Connecting a UDP socket does not result in any communication on the socket.
* It simply changes the internal state of the socket.
*
* You cannot bind a socket after its been connected.
* You can only connect a socket once.
*
* On success, returns YES.
* Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr.
*
* Note: Unlike the connectToHost:onPort:error: method, this method does not require a DNS lookup.
* Thus when this method returns, the connection has either failed or fully completed.
* In other words, this method is synchronous, unlike the asynchronous connectToHost::: method.
* However, for compatibility and simplification of delegate code, if this method returns YES
* then the corresponding delegate method (udpSocket:didConnectToHost:port:) is still invoked.
**/
- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr;
#pragma mark Multicast
/**
* Join multicast group.
* Group should be an IP address (eg @"225.228.0.1").
*
* On success, returns YES.
* Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr.
**/
- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr;
/**
* Join multicast group.
* Group should be an IP address (eg @"225.228.0.1").
* The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35").
*
* On success, returns YES.
* Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr.
**/
- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr;
- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr;
- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr;
#pragma mark Broadcast
/**
* By default, the underlying socket in the OS will not allow you to send broadcast messages.
* In order to send broadcast messages, you need to enable this functionality in the socket.
*
* A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is
* delivered to every host on the network.
* The reason this is generally disabled by default (by the OS) is to prevent
* accidental broadcast messages from flooding the network.
**/
- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr;
#pragma mark Sending
/**
* Asynchronously sends the given data, with the given timeout and tag.
*
* This method may only be used with a connected socket.
* Recall that connecting is optional for a UDP socket.
* For connected sockets, data can only be sent to the connected address.
* For non-connected sockets, the remote destination is specified for each packet.
* For more information about optionally connecting udp sockets, see the documentation for the connect methods above.
*
* @param data
* The data to send.
* If data is nil or zero-length, this method does nothing.
* If passing NSMutableData, please read the thread-safety notice below.
*
* @param timeout
* The timeout for the send opeartion.
* If the timeout value is negative, the send operation will not use a timeout.
*
* @param tag
* The tag is for your convenience.
* It is not sent or received over the socket in any manner what-so-ever.
* It is reported back as a parameter in the udpSocket:didSendDataWithTag:
* or udpSocket:didNotSendDataWithTag:dueToError: methods.
* You can use it as an array index, state id, type constant, etc.
*
*
* Thread-Safety Note:
* If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while
* the socket is sending it. In other words, it's not safe to alter the data until after the delegate method
* udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying
* that this particular send operation has completed.
* This is due to the fact that GCDAsyncUdpSocket does NOT copy the data.
* It simply retains it for performance reasons.
* Often times, if NSMutableData is passed, it is because a request/response was built up in memory.
* Copying this data adds an unwanted/unneeded overhead.
* If you need to write data from an immutable buffer, and you need to alter the buffer before the socket
* completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time
* when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method.
**/
- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
/**
* Asynchronously sends the given data, with the given timeout and tag, to the given host and port.
*
* This method cannot be used with a connected socket.
* Recall that connecting is optional for a UDP socket.
* For connected sockets, data can only be sent to the connected address.
* For non-connected sockets, the remote destination is specified for each packet.
* For more information about optionally connecting udp sockets, see the documentation for the connect methods above.
*
* @param data
* The data to send.
* If data is nil or zero-length, this method does nothing.
* If passing NSMutableData, please read the thread-safety notice below.
*
* @param host
* The destination to send the udp packet to.
* May be specified as a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2").
* You may also use the convenience strings of "loopback" or "localhost".
*
* @param port
* The port of the host to send to.
*
* @param timeout
* The timeout for the send opeartion.
* If the timeout value is negative, the send operation will not use a timeout.
*
* @param tag
* The tag is for your convenience.
* It is not sent or received over the socket in any manner what-so-ever.
* It is reported back as a parameter in the udpSocket:didSendDataWithTag:
* or udpSocket:didNotSendDataWithTag:dueToError: methods.
* You can use it as an array index, state id, type constant, etc.
*
*
* Thread-Safety Note:
* If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while
* the socket is sending it. In other words, it's not safe to alter the data until after the delegate method
* udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying
* that this particular send operation has completed.
* This is due to the fact that GCDAsyncUdpSocket does NOT copy the data.
* It simply retains it for performance reasons.
* Often times, if NSMutableData is passed, it is because a request/response was built up in memory.
* Copying this data adds an unwanted/unneeded overhead.
* If you need to write data from an immutable buffer, and you need to alter the buffer before the socket
* completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time
* when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method.
**/
- (void)sendData:(NSData *)data
toHost:(NSString *)host
port:(uint16_t)port
withTimeout:(NSTimeInterval)timeout
tag:(long)tag;
/**
* Asynchronously sends the given data, with the given timeout and tag, to the given address.
*
* This method cannot be used with a connected socket.
* Recall that connecting is optional for a UDP socket.
* For connected sockets, data can only be sent to the connected address.
* For non-connected sockets, the remote destination is specified for each packet.
* For more information about optionally connecting udp sockets, see the documentation for the connect methods above.
*
* @param data
* The data to send.
* If data is nil or zero-length, this method does nothing.
* If passing NSMutableData, please read the thread-safety notice below.
*
* @param remoteAddr
* The address to send the data to (specified as a sockaddr structure wrapped in a NSData object).
*
* @param timeout
* The timeout for the send opeartion.
* If the timeout value is negative, the send operation will not use a timeout.
*
* @param tag
* The tag is for your convenience.
* It is not sent or received over the socket in any manner what-so-ever.
* It is reported back as a parameter in the udpSocket:didSendDataWithTag:
* or udpSocket:didNotSendDataWithTag:dueToError: methods.
* You can use it as an array index, state id, type constant, etc.
*
*
* Thread-Safety Note:
* If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while
* the socket is sending it. In other words, it's not safe to alter the data until after the delegate method
* udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying
* that this particular send operation has completed.
* This is due to the fact that GCDAsyncUdpSocket does NOT copy the data.
* It simply retains it for performance reasons.
* Often times, if NSMutableData is passed, it is because a request/response was built up in memory.
* Copying this data adds an unwanted/unneeded overhead.
* If you need to write data from an immutable buffer, and you need to alter the buffer before the socket
* completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time
* when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method.
**/
- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag;
/**
* You may optionally set a send filter for the socket.
* A filter can provide several interesting possibilities:
*
* 1. Optional caching of resolved addresses for domain names.
* The cache could later be consulted, resulting in fewer system calls to getaddrinfo.
*
* 2. Reusable modules of code for bandwidth monitoring.
*
* 3. Sometimes traffic shapers are needed to simulate real world environments.
* A filter allows you to write custom code to simulate such environments.
* The ability to code this yourself is especially helpful when your simulated environment
* is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router),
* or the system tools to handle this aren't available (e.g. on a mobile device).
*
* For more information about GCDAsyncUdpSocketSendFilterBlock, see the documentation for its typedef.
* To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue.
*
* Note: This method invokes setSendFilter:withQueue:isAsynchronous: (documented below),
* passing YES for the isAsynchronous parameter.
**/
- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue;
/**
* The receive filter can be run via dispatch_async or dispatch_sync.
* Most typical situations call for asynchronous operation.
*
* However, there are a few situations in which synchronous operation is preferred.
* Such is the case when the filter is extremely minimal and fast.
* This is because dispatch_sync is faster than dispatch_async.
*
* If you choose synchronous operation, be aware of possible deadlock conditions.
* Since the socket queue is executing your block via dispatch_sync,
* then you cannot perform any tasks which may invoke dispatch_sync on the socket queue.
* For example, you can't query properties on the socket.
**/
- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock
withQueue:(dispatch_queue_t)filterQueue
isAsynchronous:(BOOL)isAsynchronous;
#pragma mark Receiving
/**
* There are two modes of operation for receiving packets: one-at-a-time & continuous.
*
* In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet.
* Receiving packets one-at-a-time may be better suited for implementing certain state machine code,
* where your state machine may not always be ready to process incoming packets.
*
* In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received.
* Receiving packets continuously is better suited to real-time streaming applications.
*
* You may switch back and forth between one-at-a-time mode and continuous mode.
* If the socket is currently in continuous mode, calling this method will switch it to one-at-a-time mode.
*
* When a packet is received (and not filtered by the optional receive filter),
* the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked.
*
* If the socket is able to begin receiving packets, this method returns YES.
* Otherwise it returns NO, and sets the errPtr with appropriate error information.
*
* An example error:
* You created a udp socket to act as a server, and immediately called receive.
* You forgot to first bind the socket to a port number, and received a error with a message like:
* "Must bind socket before you can receive data."
**/
- (BOOL)receiveOnce:(NSError **)errPtr;
/**
* There are two modes of operation for receiving packets: one-at-a-time & continuous.
*
* In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet.
* Receiving packets one-at-a-time may be better suited for implementing certain state machine code,
* where your state machine may not always be ready to process incoming packets.
*
* In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received.
* Receiving packets continuously is better suited to real-time streaming applications.
*
* You may switch back and forth between one-at-a-time mode and continuous mode.
* If the socket is currently in one-at-a-time mode, calling this method will switch it to continuous mode.
*
* For every received packet (not filtered by the optional receive filter),
* the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked.
*
* If the socket is able to begin receiving packets, this method returns YES.
* Otherwise it returns NO, and sets the errPtr with appropriate error information.
*
* An example error:
* You created a udp socket to act as a server, and immediately called receive.
* You forgot to first bind the socket to a port number, and received a error with a message like:
* "Must bind socket before you can receive data."
**/
- (BOOL)beginReceiving:(NSError **)errPtr;
/**
* If the socket is currently receiving (beginReceiving has been called), this method pauses the receiving.
* That is, it won't read any more packets from the underlying OS socket until beginReceiving is called again.
*
* Important Note:
* GCDAsyncUdpSocket may be running in parallel with your code.
* That is, your delegate is likely running on a separate thread/dispatch_queue.
* When you invoke this method, GCDAsyncUdpSocket may have already dispatched delegate methods to be invoked.
* Thus, if those delegate methods have already been dispatch_async'd,
* your didReceive delegate method may still be invoked after this method has been called.
* You should be aware of this, and program defensively.
**/
- (void)pauseReceiving;
/**
* You may optionally set a receive filter for the socket.
* This receive filter may be set to run in its own queue (independent of delegate queue).
*
* A filter can provide several useful features.
*
* 1. Many times udp packets need to be parsed.
* Since the filter can run in its own independent queue, you can parallelize this parsing quite easily.
* The end result is a parallel socket io, datagram parsing, and packet processing.
*
* 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited.
* The filter can prevent such packets from arriving at the delegate.
* And because the filter can run in its own independent queue, this doesn't slow down the delegate.
*
* - Since the udp protocol does not guarantee delivery, udp packets may be lost.
* Many protocols built atop udp thus provide various resend/re-request algorithms.
* This sometimes results in duplicate packets arriving.
* A filter may allow you to architect the duplicate detection code to run in parallel to normal processing.
*
* - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive.
* Such packets need to be ignored.
*
* 3. Sometimes traffic shapers are needed to simulate real world environments.
* A filter allows you to write custom code to simulate such environments.
* The ability to code this yourself is especially helpful when your simulated environment
* is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router),
* or the system tools to handle this aren't available (e.g. on a mobile device).
*
* Example:
*
* GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) {
*
* MyProtocolMessage *msg = [MyProtocol parseMessage:data];
*
* *context = response;
* return (response != nil);
* };
* [udpSocket setReceiveFilter:filter withQueue:myParsingQueue];
*
* For more information about GCDAsyncUdpSocketReceiveFilterBlock, see the documentation for its typedef.
* To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue.
*
* Note: This method invokes setReceiveFilter:withQueue:isAsynchronous: (documented below),
* passing YES for the isAsynchronous parameter.
**/
- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue;
/**
* The receive filter can be run via dispatch_async or dispatch_sync.
* Most typical situations call for asynchronous operation.
*
* However, there are a few situations in which synchronous operation is preferred.
* Such is the case when the filter is extremely minimal and fast.
* This is because dispatch_sync is faster than dispatch_async.
*
* If you choose synchronous operation, be aware of possible deadlock conditions.
* Since the socket queue is executing your block via dispatch_sync,
* then you cannot perform any tasks which may invoke dispatch_sync on the socket queue.
* For example, you can't query properties on the socket.
**/
- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock
withQueue:(dispatch_queue_t)filterQueue
isAsynchronous:(BOOL)isAsynchronous;
#pragma mark Closing
/**
* Immediately closes the underlying socket.
* Any pending send operations are discarded.
*
* The GCDAsyncUdpSocket instance may optionally be used again.
* (it will setup/configure/use another unnderlying BSD socket).
**/
- (void)close;
/**
* Closes the underlying socket after all pending send operations have been sent.
*
* The GCDAsyncUdpSocket instance may optionally be used again.
* (it will setup/configure/use another unnderlying BSD socket).
**/
- (void)closeAfterSending;
#pragma mark Advanced
/**
* GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue.
* In most cases, the instance creates this queue itself.
* However, to allow for maximum flexibility, the internal queue may be passed in the init method.
* This allows for some advanced options such as controlling socket priority via target queues.
* However, when one begins to use target queues like this, they open the door to some specific deadlock issues.
*
* For example, imagine there are 2 queues:
* dispatch_queue_t socketQueue;
* dispatch_queue_t socketTargetQueue;
*
* If you do this (pseudo-code):
* socketQueue.targetQueue = socketTargetQueue;
*
* Then all socketQueue operations will actually get run on the given socketTargetQueue.
* This is fine and works great in most situations.
* But if you run code directly from within the socketTargetQueue that accesses the socket,
* you could potentially get deadlock. Imagine the following code:
*
* - (BOOL)socketHasSomething
* {
* __block BOOL result = NO;
* dispatch_block_t block = ^{
* result = [self someInternalMethodToBeRunOnlyOnSocketQueue];
* }
* if (is_executing_on_queue(socketQueue))
* block();
* else
* dispatch_sync(socketQueue, block);
*
* return result;
* }
*
* What happens if you call this method from the socketTargetQueue? The result is deadlock.
* This is because the GCD API offers no mechanism to discover a queue's targetQueue.
* Thus we have no idea if our socketQueue is configured with a targetQueue.
* If we had this information, we could easily avoid deadlock.
* But, since these API's are missing or unfeasible, you'll have to explicitly set it.
*
* IF you pass a socketQueue via the init method,
* AND you've configured the passed socketQueue with a targetQueue,
* THEN you should pass the end queue in the target hierarchy.
*
* For example, consider the following queue hierarchy:
* socketQueue -> ipQueue -> moduleQueue
*
* This example demonstrates priority shaping within some server.
* All incoming client connections from the same IP address are executed on the same target queue.
* And all connections for a particular module are executed on the same target queue.
* Thus, the priority of all networking for the entire module can be changed on the fly.
* Additionally, networking traffic from a single IP cannot monopolize the module.
*
* Here's how you would accomplish something like that:
* - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock
* {
* dispatch_queue_t socketQueue = dispatch_queue_create("", NULL);
* dispatch_queue_t ipQueue = [self ipQueueForAddress:address];
*
* dispatch_set_target_queue(socketQueue, ipQueue);
* dispatch_set_target_queue(iqQueue, moduleQueue);
*
* return socketQueue;
* }
* - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
* {
* [clientConnections addObject:newSocket];
* [newSocket markSocketQueueTargetQueue:moduleQueue];
* }
*
* Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue.
* This is often NOT the case, as such queues are used solely for execution shaping.
**/
- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue;
- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue;
/**
* It's not thread-safe to access certain variables from outside the socket's internal queue.
*
* For example, the socket file descriptor.
* File descriptors are simply integers which reference an index in the per-process file table.
* However, when one requests a new file descriptor (by opening a file or socket),
* the file descriptor returned is guaranteed to be the lowest numbered unused descriptor.
* So if we're not careful, the following could be possible:
*
* - Thread A invokes a method which returns the socket's file descriptor.
* - The socket is closed via the socket's internal queue on thread B.
* - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD.
* - Thread A is now accessing/altering the file instead of the socket.
*
* In addition to this, other variables are not actually objects,
* and thus cannot be retained/released or even autoreleased.
* An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct.
*
* Although there are internal variables that make it difficult to maintain thread-safety,
* it is important to provide access to these variables
* to ensure this class can be used in a wide array of environments.
* This method helps to accomplish this by invoking the current block on the socket's internal queue.
* The methods below can be invoked from within the block to access
* those generally thread-unsafe internal variables in a thread-safe manner.
* The given block will be invoked synchronously on the socket's internal queue.
*
* If you save references to any protected variables and use them outside the block, you do so at your own peril.
**/
- (void)performBlock:(dispatch_block_t)block;
/**
* These methods are only available from within the context of a performBlock: invocation.
* See the documentation for the performBlock: method above.
*
* Provides access to the socket's file descriptor(s).
* If the socket isn't connected, or explicity bound to a particular interface,
* it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6.
**/
- (int)socketFD;
- (int)socket4FD;
- (int)socket6FD;
#if TARGET_OS_IPHONE
/**
* These methods are only available from within the context of a performBlock: invocation.
* See the documentation for the performBlock: method above.
*
* Returns (creating if necessary) a CFReadStream/CFWriteStream for the internal socket.
*
* Generally GCDAsyncUdpSocket doesn't use CFStream. (It uses the faster GCD API's.)
* However, if you need one for any reason,
* these methods are a convenient way to get access to a safe instance of one.
**/
- (CFReadStreamRef)readStream;
- (CFWriteStreamRef)writeStream;
/**
* This method is only available from within the context of a performBlock: invocation.
* See the documentation for the performBlock: method above.
*
* Configures the socket to allow it to operate when the iOS application has been backgrounded.
* In other words, this method creates a read & write stream, and invokes:
*
* CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
* CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
*
* Returns YES if successful, NO otherwise.
*
* Example usage:
*
* [asyncUdpSocket performBlock:^{
* [asyncUdpSocket enableBackgroundingOnSocket];
* }];
*
*
* NOTE : Apple doesn't currently support backgrounding UDP sockets. (Only TCP for now).
**/
//- (BOOL)enableBackgroundingOnSockets;
#endif
#pragma mark Utilities
/**
* Extracting host/port/family information from raw address data.
**/
+ (NSString *)hostFromAddress:(NSData *)address;
+ (uint16_t)portFromAddress:(NSData *)address;
+ (int)familyFromAddress:(NSData *)address;
+ (BOOL)isIPv4Address:(NSData *)address;
+ (BOOL)isIPv6Address:(NSData *)address;
+ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address;
+ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPtr fromAddress:(NSData *)address;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@protocol GCDAsyncUdpSocketDelegate
@optional
/**
* By design, UDP is a connectionless protocol, and connecting is not needed.
* However, you may optionally choose to connect to a particular host for reasons
* outlined in the documentation for the various connect methods listed above.
*
* This method is called if one of the connect methods are invoked, and the connection is successful.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address;
/**
* By design, UDP is a connectionless protocol, and connecting is not needed.
* However, you may optionally choose to connect to a particular host for reasons
* outlined in the documentation for the various connect methods listed above.
*
* This method is called if one of the connect methods are invoked, and the connection fails.
* This may happen, for example, if a domain name is given for the host and the domain name is unable to be resolved.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError *)error;
/**
* Called when the datagram with the given tag has been sent.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag;
/**
* Called if an error occurs while trying to send a datagram.
* This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError *)error;
/**
* Called when the socket has received the requested datagram.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
fromAddress:(NSData *)address
withFilterContext:(id)filterContext;
/**
* Called when the socket is closed.
**/
- (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError *)error;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
//
// PairTutorialDataController.h
// RobloxMobile
//
// Created by Ben Tkacheff on 8/15/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "PairTutorialViewController.h"
@interface PairTutorialDataController : UIViewController <UIPageViewControllerDataSource>
@property (strong, nonatomic) UIPageViewController *pageViewController;
@property (strong, nonatomic) NSArray *pageTitles;
@property (strong, nonatomic) NSArray *pageInstructions;
@property (strong, nonatomic) NSArray *pageImages;
@end
@@ -0,0 +1,112 @@
//
// PairTutorialDataController.m
// RobloxMobile
//
// Created by Ben Tkacheff on 8/15/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "PairTutorialDataController.h"
#import "RobloxGoogleAnalytics.h"
@interface PairTutorialDataController ()
@end
@implementation PairTutorialDataController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.pageTitles = @[NSLocalizedString(@"PairHelpStep1TitleString",nil), NSLocalizedString(@"PairHelpStep2TitleString",nil), NSLocalizedString(@"PairHelpStep3TitleString",nil)];
self.pageInstructions = @[NSLocalizedString(@"PairHelpStep1String", nil), NSLocalizedString(@"PairHelpStep2String", nil), NSLocalizedString(@"PairHelpStep3String", nil)];
self.pageImages = @[@"step1.png", @"step2.png", @"step3.png"];
// Create page view controller
self.pageViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"PageViewController"];
self.pageViewController.dataSource = self;
PairTutorialViewController *startingViewController = [self viewControllerAtIndex:0];
NSArray *viewControllers = @[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
// Change the size of page view controller
self.pageViewController.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - 30);
[self addChildViewController:_pageViewController];
[self.view addSubview:_pageViewController.view];
[self.pageViewController didMoveToParentViewController:self];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (PairTutorialViewController *)viewControllerAtIndex:(NSUInteger)index
{
if (([self.pageTitles count] == 0) || (index >= [self.pageTitles count])) {
return nil;
}
// Create a new view controller and pass suitable data.
PairTutorialViewController *pairTutorialViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"PairTutorialViewController"];
pairTutorialViewController.imageFile = self.pageImages[index];
pairTutorialViewController.instructionText = self.pageInstructions[index];
pairTutorialViewController.titleText = self.pageTitles[index];
pairTutorialViewController.pageIndex = index;
[RobloxGoogleAnalytics setCustomVariableWithLabel:@"PairTutorialPageView" withValue:[NSString stringWithFormat: @"%d", (int)index]];
return pairTutorialViewController;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
NSUInteger index = ((PairTutorialViewController*) viewController).pageIndex;
if ((index == 0) || (index == NSNotFound)) {
return nil;
}
index--;
return [self viewControllerAtIndex:index];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
NSUInteger index = ((PairTutorialViewController*) viewController).pageIndex;
if (index == NSNotFound) {
return nil;
}
index++;
if (index == [self.pageTitles count]) {
return nil;
}
return [self viewControllerAtIndex:index];
}
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
{
return [self.pageTitles count];
}
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
return 0;
}
@end
@@ -0,0 +1,25 @@
//
// PairTutorialViewController.h
// RobloxMobile
//
// Created by Ben Tkacheff on 8/15/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface PairTutorialViewController : UIViewController
{
}
@property NSUInteger pageIndex;
@property (retain) NSString *titleText;
@property (retain) NSString *instructionText;
@property (retain) NSString *imageFile;
@property (retain, nonatomic) IBOutlet UILabel *instructionTitle;
@property (retain, nonatomic) IBOutlet UILabel *instructionLabel;
@property (retain, nonatomic) IBOutlet UIImageView *instructionImageView;
@end
@@ -0,0 +1,70 @@
//
// PairTutorialViewController.m
// RobloxMobile
//
// Created by Ben Tkacheff on 8/15/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "PairTutorialViewController.h"
#import "UIStyleConverter.h"
#import "RobloxGoogleAnalytics.h"
@interface PairTutorialViewController ()
@end
@implementation PairTutorialViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.instructionImageView.image = [UIImage imageNamed:self.imageFile];
self.instructionLabel.text = self.instructionText;
self.instructionTitle.text = self.titleText;
[UIStyleConverter convertToLabelStyle:self.instructionLabel];
[UIStyleConverter convertToTitleStyle:self.instructionTitle];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[UIStyleConverter convertToNavigationBarStyle];
}
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[RobloxGoogleAnalytics setPageViewTracking:@"PairTutorial"];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
@@ -0,0 +1,19 @@
//
// ResetPasswordViewController.h
// RobloxMobile
//
// Created by Ben Tkacheff on 8/18/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface ResetPasswordViewController : UIViewController <UIWebViewDelegate>
{
NSURL* forgotPwURL;
}
@property (retain, nonatomic) IBOutlet UIWebView *webView;
@property (retain, nonatomic) IBOutlet UIBarButtonItem *loadingBarItem;
@end
@@ -0,0 +1,93 @@
//
// ResetPasswordViewController.m
// RobloxMobile
//
// Created by Ben Tkacheff on 8/18/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "ResetPasswordViewController.h"
#import "RobloxInfo.h"
#import "UIStyleConverter.h"
#define PASSWORD_RESET_URL @"Login/ResetPasswordRequest.aspx"
@interface ResetPasswordViewController ()
@end
@implementation ResetPasswordViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
UIImage *loadingImage = [UIImage animatedImageNamed:@"loading-" duration:0.6f];
UIButton *loadingButton = [UIButton buttonWithType:UIButtonTypeCustom];
loadingButton.bounds = CGRectMake( 0, 0, 20.0, 20.0);
[loadingButton setImage:loadingImage forState:UIControlStateNormal];
[self.loadingBarItem setCustomView:loadingButton];
NSString* baseURL = [RobloxInfo getBaseUrl];
baseURL = [baseURL stringByAppendingString:PASSWORD_RESET_URL];
forgotPwURL = [NSURL URLWithString:baseURL];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[UIStyleConverter convertToBlueNavigationBarStyle: self.navigationController.navigationBar];
}
}
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.webView loadRequest:[NSURLRequest requestWithURL:forgotPwURL]];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
// UIWebview Delegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.loadingBarItem.customView setHidden:YES];
});
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.loadingBarItem.customView setHidden:NO];
});
return YES;
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIRequiresFullScreen</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDisplayName</key>
<string>Developer</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIcons</key>
<dict/>
<key>CFBundleIcons~ipad</key>
<dict/>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Roblox Developer</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CrashlyticsKey</key>
<string>7991889011d53be5ccf9f6f42d9a1ea05a702eb7</string>
<key>Fonts provided by Application</key>
<string></string>
<key>GoogleAnalyticsAccount</key>
<string>UA-54047300-1</string>
<key>GoogleAnalyticsSampleRate</key>
<integer>100</integer>
<key>GuiBuilderDisplayStatsIndex</key>
<integer>0</integer>
<key>RbxBaseMobileUrl</key>
<string>http://www.watrbx.wtf/</string>
<key>RbxBaseUrl</key>
<string>http://www.watrbx.wtf/</string>
<key>RbxPairCode</key>
<string></string>
<key>RbxUserAgent</key>
<string>RbxRobloxDeveloper</string>
<key>UIAppFonts</key>
<array>
<string>SourceSansPro-Regular.ttf</string>
<string>SourceSansPro-Semibold.ttf</string>
<string>SourceSansPro-Light.ttf</string>
</array>
<key>UIApplicationExitsOnSuspend</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UIMainStoryboardFile</key>
<string>RobloxDeveloperiPhone</string>
<key>UIMainStoryboardFile~ipad</key>
<string>RobloxDeveloperiPad</string>
<key>UIPrerenderedIcon</key>
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
@@ -0,0 +1,707 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6254" systemVersion="14A389" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" initialViewController="OQs-cg-jQi">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6247"/>
</dependencies>
<scenes>
<!--Startup View Controller-->
<scene sceneID="dgs-w7-jlA">
<objects>
<viewController id="MbF-la-Dou" customClass="StartupViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ch9-hc-MQm"/>
<viewControllerLayoutGuide type="bottom" id="VPt-HR-Akd"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="iDV-Ae-lIU">
<rect key="frame" x="0.0" y="0.0" width="1024" height="768"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="HOi-cc-T71">
<rect key="frame" x="415" y="437" width="194" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="VPf-qc-UpK"/>
<constraint firstAttribute="width" constant="194" id="f7r-m2-fFl"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal">
<string key="title" base64-UTF8="YES">
EFNldHRpbmdzA
</string>
<color key="titleColor" red="0.1843137255" green="0.1843137255" blue="0.1843137255" alpha="1" colorSpace="calibratedRGB"/>
</state>
<connections>
<segue destination="yd8-Dw-Knl" kind="push" id="ok1-PT-No6"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fmZ-dY-rY9">
<rect key="frame" x="415" y="382" width="194" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="7Eh-ZL-Rgx"/>
<constraint firstAttribute="width" constant="194" id="eaN-PX-lcv"/>
</constraints>
<state key="normal" title="Connect to Studio">
<color key="titleColor" red="0.1843137255" green="0.1843137255" blue="0.1843137255" alpha="1" colorSpace="calibratedRGB"/>
</state>
<connections>
<action selector="connectButtonPressed:" destination="MbF-la-Dou" eventType="touchUpInside" id="TMm-7T-Bjy"/>
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="roblox-logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="pt4-gA-0n9">
<rect key="frame" x="384" y="244" width="257" height="66"/>
<constraints>
<constraint firstAttribute="width" constant="257" id="8XP-3x-Yzb"/>
<constraint firstAttribute="height" constant="66" id="mHV-bD-uiH"/>
</constraints>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="zYa-md-cLx">
<rect key="frame" x="422" y="678" width="181" height="30"/>
<state key="normal" title="Looking to Play ROBLOX?">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="robloxMobileButtonPressed:" destination="MbF-la-Dou" eventType="touchUpInside" id="Ikm-2v-msG"/>
</connections>
</button>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="- Developer -" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="y3I-Hd-a0Z">
<rect key="frame" x="430" y="318" width="164" height="24"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="nXX-Zc-tag"/>
<constraint firstAttribute="width" constant="164" id="qe1-RJ-NUa"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="HOi-cc-T71" secondAttribute="centerX" id="3oY-f0-ZgY"/>
<constraint firstItem="pt4-gA-0n9" firstAttribute="top" secondItem="Ch9-hc-MQm" secondAttribute="bottom" constant="200" id="6lS-f8-zbB"/>
<constraint firstAttribute="centerX" secondItem="pt4-gA-0n9" secondAttribute="centerX" id="ESE-4V-ur0"/>
<constraint firstItem="HOi-cc-T71" firstAttribute="top" secondItem="fmZ-dY-rY9" secondAttribute="bottom" constant="15" id="NBG-Ou-KiN"/>
<constraint firstAttribute="centerX" secondItem="fmZ-dY-rY9" secondAttribute="centerX" id="OjO-wm-fPe"/>
<constraint firstItem="fmZ-dY-rY9" firstAttribute="top" secondItem="y3I-Hd-a0Z" secondAttribute="bottom" constant="40" id="TaG-4c-dfH"/>
<constraint firstAttribute="centerX" secondItem="y3I-Hd-a0Z" secondAttribute="centerX" id="Y0V-6g-6kS"/>
<constraint firstItem="VPt-HR-Akd" firstAttribute="top" secondItem="zYa-md-cLx" secondAttribute="bottom" constant="60" id="kNK-ic-ldx"/>
<constraint firstAttribute="centerX" secondItem="zYa-md-cLx" secondAttribute="centerX" id="q2a-wc-AVA"/>
<constraint firstItem="y3I-Hd-a0Z" firstAttribute="top" secondItem="pt4-gA-0n9" secondAttribute="bottom" constant="8" id="y1E-2t-ZfO"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="gkp-9o-BRk"/>
<nil key="simulatedStatusBarMetrics"/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<connections>
<outlet property="connectToStudioButton" destination="fmZ-dY-rY9" id="PnH-vo-3ow"/>
<outlet property="developerLabel" destination="y3I-Hd-a0Z" id="TJQ-Ab-M2y"/>
<outlet property="robloxLogo" destination="pt4-gA-0n9" id="76t-O2-CoX"/>
<outlet property="robloxMobileButton" destination="zYa-md-cLx" id="nX8-0Z-JH9"/>
<outlet property="settingsButton" destination="HOi-cc-T71" id="aed-lN-Gm0"/>
<outlet property="verticalButtonConstraint" destination="TaG-4c-dfH" id="kng-pG-aSZ"/>
<outlet property="verticalLogoConstraint" destination="6lS-f8-zbB" id="YvM-28-4LC"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="eDc-47-HQs" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1510" y="-2"/>
</scene>
<!--Connect to Studio-->
<scene sceneID="hwD-fL-NU1">
<objects>
<viewController storyboardIdentifier="StudioConnectionViewController" id="a9u-m2-Uzu" customClass="StudioConnectionViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="OSx-BZ-ecZ"/>
<viewControllerLayoutGuide type="bottom" id="TMh-fQ-IKn"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="k6t-tp-4VR">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Waiting for ROBLOX Studio Connection..." textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="371" translatesAutoresizingMaskIntoConstraints="NO" id="rAA-st-Yng">
<rect key="frame" x="85" y="159" width="371" height="149"/>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<nil key="highlightedColor"/>
</label>
<button hidden="YES" opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Yln-4l-4Nz">
<rect key="frame" x="173" y="385" width="194" height="45"/>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal" title="Connect">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="connectToStudioForPlaySession:" destination="a9u-m2-Uzu" eventType="touchUpInside" id="iFi-dd-ZNS"/>
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="loading-0.png" translatesAutoresizingMaskIntoConstraints="NO" id="ZER-Ca-rap">
<rect key="frame" x="250" y="316" width="40" height="40"/>
</imageView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
<navigationItem key="navigationItem" title="Connect to Studio" id="wdR-C6-P8K">
<barButtonItem key="backBarButtonItem" image="closeButton.png" id="pRF-KY-KfB">
<connections>
<action selector="closeButtonPressed:" destination="a9u-m2-Uzu" id="4am-jP-JFH"/>
</connections>
</barButtonItem>
<connections>
<outlet property="leftBarButtonItem" destination="pRF-KY-KfB" id="BqN-bO-loV"/>
</connections>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<modalFormSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="connectToStudioButton" destination="Yln-4l-4Nz" id="bpE-6P-OBC"/>
<outlet property="connectingLabel" destination="rAA-st-Yng" id="J6v-s7-F5D"/>
<outlet property="loadingSpinner" destination="ZER-Ca-rap" id="8tk-7N-MBl"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8hg-I7-Ad2" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2764" y="72"/>
</scene>
<!--Pair With Studio-->
<scene sceneID="T95-7F-r37">
<objects>
<viewController storyboardIdentifier="StudioPairViewController" id="EBd-JD-j6Y" customClass="StudioPairViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="dCa-k4-5rS"/>
<viewControllerLayoutGuide type="bottom" id="Xao-Qf-OQp"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="iTc-R0-oFd">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" usesAttributedText="YES" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="500" translatesAutoresizingMaskIntoConstraints="NO" id="tai-je-o4w">
<rect key="frame" x="20" y="84" width="500" height="67"/>
<attributedString key="attributedText">
<fragment content="Enter ">
<attributes>
<color key="NSColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<font key="NSFont" size="17" name="HelveticaNeue"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
<fragment content="ROBLOX Dev Code">
<attributes>
<color key="NSColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<font key="NSFont" size="17" name="HelveticaNeue-Bold"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
<fragment content=" from ROBLOX Studio to connect">
<attributes>
<color key="NSColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<font key="NSFont" size="17" name="HelveticaNeue"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" ambiguous="YES" misplaced="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ByN-Dt-Aiz">
<rect key="frame" x="145" y="326" width="250" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="250" id="5Uh-41-Ur6"/>
<constraint firstAttribute="height" constant="30" id="G24-7h-JA4"/>
</constraints>
<state key="normal" title="How to find RBX Dev Code">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="helpButtonPressed:" destination="EBd-JD-j6Y" eventType="touchUpInside" id="3gm-FR-Hvd"/>
</connections>
</button>
<imageView hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="loading-0.png" translatesAutoresizingMaskIntoConstraints="NO" id="vwD-TK-IRb">
<rect key="frame" x="250" y="372" width="40" height="40"/>
</imageView>
<imageView hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="behind_alert_view.png" translatesAutoresizingMaskIntoConstraints="NO" id="5k6-zS-jaJ">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
</imageView>
<button opaque="NO" alpha="0.5" contentMode="scaleToFill" verticalHuggingPriority="249" ambiguous="YES" misplaced="YES" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" showsTouchWhenHighlighted="YES" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wlp-V4-t65">
<rect key="frame" x="173" y="271" width="194" height="45"/>
<constraints>
<constraint firstAttribute="width" constant="194" id="6ht-qm-lYb"/>
<constraint firstAttribute="height" constant="45" id="Egw-yF-Mtx"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal" title="Pair">
<color key="titleColor" red="0.4156863093" green="0.82352948189999997" blue="0.070588238540000001" alpha="1" colorSpace="deviceRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="pairWithStudio:" destination="EBd-JD-j6Y" eventType="touchUpInside" id="8Cu-hi-iMg"/>
</connections>
</button>
<view autoresizesSubviews="NO" opaque="NO" contentMode="scaleToFill" ambiguous="YES" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Jvp-Na-OCL" userLabel="DevCode View">
<rect key="frame" x="143" y="178" width="256" height="60"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" ambiguous="YES" misplaced="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" usesAttributedText="YES" placeholder="4-digit number" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="t8W-QH-nkT">
<rect key="frame" x="113" y="22" width="139" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="139" id="fij-F7-s8J"/>
<constraint firstAttribute="height" constant="30" id="qee-5F-W4Z"/>
</constraints>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" keyboardType="numberPad" keyboardAppearance="light" returnKeyType="go"/>
<connections>
<action selector="codeFieldDidEndOnExit:" destination="EBd-JD-j6Y" eventType="editingDidEndOnExit" id="XZB-EL-4LB"/>
</connections>
</textField>
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="249" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" adjustsImageWhenDisabled="NO" lineBreakMode="wordWrap" translatesAutoresizingMaskIntoConstraints="NO" id="bQh-hR-zHF">
<rect key="frame" x="1" y="18" width="254" height="37"/>
<constraints>
<constraint firstAttribute="height" constant="37" id="f4w-TT-zoA"/>
<constraint firstAttribute="width" constant="254" id="j0E-3m-OLA"/>
</constraints>
<state key="normal" title="Dev Code">
<color key="titleColor" red="0.0061270000220000004" green="0.60869565219999999" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
</state>
<connections>
<action selector="devCodeAreaPressed:" destination="EBd-JD-j6Y" eventType="touchUpInside" id="LaE-Vc-XxY"/>
</connections>
</button>
<view userInteractionEnabled="NO" contentMode="scaleToFill" ambiguous="YES" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8nA-6d-8Yb" userLabel="Line View">
<rect key="frame" x="1" y="54" width="254" height="2"/>
<color key="backgroundColor" red="0.2768455038" green="0.2768455038" blue="0.2768455038" alpha="1" colorSpace="calibratedRGB"/>
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
</view>
</subviews>
<color key="backgroundColor" white="1" alpha="0.0" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="tintColor" white="1" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="bQh-hR-zHF" firstAttribute="top" secondItem="Jvp-Na-OCL" secondAttribute="top" constant="18" id="8W5-vc-B6Q"/>
<constraint firstAttribute="width" constant="256" id="Bad-cp-WNx"/>
<constraint firstAttribute="centerX" secondItem="bQh-hR-zHF" secondAttribute="centerX" id="JFS-QA-FfE"/>
<constraint firstAttribute="centerX" secondItem="8nA-6d-8Yb" secondAttribute="centerX" id="JGf-8X-CiE"/>
<constraint firstItem="t8W-QH-nkT" firstAttribute="leading" secondItem="Jvp-Na-OCL" secondAttribute="leading" constant="113" id="X2d-mD-Xw5"/>
<constraint firstItem="bQh-hR-zHF" firstAttribute="leading" secondItem="Jvp-Na-OCL" secondAttribute="leading" constant="1" id="ajM-Fb-U4p"/>
<constraint firstAttribute="height" constant="60" id="soY-VM-sUC"/>
<constraint firstItem="8nA-6d-8Yb" firstAttribute="top" secondItem="t8W-QH-nkT" secondAttribute="bottom" constant="2" id="yRI-yg-v6s"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="5k6-zS-jaJ" secondAttribute="trailing" id="7Hg-dE-gWm"/>
<constraint firstItem="Xao-Qf-OQp" firstAttribute="top" secondItem="5k6-zS-jaJ" secondAttribute="bottom" id="HW8-4n-W6H"/>
<constraint firstItem="5k6-zS-jaJ" firstAttribute="top" secondItem="iTc-R0-oFd" secondAttribute="top" id="Wdo-0F-oTg"/>
<constraint firstItem="5k6-zS-jaJ" firstAttribute="leading" secondItem="iTc-R0-oFd" secondAttribute="leading" id="lla-5i-jEi"/>
</constraints>
</view>
<toolbarItems/>
<navigationItem key="navigationItem" title="Pair With Studio" id="3lR-C9-pzM">
<barButtonItem key="leftBarButtonItem" style="plain" id="kaM-wV-tvk">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" id="nZl-JW-y2k">
<rect key="frame" x="16" y="10" width="24" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" image="closeButton.png">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted" image="Close Button"/>
<connections>
<action selector="closeButtonPressed:" destination="EBd-JD-j6Y" eventType="touchUpInside" id="jc7-lL-lhN"/>
</connections>
</button>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Clear Code" id="wXg-Ih-JqC">
<color key="tintColor" red="0.14509803921568626" green="0.31372549019607843" blue="0.58039215686274503" alpha="1" colorSpace="calibratedRGB"/>
<connections>
<action selector="clearPairCode:" destination="EBd-JD-j6Y" id="94U-Fu-x2i"/>
</connections>
</barButtonItem>
</navigationItem>
<nil key="simulatedStatusBarMetrics"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<nil key="simulatedBottomBarMetrics"/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<modalFormSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="clearButton" destination="wXg-Ih-JqC" id="7zs-9l-JeN"/>
<outlet property="codeField" destination="t8W-QH-nkT" id="nFX-dX-9xh"/>
<outlet property="enterPairCodeButton" destination="bQh-hR-zHF" id="Umt-jk-Gzq"/>
<outlet property="loadingSpinner" destination="vwD-TK-IRb" id="U9h-oW-9y1"/>
<outlet property="pairToStudioButton" destination="wlp-V4-t65" id="LCg-ev-sBN"/>
<outlet property="pairToStudioHelpButton" destination="ByN-Dt-Aiz" id="5vr-aH-5id"/>
<outlet property="pairingLabel" destination="tai-je-o4w" id="Rvj-Kp-iwO"/>
<outlet property="pairingVignette" destination="5k6-zS-jaJ" id="LRL-Vu-PM2"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Iml-dm-3OK" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2764" y="1140"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="Dlo-dd-3rN">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="OQs-cg-jQi" sceneMemberID="viewController">
<toolbarItems/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="GGG-ym-r78">
<rect key="frame" x="0.0" y="0.0" width="768" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<color key="barTintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<textAttributes key="titleTextAttributes">
<fontDescription key="fontDescription" name="HelveticaNeue" family="Helvetica Neue" pointSize="18"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
</textAttributes>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="MbF-la-Dou" kind="relationship" relationship="rootViewController" id="beH-0B-Mm0"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Mw9-Y8-hfn" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="149" y="-2"/>
</scene>
<!--Debug Settings View Controller-->
<scene sceneID="GKi-ry-aO3">
<objects>
<viewController modalPresentationStyle="formSheet" id="yd8-Dw-Knl" userLabel="Debug Settings View Controller" customClass="SettingsViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="2pX-QI-tRW"/>
<viewControllerLayoutGuide type="bottom" id="rCx-6b-JSE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="SIe-ef-byn">
<rect key="frame" x="0.0" y="0.0" width="1024" height="768"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" fixedFrame="YES" text="Stats Display" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RcY-ml-QaK">
<rect key="frame" x="220" y="86" width="107" height="20"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" fixedFrame="YES" text="Connection" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2Tj-eU-TOm">
<rect key="frame" x="220" y="415" width="107" height="20"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" userInteractionEnabled="NO" tag="13" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Hb-Zv-zgz" userLabel="verifyStatusButton">
<rect key="frame" x="461" y="165" width="36" height="36"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<button opaque="NO" userInteractionEnabled="NO" tag="12" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Xuw-W8-0Ll" userLabel="passwordStatusButton">
<rect key="frame" x="461" y="124" width="36" height="36"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<tableView clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="QfJ-JH-atI">
<rect key="frame" x="200" y="129" width="625" height="263"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<rect key="contentStretch" x="0.0" y="1.3877787807814457e-17" width="1" height="1"/>
<inset key="separatorInset" minX="20" minY="0.0" maxX="0.0" maxY="0.0"/>
<color key="sectionIndexBackgroundColor" red="0.96785878270000003" green="0.97589408700000002" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<prototypes>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="Evc-Sx-Q42">
<rect key="frame" x="0.0" y="22" width="625" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Evc-Sx-Q42" id="LF2-rA-sbg">
<rect key="frame" x="0.0" y="0.0" width="625" height="43"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<sections/>
<connections>
<outlet property="dataSource" destination="yd8-Dw-Knl" id="VBx-us-04F"/>
<outlet property="delegate" destination="yd8-Dw-Knl" id="wjS-Hv-5Gy"/>
</connections>
</tableView>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="wordWrap" translatesAutoresizingMaskIntoConstraints="NO" id="kPU-dt-hdm">
<rect key="frame" x="200" y="460" width="625" height="40"/>
<color key="backgroundColor" red="0.96785878270000003" green="0.97589408700000002" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<fontDescription key="fontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="15"/>
<inset key="contentEdgeInsets" minX="20" minY="0.0" maxX="0.0" maxY="0.0"/>
<state key="normal" title="Connect to New Studio"/>
<connections>
<action selector="pairWithStudioPressed:" destination="yd8-Dw-Knl" eventType="touchUpInside" id="lhC-jT-B6d"/>
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="forwardChevron.png" translatesAutoresizingMaskIntoConstraints="NO" id="zrI-kz-qZg">
<rect key="frame" x="795" y="468" width="24" height="24"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.94901960780000005" green="0.95294117649999999" blue="0.96470588239999999" alpha="1" colorSpace="calibratedRGB"/>
</view>
<navigationItem key="navigationItem" title="Settings" id="9JF-49-xfZ">
<barButtonItem key="rightBarButtonItem" title="Log In" id="EeS-yw-ADY">
<connections>
<action selector="loginButtonPressed:" destination="yd8-Dw-Knl" id="afx-43-0M5"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="debugDisplayLabel" destination="RcY-ml-QaK" id="CoH-Ky-DpQ"/>
<outlet property="debugDisplayTableView" destination="QfJ-JH-atI" id="UPn-9u-d6a"/>
<outlet property="pairWithStudioButton" destination="kPU-dt-hdm" id="V2n-Fa-oSC"/>
<outlet property="pairWithStudioLabel" destination="2Tj-eU-TOm" id="Lpi-ia-CsW"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="gIo-bs-Lh1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2756" y="-1142"/>
</scene>
<!--Login-->
<scene sceneID="8jX-RI-lWg">
<objects>
<viewController storyboardIdentifier="TestAccountSigninViewController" id="fBv-SN-6OS" customClass="TestAccountSigninViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="deN-10-O2l"/>
<viewControllerLayoutGuide type="bottom" id="jnH-AJ-X2e"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="DIP-H4-Vcz">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder=" Username" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="tKW-5w-1GH">
<rect key="frame" x="145" y="142" width="250" height="30"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="next"/>
<connections>
<action selector="usernameDidEndOnExit:" destination="fBv-SN-6OS" eventType="editingDidEndOnExit" id="v1S-2e-x6e"/>
</connections>
</textField>
<textField opaque="NO" clipsSubviews="YES" tag="1" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder=" Password" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="O2u-wM-wV0">
<rect key="frame" x="145" y="207" width="250" height="30"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="go" secureTextEntry="YES"/>
<connections>
<action selector="passwordDidEndOnExit:" destination="fBv-SN-6OS" eventType="editingDidEndOnExit" id="Ndo-cK-Yyb"/>
</connections>
</textField>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nbv-S7-Ofi">
<rect key="frame" x="173" y="266" width="194" height="40"/>
<fontDescription key="fontDescription" type="system" pointSize="22"/>
<state key="normal" title="Login">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="loginButtonPressed:" destination="fBv-SN-6OS" eventType="touchUpInside" id="tHk-wO-cOs"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Z4c-Tl-Okf">
<rect key="frame" x="173" y="321" width="194" height="40"/>
<fontDescription key="fontDescription" type="system" pointSize="22"/>
<state key="normal" title="Forgot Password?">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<segue destination="F3P-Xl-25P" kind="push" id="K5v-LV-vON"/>
</connections>
</button>
<view userInteractionEnabled="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="a0Q-E5-UZF" userLabel="Line View">
<rect key="frame" x="143" y="171" width="254" height="2"/>
<color key="backgroundColor" red="0.2784313725" green="0.2784313725" blue="0.2784313725" alpha="1" colorSpace="calibratedRGB"/>
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
</view>
<view userInteractionEnabled="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tpx-7e-hMh" userLabel="Line View">
<rect key="frame" x="143" y="236" width="254" height="2"/>
<color key="backgroundColor" red="0.2784313725" green="0.2784313725" blue="0.2784313725" alpha="1" colorSpace="calibratedRGB"/>
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
</view>
<view hidden="YES" alpha="0.85000002384185791" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cGP-3K-5N1">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Logging in..." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qt3-OO-Emu">
<rect key="frame" x="201" y="340" width="139" height="32"/>
<fontDescription key="fontDescription" type="system" pointSize="23"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1Uv-7f-Rsa">
<rect key="frame" x="258" y="308" width="24" height="24"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
</view>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
<toolbarItems/>
<navigationItem key="navigationItem" title="Login" id="Rf5-I4-S38">
<barButtonItem key="backBarButtonItem" image="closeButton.png" id="hiB-yc-Zoa">
<connections>
<action selector="closeButtonPressed:" destination="fBv-SN-6OS" id="e90-0C-nPU"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Logout" id="uok-lv-ohj">
<connections>
<action selector="logoutButtonPressed:" destination="fBv-SN-6OS" id="Sbr-qS-GSb"/>
</connections>
</barButtonItem>
<connections>
<outlet property="leftBarButtonItem" destination="hiB-yc-Zoa" id="kp7-u3-jsn"/>
</connections>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<nil key="simulatedBottomBarMetrics"/>
<modalFormSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="forgotPasswordButton" destination="Z4c-Tl-Okf" id="Jcv-Vo-HLf"/>
<outlet property="loadingSpinner" destination="1Uv-7f-Rsa" id="ATb-qF-OFb"/>
<outlet property="loggingInView" destination="cGP-3K-5N1" id="hKp-KP-q1G"/>
<outlet property="loginButton" destination="nbv-S7-Ofi" id="4O5-Qs-MB0"/>
<outlet property="logoutButton" destination="uok-lv-ohj" id="gDj-dn-OAq"/>
<outlet property="password" destination="O2u-wM-wV0" id="hWR-iG-PJc"/>
<outlet property="username" destination="tKW-5w-1GH" id="CVH-LI-dIO"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="U1o-vC-tBn" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4111" y="-1068"/>
</scene>
<!--Forgot Password-->
<scene sceneID="XOr-FJ-S0l">
<objects>
<viewController id="F3P-Xl-25P" customClass="ResetPasswordViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="YhJ-rW-uvB"/>
<viewControllerLayoutGuide type="bottom" id="q3f-E9-XFh"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="jJs-fI-P5b">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<webView contentMode="scaleToFill" fixedFrame="YES" scalesPageToFit="YES" translatesAutoresizingMaskIntoConstraints="NO" id="20B-y2-aTV">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<connections>
<outlet property="delegate" destination="F3P-Xl-25P" id="Sgc-iy-wI4"/>
</connections>
</webView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
<navigationItem key="navigationItem" title="Forgot Password" id="Cct-Tg-Hcz">
<barButtonItem key="rightBarButtonItem" image="loading-0.png" width="24" id="qf2-mg-1D2">
<inset key="imageInsets" minX="0.0" minY="24" maxX="0.0" maxY="24"/>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="loadingBarItem" destination="qf2-mg-1D2" id="vRo-Fc-L7o"/>
<outlet property="webView" destination="20B-y2-aTV" id="5v8-Hf-zVq"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lZq-qf-lb7" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="5309" y="-1068"/>
</scene>
<!--How to Pair-->
<scene sceneID="eRU-fv-yJn">
<objects>
<viewController storyboardIdentifier="PairTutorialViewController" id="c7l-KI-wcJ" customClass="PairTutorialViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="phJ-1S-I7E"/>
<viewControllerLayoutGuide type="bottom" id="5Zo-aV-EMC"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="qkG-aH-fD4">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" usesAttributedText="YES" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="279" translatesAutoresizingMaskIntoConstraints="NO" id="w12-KU-FSu">
<rect key="frame" x="130" y="186" width="279" height="152"/>
<attributedString key="attributedText">
<fragment content="STEP_INSTRUCTIONS">
<attributes>
<color key="NSColor" cocoaTouchSystemColor="darkTextColor"/>
<font key="NSFont" size="17" name="HelveticaNeue"/>
<paragraphStyle key="NSParagraphStyle" alignment="left" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="step3.png" translatesAutoresizingMaskIntoConstraints="NO" id="FWQ-kF-Y1Z">
<rect key="frame" x="122" y="355" width="296" height="218"/>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="STEP_TITLE" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XMH-tz-aGh">
<rect key="frame" x="130" y="111" width="280" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
<navigationItem key="navigationItem" title="How to Pair" id="ruo-gi-saN">
<barButtonItem key="backBarButtonItem" title="Back" id="XU0-AC-W2v"/>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<modalFormSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="instructionImageView" destination="FWQ-kF-Y1Z" id="9Xi-qZ-xMi"/>
<outlet property="instructionLabel" destination="w12-KU-FSu" id="0q9-bp-eLk"/>
<outlet property="instructionTitle" destination="XMH-tz-aGh" id="c1h-IG-cYK"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="hfc-Zs-Tq4" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="5672" y="1132"/>
</scene>
<!--How to Pair-->
<scene sceneID="RlO-ln-hSn">
<objects>
<pageViewController storyboardIdentifier="PageViewController" autoresizesArchivedViewToFullSize="NO" transitionStyle="scroll" navigationOrientation="horizontal" spineLocation="none" id="J37-xD-UeI" sceneMemberID="viewController">
<navigationItem key="navigationItem" title="How to Pair" id="s3K-54-46A"/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<modalFormSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
</pageViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Nkr-mb-BPA" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3669" y="1140"/>
</scene>
<!--Pair Tutorial Data Controller-->
<scene sceneID="8UM-We-WSs">
<objects>
<viewController storyboardIdentifier="PairTutorialDataController" id="uRK-fo-qGV" customClass="PairTutorialDataController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="CVo-0t-ee0"/>
<viewControllerLayoutGuide type="bottom" id="xIN-D0-GDp"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="sGh-HA-Tkn">
<rect key="frame" x="0.0" y="0.0" width="540" height="620"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<modalFormSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="hJc-FM-k8M" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4639" y="1140"/>
</scene>
</scenes>
<resources>
<image name="Close Button" width="17" height="17"/>
<image name="behind_alert_view.png" width="320" height="460"/>
<image name="closeButton.png" width="24" height="24"/>
<image name="forwardChevron.png" width="24" height="24"/>
<image name="loading-0.png" width="40" height="40"/>
<image name="roblox-logo.png" width="1031" height="265"/>
<image name="step3.png" width="346" height="212"/>
</resources>
<simulatedMetricsContainer key="defaultSimulatedMetrics">
<simulatedStatusBarMetrics key="statusBar" statusBarStyle="lightContent"/>
<simulatedOrientationMetrics key="orientation"/>
<simulatedScreenMetrics key="destination"/>
</simulatedMetricsContainer>
</document>
@@ -0,0 +1,786 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6254" systemVersion="14B25" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" initialViewController="XDK-F6-9Y5">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6247"/>
<capability name="Alignment constraints with different attributes" minToolsVersion="5.1"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<scenes>
<!--Startup View Controller-->
<scene sceneID="8mf-Ea-WOt">
<objects>
<viewController id="pzz-xK-D6a" customClass="StartupViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="jPp-ga-fb3"/>
<viewControllerLayoutGuide type="bottom" id="rKs-jM-S2G"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="K2y-jH-rbC">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BZp-aM-oZh">
<rect key="frame" x="63" y="350" width="194" height="45"/>
<constraints>
<constraint firstAttribute="height" constant="45" id="3Ui-FA-dfu"/>
<constraint firstAttribute="width" constant="194" id="4sD-50-a3j"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal">
<string key="title" base64-UTF8="YES">
EFNldHRpbmdzA
</string>
<color key="titleColor" red="0.1843137255" green="0.1843137255" blue="0.1843137255" alpha="1" colorSpace="calibratedRGB"/>
</state>
<connections>
<segue destination="Rch-p7-CnX" kind="push" id="BKY-7m-ZaX"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Zg1-VC-88U">
<rect key="frame" x="63" y="285" width="194" height="45"/>
<constraints>
<constraint firstAttribute="width" constant="194" id="QNj-lm-Eql"/>
<constraint firstAttribute="height" constant="45" id="wo5-er-dTN"/>
</constraints>
<state key="normal" title="Connect to Studio">
<color key="titleColor" red="0.18431372549019609" green="0.18431372549019609" blue="0.18431372549019609" alpha="1" colorSpace="calibratedRGB"/>
</state>
<connections>
<action selector="connectButtonPressed:" destination="pzz-xK-D6a" eventType="touchUpInside" id="Fvh-So-dh7"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wh4-Za-ns7">
<rect key="frame" x="70" y="528" width="181" height="30"/>
<state key="normal" title="Looking to Play ROBLOX?">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="robloxMobileButtonPressed:" destination="pzz-xK-D6a" eventType="touchUpInside" id="2Kg-Mw-4Sy"/>
</connections>
</button>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="- Developer -" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dqF-pK-AvF">
<rect key="frame" x="110" y="172" width="101" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="749" image="roblox-logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="Jvj-6M-Rwi">
<rect key="frame" x="31" y="104" width="258" height="66"/>
<constraints>
<constraint firstAttribute="width" constant="258" id="aoL-oj-Igt"/>
<constraint firstAttribute="height" constant="66" id="pfe-Hk-JcM"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="dqF-pK-AvF" firstAttribute="top" secondItem="Jvj-6M-Rwi" secondAttribute="bottom" constant="2" id="05w-3y-z0T"/>
<constraint firstItem="Jvj-6M-Rwi" firstAttribute="top" secondItem="jPp-ga-fb3" secondAttribute="bottom" constant="60" id="4Mc-Q6-KU1"/>
<constraint firstAttribute="centerX" secondItem="wh4-Za-ns7" secondAttribute="centerX" id="4jU-sf-sWr"/>
<constraint firstAttribute="centerX" secondItem="Zg1-VC-88U" secondAttribute="centerX" id="K3R-qU-lGg"/>
<constraint firstItem="Zg1-VC-88U" firstAttribute="top" secondItem="dqF-pK-AvF" secondAttribute="bottom" constant="92" id="OAG-Ke-4k6"/>
<constraint firstAttribute="centerX" secondItem="BZp-aM-oZh" secondAttribute="centerX" id="ODc-js-JGJ"/>
<constraint firstItem="BZp-aM-oZh" firstAttribute="top" secondItem="Zg1-VC-88U" secondAttribute="bottom" constant="20" id="cuP-ef-Pyy"/>
<constraint firstAttribute="centerX" secondItem="Jvj-6M-Rwi" secondAttribute="centerX" id="gJF-t0-Uyl"/>
<constraint firstItem="rKs-jM-S2G" firstAttribute="top" secondItem="wh4-Za-ns7" secondAttribute="bottom" constant="10" id="gzL-pR-Yb9"/>
<constraint firstAttribute="centerX" secondItem="dqF-pK-AvF" secondAttribute="centerX" id="k1X-yV-cX9"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="AHM-RA-cDZ"/>
<nil key="simulatedStatusBarMetrics"/>
<connections>
<outlet property="connectToStudioButton" destination="Zg1-VC-88U" id="khY-Tf-bJk"/>
<outlet property="developerLabel" destination="dqF-pK-AvF" id="INJ-Qu-B2A"/>
<outlet property="robloxLogo" destination="Jvj-6M-Rwi" id="k5v-Pp-wfi"/>
<outlet property="robloxMobileButton" destination="wh4-Za-ns7" id="Rmc-JZ-KfM"/>
<outlet property="settingsButton" destination="BZp-aM-oZh" id="SHQ-zq-QTb"/>
<outlet property="verticalButtonConstraint" destination="OAG-Ke-4k6" id="CWh-cz-b5z"/>
<outlet property="verticalLogoConstraint" destination="4Mc-Q6-KU1" id="xdJ-aa-Tpu"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Bax-VJ-sRf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1510" y="-2"/>
</scene>
<!--Connect to Studio-->
<scene sceneID="0Le-Ku-tpO">
<objects>
<viewController storyboardIdentifier="StudioConnectionViewController" id="ao4-mp-6nm" customClass="StudioConnectionViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="yLd-Mh-rQg"/>
<viewControllerLayoutGuide type="bottom" id="5CZ-Rh-0L5"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="xLB-25-pws">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Waiting for ROBLOX Studio Connection..." textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="270" translatesAutoresizingMaskIntoConstraints="NO" id="PDz-NE-dU9">
<rect key="frame" x="47" y="79" width="227" height="189"/>
<constraints>
<constraint firstAttribute="height" constant="189" id="rAc-5g-o6I"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<nil key="highlightedColor"/>
</label>
<button hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1bL-DF-NV4">
<rect key="frame" x="63" y="468" width="194" height="45"/>
<constraints>
<constraint firstAttribute="height" constant="45" id="9mo-O9-7lh"/>
<constraint firstAttribute="width" constant="194" id="PWN-SB-mjZ"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal" title="Connect">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="connectToStudioForPlaySession:" destination="ao4-mp-6nm" eventType="touchUpInside" id="WDs-BE-j3y"/>
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="loading-0.png" translatesAutoresizingMaskIntoConstraints="NO" id="vE7-Se-lFo">
<rect key="frame" x="140" y="288" width="40" height="40"/>
</imageView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="vE7-Se-lFo" secondAttribute="centerX" id="9Z6-Kk-5a8"/>
<constraint firstAttribute="centerX" secondItem="1bL-DF-NV4" secondAttribute="centerX" id="VbL-je-vMe"/>
<constraint firstItem="PDz-NE-dU9" firstAttribute="top" secondItem="yLd-Mh-rQg" secondAttribute="bottom" constant="15" id="Y9x-hg-mUz"/>
<constraint firstItem="5CZ-Rh-0L5" firstAttribute="top" secondItem="1bL-DF-NV4" secondAttribute="bottom" constant="55" id="You-8B-9Tj"/>
<constraint firstItem="vE7-Se-lFo" firstAttribute="top" secondItem="PDz-NE-dU9" secondAttribute="bottom" constant="20" id="pAM-wz-kyh"/>
<constraint firstAttribute="centerX" secondItem="PDz-NE-dU9" secondAttribute="centerX" id="wtd-Ir-Mvj"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Connect to Studio" id="jIE-9O-h2a"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="connectToStudioButton" destination="1bL-DF-NV4" id="gjW-uR-z9G"/>
<outlet property="connectingLabel" destination="PDz-NE-dU9" id="gFX-eh-8QH"/>
<outlet property="loadingSpinner" destination="vE7-Se-lFo" id="svm-gV-wPT"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="IfW-I0-5i6" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2756" y="-2"/>
</scene>
<!--Pair With Studio-->
<scene sceneID="2Og-hV-UMJ">
<objects>
<viewController storyboardIdentifier="StudioPairViewController" id="GQ2-wq-gkr" customClass="StudioPairViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="893-BY-bAS"/>
<viewControllerLayoutGuide type="bottom" id="Ch8-rK-ke8"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="qDS-Yu-1fk">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" usesAttributedText="YES" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="258" translatesAutoresizingMaskIntoConstraints="NO" id="pBw-XR-qfs">
<rect key="frame" x="31" y="90" width="258" height="64"/>
<attributedString key="attributedText">
<fragment content="Enter ">
<attributes>
<color key="NSColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<font key="NSFont" size="17" name="HelveticaNeue"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
<fragment content="ROBLOX Dev Code">
<attributes>
<color key="NSColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<font key="NSFont" size="17" name="HelveticaNeue-Bold"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
<fragment content=" from ROBLOX Studio to connect">
<attributes>
<color key="NSColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<font key="NSFont" size="17" name="HelveticaNeue"/>
<paragraphStyle key="NSParagraphStyle" alignment="center" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" misplaced="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ene-YC-U7w">
<rect key="frame" x="35" y="341" width="250" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="250" id="JZQ-It-OWX"/>
<constraint firstAttribute="height" constant="30" id="OZY-BI-KDP"/>
</constraints>
<state key="normal" title="How to find RBX Dev Code">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="helpButtonPressed:" destination="GQ2-wq-gkr" eventType="touchUpInside" id="7YI-aj-sfU"/>
</connections>
</button>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" misplaced="YES" image="loading-0.png" translatesAutoresizingMaskIntoConstraints="NO" id="Ejj-4P-ZI1">
<rect key="frame" x="140" y="418" width="40" height="40"/>
</imageView>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="behind_alert_view.png" translatesAutoresizingMaskIntoConstraints="NO" id="KYA-z0-XvC">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
</imageView>
<button opaque="NO" alpha="0.5" contentMode="scaleToFill" verticalHuggingPriority="249" misplaced="YES" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" showsTouchWhenHighlighted="YES" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1Mr-vh-rXY">
<rect key="frame" x="63" y="279" width="194" height="45"/>
<constraints>
<constraint firstAttribute="width" constant="194" id="f1x-iV-Egq"/>
<constraint firstAttribute="height" constant="45" id="fdu-3l-Jjl"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal" title="Pair">
<color key="titleColor" red="0.41568630933761597" green="0.82352948188781738" blue="0.070588238537311554" alpha="1" colorSpace="deviceRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="pairWithStudio:" destination="GQ2-wq-gkr" eventType="touchUpInside" id="ceK-I8-5b2"/>
</connections>
</button>
<view autoresizesSubviews="NO" opaque="NO" contentMode="scaleToFill" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BeZ-ED-60B" userLabel="DevCode View">
<rect key="frame" x="33" y="177" width="256" height="60"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" ambiguous="YES" misplaced="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" usesAttributedText="YES" placeholder="4-digit number" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="D4s-av-6OG">
<rect key="frame" x="113" y="22" width="139" height="30"/>
<attributedString key="attributedText"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" keyboardType="numberPad" keyboardAppearance="light" returnKeyType="go"/>
<connections>
<action selector="codeFieldDidEndOnExit:" destination="GQ2-wq-gkr" eventType="editingDidEndOnExit" id="pnB-r2-qEO"/>
</connections>
</textField>
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="249" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" adjustsImageWhenDisabled="NO" lineBreakMode="wordWrap" translatesAutoresizingMaskIntoConstraints="NO" id="Ko7-UF-JJJ">
<rect key="frame" x="1" y="18" width="254" height="37"/>
<constraints>
<constraint firstAttribute="width" constant="254" id="Ws5-zo-868"/>
<constraint firstAttribute="height" constant="37" id="fNf-kn-hnh"/>
</constraints>
<state key="normal" title="Dev Code">
<color key="titleColor" red="0.0061270000220000004" green="0.60869565219999999" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
</state>
<connections>
<action selector="devCodeAreaPressed:" destination="GQ2-wq-gkr" eventType="touchUpInside" id="yd2-p5-iyC"/>
</connections>
</button>
<view userInteractionEnabled="NO" contentMode="scaleToFill" ambiguous="YES" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0jI-U8-EhF" userLabel="Line View">
<rect key="frame" x="1" y="54" width="254" height="2"/>
<color key="backgroundColor" red="0.27684550382653061" green="0.27684550382653061" blue="0.27684550382653061" alpha="1" colorSpace="calibratedRGB"/>
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
</view>
</subviews>
<color key="backgroundColor" white="1" alpha="0.0" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="tintColor" white="1" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="0jI-U8-EhF" secondAttribute="centerX" id="39J-DG-g1v"/>
<constraint firstAttribute="height" constant="60" id="5Js-yr-6DX"/>
<constraint firstAttribute="centerX" secondItem="D4s-av-6OG" secondAttribute="centerX" constant="-54.5" id="Iey-3p-9P0"/>
<constraint firstAttribute="width" constant="256" id="aa3-zt-S5j"/>
<constraint firstItem="Ko7-UF-JJJ" firstAttribute="top" secondItem="BeZ-ED-60B" secondAttribute="top" constant="18" id="b0Z-yd-LdZ"/>
<constraint firstAttribute="centerX" secondItem="Ko7-UF-JJJ" secondAttribute="centerX" id="iXH-yG-d8m"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstItem="ene-YC-U7w" firstAttribute="leading" secondItem="qDS-Yu-1fk" secondAttribute="leading" constant="35" id="0Jg-TB-tC9"/>
<constraint firstItem="pBw-XR-qfs" firstAttribute="leading" secondItem="qDS-Yu-1fk" secondAttribute="leading" constant="31" id="28n-uW-xGc"/>
<constraint firstItem="KYA-z0-XvC" firstAttribute="top" secondItem="qDS-Yu-1fk" secondAttribute="top" id="2wh-AJ-g4d"/>
<constraint firstItem="1Mr-vh-rXY" firstAttribute="leading" secondItem="qDS-Yu-1fk" secondAttribute="leading" constant="63" id="3ib-SX-9dK"/>
<constraint firstAttribute="centerX" secondItem="ene-YC-U7w" secondAttribute="centerX" id="D0V-WY-uf5"/>
<constraint firstAttribute="trailing" secondItem="KYA-z0-XvC" secondAttribute="trailing" id="Fbx-GR-STR"/>
<constraint firstAttribute="trailing" secondItem="1Mr-vh-rXY" secondAttribute="trailing" constant="63" id="Gz7-AE-ufw"/>
<constraint firstItem="1Mr-vh-rXY" firstAttribute="centerY" secondItem="BeZ-ED-60B" secondAttribute="centerY" constant="70" id="KQs-vd-LvU"/>
<constraint firstItem="BeZ-ED-60B" firstAttribute="leading" secondItem="qDS-Yu-1fk" secondAttribute="leading" constant="33" id="Qhk-kl-VcH"/>
<constraint firstAttribute="centerX" secondItem="BeZ-ED-60B" secondAttribute="centerX" id="QkI-6u-TPR"/>
<constraint firstItem="KYA-z0-XvC" firstAttribute="leading" secondItem="qDS-Yu-1fk" secondAttribute="leading" id="QpO-uU-Z83"/>
<constraint firstItem="Ch8-rK-ke8" firstAttribute="top" secondItem="KYA-z0-XvC" secondAttribute="bottom" id="RkJ-1Q-eMG"/>
<constraint firstAttribute="centerX" secondItem="BeZ-ED-60B" secondAttribute="centerX" id="S1x-xt-sQa"/>
<constraint firstItem="BeZ-ED-60B" firstAttribute="top" secondItem="pBw-XR-qfs" secondAttribute="centerY" constant="30" id="amk-8e-zq0"/>
<constraint firstItem="ene-YC-U7w" firstAttribute="top" secondItem="1Mr-vh-rXY" secondAttribute="bottom" constant="12" id="d8a-3k-AdV"/>
<constraint firstAttribute="centerX" secondItem="BeZ-ED-60B" secondAttribute="centerX" id="hm9-ro-oQe"/>
<constraint firstItem="pBw-XR-qfs" firstAttribute="top" secondItem="893-BY-bAS" secondAttribute="top" constant="40" id="jso-e4-P3T"/>
<constraint firstItem="Ch8-rK-ke8" firstAttribute="top" secondItem="KYA-z0-XvC" secondAttribute="bottom" id="nPy-i5-vot"/>
<constraint firstAttribute="centerX" secondItem="Ejj-4P-ZI1" secondAttribute="centerX" id="oyc-bn-P3h"/>
<constraint firstAttribute="centerX" secondItem="pBw-XR-qfs" secondAttribute="centerX" id="qU3-6h-36j"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Pair With Studio" id="Weu-Uu-bsz">
<barButtonItem key="rightBarButtonItem" title="Clear" id="3dC-a3-2mR">
<connections>
<action selector="clearPairCode:" destination="GQ2-wq-gkr" id="9aK-xo-B3L"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="clearButton" destination="3dC-a3-2mR" id="7Wl-IT-kbo"/>
<outlet property="codeField" destination="D4s-av-6OG" id="0Gp-IF-Ekf"/>
<outlet property="devCodeConstraint" destination="amk-8e-zq0" id="k4M-dP-qK9"/>
<outlet property="enterPairCodeButton" destination="Ko7-UF-JJJ" id="Xsk-t4-bnP"/>
<outlet property="loadingSpinner" destination="Ejj-4P-ZI1" id="7ho-gT-zOa"/>
<outlet property="pairButtonConstraint" destination="KQs-vd-LvU" id="FV4-68-sZ3"/>
<outlet property="pairToStudioButton" destination="1Mr-vh-rXY" id="3Z3-l6-ME2"/>
<outlet property="pairToStudioHelpButton" destination="ene-YC-U7w" id="CZ4-VD-Kho"/>
<outlet property="pairingLabel" destination="pBw-XR-qfs" id="IIa-BI-0D5"/>
<outlet property="pairingVignette" destination="KYA-z0-XvC" id="kJL-uL-ab4"/>
<outlet property="topMostConstraint" destination="jso-e4-P3T" id="sLJ-X5-mxO"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="aYN-Zm-NCa" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2756" y="956"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="GgN-Fm-hXv">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="XDK-F6-9Y5" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="nmu-u4-ngf">
<rect key="frame" x="0.0" y="0.0" width="768" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<color key="barTintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<textAttributes key="titleTextAttributes">
<fontDescription key="fontDescription" name="HelveticaNeue" family="Helvetica Neue" pointSize="18"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
</textAttributes>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="pzz-xK-D6a" kind="relationship" relationship="rootViewController" id="g5Q-7r-TcX"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="d5D-g4-btP" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="816" y="-2"/>
</scene>
<!--Debug Settings View Controller-->
<scene sceneID="b7j-Pu-K4g">
<objects>
<viewController modalPresentationStyle="formSheet" id="Rch-p7-CnX" userLabel="Debug Settings View Controller" customClass="SettingsViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="KRs-8N-dSI"/>
<viewControllerLayoutGuide type="bottom" id="bvv-LD-6PZ"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="NoT-2j-3pn">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" directionalLockEnabled="YES" showsHorizontalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tSZ-kW-ci4">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<subviews>
<view contentMode="scaleToFill" ambiguous="YES" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="oei-07-Ofu" userLabel="Content View">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" fixedFrame="YES" text="Stats Display" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Qdv-ND-e9O">
<rect key="frame" x="20" y="20" width="107" height="20"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" fixedFrame="YES" text="Connection" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7XS-Wf-vIe">
<rect key="frame" x="20" y="320" width="107" height="20"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" userInteractionEnabled="NO" tag="13" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="c0g-JS-eAc" userLabel="verifyStatusButton">
<rect key="frame" x="481" y="150" width="36" height="36"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<button opaque="NO" userInteractionEnabled="NO" tag="12" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bJz-hq-J0k" userLabel="passwordStatusButton">
<rect key="frame" x="481" y="101" width="36" height="36"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal">
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<state key="highlighted">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
</button>
<button opaque="NO" contentMode="scaleToFill" ambiguous="YES" misplaced="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="wordWrap" translatesAutoresizingMaskIntoConstraints="NO" id="5jy-Kk-LHY">
<rect key="frame" x="0.0" y="364" width="320" height="40"/>
<color key="backgroundColor" red="0.9678587826733388" green="0.97589408700500402" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="COH-vS-t6E"/>
<constraint firstAttribute="width" constant="320" id="Yzc-8u-0tN"/>
</constraints>
<fontDescription key="fontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="15"/>
<inset key="contentEdgeInsets" minX="20" minY="0.0" maxX="0.0" maxY="0.0"/>
<state key="normal" title="Connect to New Studio"/>
<connections>
<action selector="pairWithStudioPressed:" destination="Rch-p7-CnX" eventType="touchUpInside" id="Xqv-yY-Yil"/>
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" misplaced="YES" image="forwardChevron.png" translatesAutoresizingMaskIntoConstraints="NO" id="nlc-aR-x6Z">
<rect key="frame" x="290" y="372" width="24" height="24"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="XOR-iT-i9j"/>
<constraint firstAttribute="width" constant="24" id="bae-a3-6Yo"/>
</constraints>
</imageView>
<tableView clipsSubviews="YES" contentMode="scaleToFill" ambiguous="YES" misplaced="YES" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="3NY-CL-b0K">
<rect key="frame" x="0.0" y="65" width="320" height="220"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<rect key="contentStretch" x="0.0" y="1.3877787807814457e-17" width="1" height="1"/>
<constraints>
<constraint firstAttribute="height" constant="220" id="btw-N7-64W"/>
</constraints>
<inset key="separatorInset" minX="20" minY="0.0" maxX="0.0" maxY="0.0"/>
<color key="sectionIndexBackgroundColor" red="0.96785878270000003" green="0.97589408700000002" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<prototypes>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="lTn-20-B0k">
<rect key="frame" x="0.0" y="22" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="lTn-20-B0k" id="iIF-3j-rma">
<rect key="frame" x="0.0" y="0.0" width="320" height="43"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<sections/>
<connections>
<outlet property="dataSource" destination="Rch-p7-CnX" id="C9F-uw-6Wc"/>
<outlet property="delegate" destination="Rch-p7-CnX" id="DFo-wl-LQZ"/>
</connections>
</tableView>
</subviews>
<color key="backgroundColor" red="0.9583648632595535" green="0.95704415513644048" blue="0.98011363636363635" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="nlc-aR-x6Z" secondAttribute="centerX" constant="-142" id="9Io-LZ-qqV"/>
<constraint firstAttribute="centerX" secondItem="3NY-CL-b0K" secondAttribute="centerX" id="nW7-ez-FLm"/>
<constraint firstAttribute="centerX" secondItem="5jy-Kk-LHY" secondAttribute="centerX" id="z1j-fU-uhY"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="oei-07-Ofu" secondAttribute="trailing" id="0jC-VX-D77"/>
<constraint firstItem="oei-07-Ofu" firstAttribute="top" secondItem="tSZ-kW-ci4" secondAttribute="top" id="Lyx-qs-LMB"/>
<constraint firstItem="oei-07-Ofu" firstAttribute="leading" secondItem="tSZ-kW-ci4" secondAttribute="leading" id="Tvd-fL-4Oe"/>
<constraint firstAttribute="bottom" secondItem="oei-07-Ofu" secondAttribute="bottom" id="h2v-wp-E4q"/>
</constraints>
</scrollView>
</subviews>
<color key="backgroundColor" red="0.94901960784313721" green="0.95294117647058818" blue="0.96470588235294119" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstItem="tSZ-kW-ci4" firstAttribute="top" secondItem="NoT-2j-3pn" secondAttribute="top" id="D6e-VS-qmB"/>
<constraint firstItem="bvv-LD-6PZ" firstAttribute="top" secondItem="tSZ-kW-ci4" secondAttribute="bottom" id="fWl-64-udC"/>
<constraint firstItem="tSZ-kW-ci4" firstAttribute="leading" secondItem="NoT-2j-3pn" secondAttribute="leading" id="mMe-3o-cwT"/>
<constraint firstAttribute="trailing" secondItem="tSZ-kW-ci4" secondAttribute="trailing" id="wd1-d2-uiM"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Settings" id="1Vv-xV-cy7">
<barButtonItem key="rightBarButtonItem" title="Log In" id="aDa-uy-Am0">
<connections>
<segue destination="aoT-U4-pya" kind="push" id="aId-CB-DBW"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="contentView" destination="oei-07-Ofu" id="Qbb-JA-aq5"/>
<outlet property="debugDisplayLabel" destination="Qdv-ND-e9O" id="PYi-G3-odm"/>
<outlet property="debugDisplayTableView" destination="3NY-CL-b0K" id="z3e-zY-PVc"/>
<outlet property="pairWithStudioButton" destination="5jy-Kk-LHY" id="OQp-U5-rVo"/>
<outlet property="pairWithStudioLabel" destination="7XS-Wf-vIe" id="nSv-le-IQd"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="J4Y-ta-CPM" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2756" y="-918"/>
</scene>
<!--Login-->
<scene sceneID="tZ5-KK-tGH">
<objects>
<viewController id="aoT-U4-pya" customClass="TestAccountSigninViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="uRm-JS-TQO"/>
<viewControllerLayoutGuide type="bottom" id="5Fi-8G-Vif"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="duP-2f-f8F">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder=" Username" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="IJQ-Av-wVk">
<rect key="frame" x="35" y="74" width="250" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="G1J-Qb-5Hx"/>
<constraint firstAttribute="width" constant="250" id="ORN-Nw-aNK"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="next"/>
<connections>
<action selector="usernameDidEndOnExit:" destination="aoT-U4-pya" eventType="editingDidEndOnExit" id="KIA-DJ-FZU"/>
</connections>
</textField>
<textField opaque="NO" clipsSubviews="YES" tag="1" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder=" Password" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="OwM-OU-JoF">
<rect key="frame" x="35" y="154" width="250" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="250" id="4Sm-gQ-XWL"/>
<constraint firstAttribute="height" constant="30" id="T6S-Ni-Im9"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="go" secureTextEntry="YES"/>
<connections>
<action selector="passwordDidEndOnExit:" destination="aoT-U4-pya" eventType="editingDidEndOnExit" id="KCO-o3-OCV"/>
</connections>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Apb-bb-TUZ">
<rect key="frame" x="63" y="226" width="194" height="40"/>
<constraints>
<constraint firstAttribute="width" constant="194" id="QOH-E7-UGO"/>
<constraint firstAttribute="height" constant="40" id="dCp-3t-CR7"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="22"/>
<state key="normal" title="Login">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="loginButtonPressed:" destination="aoT-U4-pya" eventType="touchUpInside" id="g2y-2V-6C4"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" misplaced="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="PTL-AT-PHZ">
<rect key="frame" x="63" y="360" width="194" height="40"/>
<fontDescription key="fontDescription" type="system" pointSize="22"/>
<state key="normal" title="Forgot Password?">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<segue destination="kQX-BR-Ebn" kind="push" id="JcM-Ma-RxY"/>
</connections>
</button>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qyi-RP-IEf" userLabel="Line View">
<rect key="frame" x="33" y="110" width="254" height="2"/>
<color key="backgroundColor" red="0.27843137254901962" green="0.27843137254901962" blue="0.27843137254901962" alpha="1" colorSpace="calibratedRGB"/>
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" constant="2" id="SbN-dk-ngk"/>
<constraint firstAttribute="width" constant="254" id="o95-Ou-8vH"/>
</constraints>
</view>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3cf-WT-mjz" userLabel="Line View">
<rect key="frame" x="33" y="187" width="254" height="2"/>
<color key="backgroundColor" red="0.27843137254901962" green="0.27843137254901962" blue="0.27843137254901962" alpha="1" colorSpace="calibratedRGB"/>
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" constant="2" id="8gV-wD-TXI"/>
<constraint firstAttribute="width" constant="254" id="BUq-f6-Zoz"/>
</constraints>
</view>
<view hidden="YES" alpha="0.84999999999999998" contentMode="scaleToFill" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="OW1-vT-vUB">
<rect key="frame" x="0.0" y="0.0" width="320" height="570"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" misplaced="YES" text="Logging in..." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lqU-0u-0LH">
<rect key="frame" x="91" y="300" width="139" height="32"/>
<fontDescription key="fontDescription" type="system" pointSize="23"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3cS-as-T8r">
<rect key="frame" x="148" y="268" width="24" height="24"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="lqU-0u-0LH" secondAttribute="centerX" id="oLE-vf-xto"/>
<constraint firstAttribute="centerX" secondItem="3cS-as-T8r" secondAttribute="centerX" id="wx8-Ia-Fwi"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="OW1-vT-vUB" secondAttribute="centerX" id="6eO-rq-DIu"/>
<constraint firstAttribute="centerX" secondItem="OwM-OU-JoF" secondAttribute="centerX" id="EoY-JM-bwX"/>
<constraint firstItem="Apb-bb-TUZ" firstAttribute="top" secondItem="3cf-WT-mjz" secondAttribute="bottom" constant="37" id="G5k-UB-PLy"/>
<constraint firstAttribute="centerX" secondItem="3cf-WT-mjz" secondAttribute="centerX" id="GAV-xw-20Y"/>
<constraint firstItem="IJQ-Av-wVk" firstAttribute="top" secondItem="uRm-JS-TQO" secondAttribute="bottom" constant="30" id="GoS-Z6-Fhw"/>
<constraint firstItem="3cf-WT-mjz" firstAttribute="top" secondItem="OwM-OU-JoF" secondAttribute="bottom" constant="3" id="HS0-v2-9w6"/>
<constraint firstItem="5Fi-8G-Vif" firstAttribute="top" secondItem="PTL-AT-PHZ" secondAttribute="bottom" constant="5" id="LjQ-iv-3An"/>
<constraint firstItem="OW1-vT-vUB" firstAttribute="leading" secondItem="duP-2f-f8F" secondAttribute="leading" id="TTv-iP-Vso"/>
<constraint firstAttribute="centerX" secondItem="Apb-bb-TUZ" secondAttribute="centerX" id="XGU-fK-oYK"/>
<constraint firstAttribute="bottom" secondItem="OW1-vT-vUB" secondAttribute="bottom" constant="2" id="ZMq-T7-sQp"/>
<constraint firstAttribute="centerX" secondItem="qyi-RP-IEf" secondAttribute="centerX" id="ZrW-nc-hFZ"/>
<constraint firstItem="OW1-vT-vUB" firstAttribute="top" secondItem="duP-2f-f8F" secondAttribute="top" id="dpG-AA-dtJ"/>
<constraint firstAttribute="centerX" secondItem="PTL-AT-PHZ" secondAttribute="centerX" id="gjk-HY-G1J"/>
<constraint firstAttribute="centerX" secondItem="IJQ-Av-wVk" secondAttribute="centerX" id="hLW-Je-15n"/>
<constraint firstAttribute="trailing" secondItem="OW1-vT-vUB" secondAttribute="trailing" id="lsS-Q0-eSv"/>
<constraint firstItem="qyi-RP-IEf" firstAttribute="top" secondItem="IJQ-Av-wVk" secondAttribute="bottom" constant="6" id="rS4-Bw-tht"/>
<constraint firstItem="OwM-OU-JoF" firstAttribute="top" secondItem="qyi-RP-IEf" secondAttribute="bottom" constant="42" id="zb8-Fh-pmS"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Login" id="CSl-Qf-pXv">
<barButtonItem key="rightBarButtonItem" title="Clear" id="luV-Xc-qZF">
<connections>
<action selector="logoutButtonPressed:" destination="aoT-U4-pya" id="mGH-Sq-9dW"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="forgotPasswordButton" destination="PTL-AT-PHZ" id="XDV-sc-HHt"/>
<outlet property="loadingSpinner" destination="3cS-as-T8r" id="fbc-bk-kn8"/>
<outlet property="loggingInView" destination="OW1-vT-vUB" id="XfC-Nk-6dF"/>
<outlet property="loginButton" destination="Apb-bb-TUZ" id="6M0-gm-cTM"/>
<outlet property="password" destination="OwM-OU-JoF" id="vjj-87-3RV"/>
<outlet property="username" destination="IJQ-Av-wVk" id="XRC-tW-PNn"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kLx-yl-OQ6" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3482" y="-918"/>
</scene>
<!--Forgot Password-->
<scene sceneID="EyZ-TC-oKL">
<objects>
<viewController id="kQX-BR-Ebn" customClass="ResetPasswordViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="H0D-in-4eo"/>
<viewControllerLayoutGuide type="bottom" id="gSf-DL-sRh"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="PBI-TI-jeV">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<webView contentMode="scaleToFill" scalesPageToFit="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FP3-eb-M4w">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<connections>
<outlet property="delegate" destination="kQX-BR-Ebn" id="Rq1-gT-Dve"/>
</connections>
</webView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="FP3-eb-M4w" firstAttribute="leading" secondItem="PBI-TI-jeV" secondAttribute="leadingMargin" constant="-16" id="HXw-Xb-8eq"/>
<constraint firstItem="gSf-DL-sRh" firstAttribute="top" secondItem="FP3-eb-M4w" secondAttribute="bottom" id="Ulr-4z-KmN"/>
<constraint firstAttribute="trailingMargin" secondItem="FP3-eb-M4w" secondAttribute="trailing" constant="-16" id="lNn-f1-5hY"/>
<constraint firstItem="FP3-eb-M4w" firstAttribute="top" secondItem="PBI-TI-jeV" secondAttribute="topMargin" id="p7i-ew-wTs"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Forgot Password" id="osN-sB-Qzq">
<barButtonItem key="rightBarButtonItem" image="loading-0.png" width="24" id="CAi-HY-fj4">
<inset key="imageInsets" minX="0.0" minY="24" maxX="0.0" maxY="24"/>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="loadingBarItem" destination="CAi-HY-fj4" id="uyr-K6-ZVa"/>
<outlet property="webView" destination="FP3-eb-M4w" id="Blv-nf-OEb"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lSm-hl-3Gc" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4197" y="-918"/>
</scene>
<!--How to Pair-->
<scene sceneID="xQx-Z9-cyT">
<objects>
<viewController storyboardIdentifier="PairTutorialViewController" id="eF6-dW-4zi" customClass="PairTutorialViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="ebb-l9-XFR"/>
<viewControllerLayoutGuide type="bottom" id="Tzp-6P-6dQ"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="SMK-K1-Wfz">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" usesAttributedText="YES" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="279" translatesAutoresizingMaskIntoConstraints="NO" id="Gzm-wJ-Qp5">
<rect key="frame" x="21" y="150" width="279" height="154"/>
<attributedString key="attributedText">
<fragment content="STEP_INSTRUCTIONS">
<attributes>
<color key="NSColor" cocoaTouchSystemColor="darkTextColor"/>
<font key="NSFont" size="17" name="HelveticaNeue"/>
<paragraphStyle key="NSParagraphStyle" alignment="left" lineBreakMode="wordWrapping" baseWritingDirection="natural"/>
</attributes>
</fragment>
</attributedString>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" misplaced="YES" image="step3.png" translatesAutoresizingMaskIntoConstraints="NO" id="Mh4-fy-ePe">
<rect key="frame" x="12" y="325" width="296" height="218"/>
<constraints>
<constraint firstAttribute="height" constant="218" id="iZl-NI-omW"/>
<constraint firstAttribute="width" constant="296" id="onq-pq-xa3"/>
</constraints>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="STEP_TITLE" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="F8Q-on-LCE">
<rect key="frame" x="20" y="112" width="280" height="36"/>
<constraints>
<constraint firstAttribute="width" constant="280" id="eLQ-OU-Zdc"/>
<constraint firstAttribute="height" constant="36" id="wjQ-cI-4rY"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="Gzm-wJ-Qp5" secondAttribute="trailing" constant="5" id="Alf-hf-R3g"/>
<constraint firstItem="Gzm-wJ-Qp5" firstAttribute="top" secondItem="F8Q-on-LCE" secondAttribute="bottom" constant="12" id="D1C-9X-lNx"/>
<constraint firstAttribute="centerX" secondItem="Gzm-wJ-Qp5" secondAttribute="centerX" id="Rva-gR-SqU"/>
<constraint firstItem="Gzm-wJ-Qp5" firstAttribute="leading" secondItem="SMK-K1-Wfz" secondAttribute="leadingMargin" constant="5" id="XeZ-u8-0uN"/>
<constraint firstAttribute="centerX" secondItem="Mh4-fy-ePe" secondAttribute="centerX" id="YTc-nb-PFY"/>
<constraint firstItem="F8Q-on-LCE" firstAttribute="top" secondItem="ebb-l9-XFR" secondAttribute="bottom" constant="48" id="nOZ-za-WG2"/>
<constraint firstAttribute="centerX" secondItem="F8Q-on-LCE" secondAttribute="centerX" id="sNL-ax-UPI"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="How to Pair" id="7vK-DV-VMr">
<barButtonItem key="backBarButtonItem" title="Back" id="5X5-wS-Vax"/>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="instructionImageView" destination="Mh4-fy-ePe" id="mTA-Ci-AwE"/>
<outlet property="instructionLabel" destination="Gzm-wJ-Qp5" id="2oS-WC-fAH"/>
<outlet property="instructionTitle" destination="F8Q-on-LCE" id="pzF-Gi-aoY"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="auE-5N-8JM" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4312" y="956"/>
</scene>
<!--How to Pair-->
<scene sceneID="FBf-LZ-9fW">
<objects>
<pageViewController storyboardIdentifier="PageViewController" autoresizesArchivedViewToFullSize="NO" transitionStyle="scroll" navigationOrientation="horizontal" spineLocation="none" id="XLh-x5-zdY" sceneMemberID="viewController">
<navigationItem key="navigationItem" title="How to Pair" id="ERL-u6-QvI"/>
</pageViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="3XX-OJ-4vV" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3330" y="956"/>
</scene>
<!--Pair Tutorial Data Controller-->
<scene sceneID="csA-aq-N7P">
<objects>
<viewController storyboardIdentifier="PairTutorialDataController" id="tgZ-d5-GUX" customClass="PairTutorialDataController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="wDr-iI-1HP"/>
<viewControllerLayoutGuide type="bottom" id="0tW-kF-1wA"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="nFG-ML-XfU">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Nrj-cM-YJH" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3829" y="956"/>
</scene>
</scenes>
<resources>
<image name="behind_alert_view.png" width="320" height="460"/>
<image name="forwardChevron.png" width="24" height="24"/>
<image name="loading-0.png" width="40" height="40"/>
<image name="roblox-logo.png" width="1031" height="265"/>
<image name="step3.png" width="346" height="212"/>
</resources>
<simulatedMetricsContainer key="defaultSimulatedMetrics">
<simulatedStatusBarMetrics key="statusBar"/>
<simulatedOrientationMetrics key="orientation"/>
<simulatedScreenMetrics key="destination" type="retina4"/>
</simulatedMetricsContainer>
</document>
@@ -0,0 +1,28 @@
//
// SettingsViewController.h
// RobloxMobile
//
// Created by Ben Tkacheff on 8/18/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface SettingsViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
{
NSArray *debugDisplayTableData;
}
@property (retain, nonatomic) IBOutlet UITableView *debugDisplayTableView;
@property (retain, nonatomic) IBOutlet UILabel *debugDisplayLabel;
@property (retain, nonatomic) IBOutlet UIButton *pairWithStudioButton;
@property (retain, nonatomic) IBOutlet UILabel *pairWithStudioLabel;
@property (retain, nonatomic) IBOutlet UIView *contentView;
- (IBAction) pairWithStudioPressed:(UIButton *)sender;
- (IBAction) loginButtonPressed:(UIBarButtonItem *)sender;
@end
@@ -0,0 +1,168 @@
//
// SettingsViewController.m
// RobloxMobile
//
// Created by Ben Tkacheff on 8/18/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#include <v8datamodel/GuiBuilder.h>
#import "SettingsViewController.h"
#import "StudioPairViewController.h"
#import "UIStyleConverter.h"
#import "TestAccountSigninViewController.h"
#import "RobloxGoogleAnalytics.h"
@interface SettingsViewController ()
@end
@implementation SettingsViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
debugDisplayTableData = [NSArray arrayWithObjects:@"None", @"FPS", @"Summary", @"Physics", @"PhysicsAndOwner", @"Render", nil];
[UIStyleConverter convertToBlueTitleStyle:self.pairWithStudioLabel];
[UIStyleConverter convertToBlueTitleStyle:self.debugDisplayLabel];
[UIStyleConverter convertUIButtonToLabelStyle:self.pairWithStudioButton];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[UIStyleConverter convertToBlueNavigationBarStyle: self.navigationController.navigationBar];
self.debugDisplayTableView.scrollEnabled = NO;
}
else
{
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.contentView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0];
[self.view addConstraint:leftConstraint];
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.contentView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0];
[self.view addConstraint:rightConstraint];
}
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[RobloxGoogleAnalytics setPageViewTracking:@"Settings"];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction) pairWithStudioPressed:(UIButton *)sender
{
StudioPairViewController *pairViewcontroller = (StudioPairViewController*) [self.storyboard instantiateViewControllerWithIdentifier:@"StudioPairViewController"];
pairViewcontroller.shouldTryConnect = NO;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
[self.navigationController pushViewController:pairViewcontroller animated:YES];
}
else
{
UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:pairViewcontroller];
[navigation setModalPresentationStyle:UIModalPresentationFormSheet];
[navigation setModalTransitionStyle:UIModalTransitionStyleCoverVertical];
[self presentViewController:navigation animated:YES completion:nil];
}
}
- (IBAction) loginButtonPressed:(UIBarButtonItem *)sender
{
TestAccountSigninViewController *testAccountSigninViewController = (TestAccountSigninViewController*) [self.storyboard instantiateViewControllerWithIdentifier:@"TestAccountSigninViewController"];
UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:testAccountSigninViewController];
[navigation setModalPresentationStyle:UIModalPresentationFormSheet];
[navigation setModalTransitionStyle:UIModalTransitionStyleCoverVertical];
[self presentViewController:navigation animated:YES completion:nil];
[RobloxGoogleAnalytics setPageViewTracking:@"Settings/LoginButton"];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [debugDisplayTableData count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = @"SimpleTableItem";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier];
}
cell.accessoryView = [[ UIImageView alloc ] initWithImage:[UIImage imageNamed:@"checkmark.png"]];
[cell.accessoryView setFrame:CGRectMake(0, 0, 24, 24)];
const int currentIndex = static_cast<int>(RBX::GuiBuilder::getDebugDisplay());
[cell.accessoryView setHidden:(currentIndex != indexPath.row)];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
[UIStyleConverter convertToLabelStyle:cell.textLabel];
cell.textLabel.text = [debugDisplayTableData objectAtIndex:indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
for (int section = 0; section < [tableView numberOfSections]; section++)
{
for (int row = 0; row < [tableView numberOfRowsInSection:section]; row++)
{
NSIndexPath* cellPath = [NSIndexPath indexPathForRow:row inSection:section];
UITableViewCell* cell = [tableView cellForRowAtIndexPath:cellPath];
[cell.accessoryView setHidden:YES];
}
}
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell)
{
[cell.accessoryView setHidden:NO];
RBX::GuiBuilder::setDebugDisplay(static_cast<RBX::GuiBuilder::Display>(indexPath.row));
[RobloxGoogleAnalytics setCustomVariableWithLabel:@"StatsDisplay" withValue:[NSString stringWithFormat: @"%d", (int)indexPath.row]];
}
else
{
[RobloxGoogleAnalytics setPageViewTracking:@"Settings/StatsDisplay/InvalidCell"];
}
}
@end
@@ -0,0 +1,28 @@
//
// StartupViewController.h
// RobloxMobile
//
// Created by Ben Tkacheff on 12/13/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface StartupViewController : UIViewController
{
int initialLogoConstant;
int initialButtonConstant;
}
@property (retain, nonatomic) IBOutlet UIButton *settingsButton;
@property (retain, nonatomic) IBOutlet UIButton *connectToStudioButton;
@property (retain, nonatomic) IBOutlet UIButton *robloxMobileButton;
@property (retain, nonatomic) IBOutlet UIImageView *robloxLogo;
@property (retain, nonatomic) IBOutlet UILabel *developerLabel;
@property (retain, nonatomic) IBOutlet NSLayoutConstraint *verticalLogoConstraint;
@property (retain, nonatomic) IBOutlet NSLayoutConstraint *verticalButtonConstraint;
- (IBAction) connectButtonPressed:(UIButton *)sender;
- (IBAction) robloxMobileButtonPressed:(UIButton *)sender;
@end
@@ -0,0 +1,192 @@
//
// StartupViewController.m
// RobloxMobile
//
// Created by Ben Tkacheff on 12/13/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "StartupViewController.h"
#import "UserInfo.h"
#import "LoginManager.h"
#import "AppDelegate.h"
#import "UIStyleConverter.h"
#import "StudioPairViewController.h"
#import "StudioConnectionViewController.h"
#import "RobloxGoogleAnalytics.h"
#import "RobloxInfo.h"
@interface StartupViewController ()
@end
@implementation StartupViewController
- (id) initWithCoder:(NSCoder*) decoder
{
self = [super initWithCoder:decoder];
if (self)
{
[UIStyleConverter convertToNavigationBarStyle];
[UIStyleConverter convertToPagingStyle];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[UIStyleConverter convertToTexturedBackgroundStyle:self.view];
[UIStyleConverter convertToButtonStyle:self.connectToStudioButton];
[UIStyleConverter convertToButtonStyle:self.settingsButton];
[UIStyleConverter convertToHyperlinkStyle:self.robloxMobileButton];
[UIStyleConverter convertToLargeLabelStyle:self.developerLabel];
// if we have a saved account, sign in with it on startup
if(![[UserInfo CurrentPlayer].username isEqual: @""] && ![[UserInfo CurrentPlayer].password isEqual: @""])
{
[[LoginManager sharedInstance] loginWithUsername:[UserInfo CurrentPlayer].username password:[UserInfo CurrentPlayer].password completionBlock:nil];
}
[RobloxGoogleAnalytics setEventTracking:@"DeviceType" withAction:[RobloxInfo friendlyDeviceName] withLabel:[RobloxInfo deviceOSVersion] withValue:0];
initialLogoConstant = self.verticalLogoConstraint.constant;
initialButtonConstant = self.verticalButtonConstraint.constant;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:YES animated:YES];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
[[UIApplication sharedApplication] setStatusBarHidden:NO];
[self setConstraints:UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication]statusBarOrientation])];
}
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
-(void) viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
}
- (void)viewWillDisappear:(BOOL)animated
{
[self.navigationController setNavigationBarHidden:NO animated:animated];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
[super viewWillDisappear:animated];
}
- (IBAction) connectButtonPressed:(UIButton *)sender
{
NSString* storedCode = [[NSUserDefaults standardUserDefaults] stringForKey:@"RbxPairCode"];
if(!storedCode || storedCode.length <= 0)
{
StudioPairViewController *pairViewcontroller = (StudioPairViewController*) [self.storyboard instantiateViewControllerWithIdentifier:@"StudioPairViewController"];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
[self.navigationController pushViewController:pairViewcontroller animated:YES];
}
else
{
UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:pairViewcontroller];
[navigation setModalPresentationStyle:UIModalPresentationFormSheet];
[navigation setModalTransitionStyle:UIModalTransitionStyleCoverVertical];
[self presentViewController:navigation animated:YES completion:nil];
}
}
else
{
StudioConnectionViewController *connectionViewController = (StudioConnectionViewController*) [self.storyboard instantiateViewControllerWithIdentifier:@"StudioConnectionViewController"];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
[self.navigationController pushViewController:connectionViewController animated:YES];
}
else
{
UINavigationController* navigation = [[UINavigationController alloc] initWithRootViewController:connectionViewController];
[navigation setModalPresentationStyle:UIModalPresentationFormSheet];
[navigation setModalTransitionStyle:UIModalTransitionStyleCoverVertical];
[self presentViewController:navigation animated:YES completion:^{}];
}
}
}
- (IBAction) robloxMobileButtonPressed:(UIButton *)sender
{
[RobloxGoogleAnalytics setPageViewTracking:@"StartupView/RobloxMobileButton"];
NSString *iTunesLink = @"https://itunes.apple.com/us/app/roblox-mobile/id431946152?mt=8";
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:iTunesLink]];
}
- (void) setConstraints:(BOOL) isLandscape
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
if ( isLandscape )
{
self.verticalLogoConstraint.constant = 20;
self.verticalButtonConstraint.constant = 20;
}
else
{
self.verticalLogoConstraint.constant = initialLogoConstant;
self.verticalButtonConstraint.constant = initialButtonConstant;
}
}
}
// for iOS 7.0 and below
-(void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:orientation duration:duration];
[self setConstraints:UIInterfaceOrientationIsLandscape(orientation)];
}
// for iOS 8.0 and above
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[self setConstraints:(size.width > size.height)];
}
-(NSUInteger)supportedInterfaceOrientations
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
return UIInterfaceOrientationMaskAll;
else
return UIInterfaceOrientationMaskLandscape;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
return (interfaceOrientation==UIInterfaceOrientationPortrait) || (interfaceOrientation==UIInterfaceOrientationPortraitUpsideDown) ||
(interfaceOrientation==UIInterfaceOrientationLandscapeLeft) || (interfaceOrientation==UIInterfaceOrientationLandscapeRight);
else
return (interfaceOrientation==UIInterfaceOrientationLandscapeLeft) || (interfaceOrientation==UIInterfaceOrientationLandscapeRight);
}
@end
@@ -0,0 +1,45 @@
//
// StudioConnectionViewController.h
// RobloxMobile
//
// Created by Ben Tkacheff on 12/13/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "TcpViewController.h"
#import "rbx/signal.h"
typedef enum ConnectionState
{
STATE_NONE, // Don't try to connect to anything
STATE_CONNECTSERVER, // Try to pair to server to get ip address
STATE_LAUNCHGAME // Use ip address to connect to game server
} ConnectionState;
typedef enum CoreScriptRequestState
{
REQUEST_CORESCRIPT_EXISTS, // see if server has any core scripts for us
REQUEST_CORESCRIPT_NEXT_STREAM, // Tell server to send core scripts in next request
REQUEST_CORESCRIPT_WAITING_RESPONSE, // Waiting for server to tell us something
REQUEST_CORESCRIPT_EXPECT_NOW, // Server is now sending over core scripts
REQUEST_OVER // no more corescript work
} CoreScriptRequestState;
@interface StudioConnectionViewController : TcpViewController
{
ConnectionState connectionState;
BOOL isVisible;
CoreScriptRequestState coreScriptRequestState;
NSMutableString* allCoreScriptsData;
}
@property (retain, nonatomic) IBOutlet UILabel *connectingLabel;
@property (retain, nonatomic) IBOutlet UIButton *connectToStudioButton;
@property (retain, nonatomic) IBOutlet UIImageView *loadingSpinner;
- (IBAction) connectToStudioForPlaySession:(UIButton *)sender;
- (IBAction) closeButtonPressed:(UIBarButtonItem *)sender;
@end
@@ -0,0 +1,565 @@
//
// StudioConnectionViewController.m
// RobloxMobile
//
// Created by Ben Tkacheff on 12/13/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "StudioConnectionViewController.h"
#import "PlaceLauncher.h"
#import "StudioConnector.h"
#import "MainViewController.h"
#import "ObjectiveCUtilities.h"
#import "GameViewController.h"
#import "UIStyleConverter.h"
#import "RobloxInfo.h"
#import "UserInfo.h"
#import "RobloxNotifications.h"
#include "script/ScriptContext.h"
#define CORE_SCRIPT_BUFFER_SIZE_BYTES 2000000
#define CORE_SCRIPT_END_TAG @"RbxEnd"
#define CORE_SCRIPT_SOURCE_TAG @"RbxScriptSource"
#define CORE_SCRIPT_NAME_TAG @"RbxScriptName"
#define CORE_SCRIPT_DONE_TAG @"RbxCoreScriptEnd"
@interface StudioConnectionViewController ()
@end
@implementation StudioConnectionViewController
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
connectionState = STATE_NONE;
isVisible = NO;
}
return self;
}
- (void) dealloc
{
[self stopConnection];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad
{
[super viewDidLoad];
connectionState = STATE_CONNECTSERVER;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(gotDidLeaveGameNotification:)
name:RBX_NOTIFY_GAME_DID_LEAVE
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(gotStartLeaveGameNotification:)
name:RBX_NOTIFY_GAME_START_LEAVING
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(pairingDidEnd:)
name:[[StudioConnector sharedInstance] getPairEndedNotificationString]
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[UIStyleConverter convertToLabelStyle:self.connectingLabel];
[UIStyleConverter convertToLoadingStyle:self.loadingSpinner];
[UIStyleConverter convertToButtonBlueStyle:self.connectToStudioButton];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[UIStyleConverter convertToNavigationBarStyle];
}
self.connectingLabel.text = NSLocalizedString(@"RbxDevWaiting", nil);
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
RBX::ScriptContext::setAdminScriptPath("");
allCoreScriptsData = nil;
coreScriptRequestState = REQUEST_CORESCRIPT_EXISTS;
}
-(void) stopConnection
{
[self tryToDestroyStreams];
[[StudioConnector sharedInstance] stopPairing];
}
-(void) startConnection
{
if (connectionState == STATE_CONNECTSERVER)
{
[[StudioConnector sharedInstance] tryToPairWithStudioUsingCurrentCode];
}
if (connectionState == STATE_CONNECTSERVER || connectionState == STATE_LAUNCHGAME)
{
self.connectingLabel.text = NSLocalizedString(@"RbxDevWaiting", nil);
}
else
{
[self.connectToStudioButton setHidden:NO];
[self.loadingSpinner setHidden:YES];
self.connectingLabel.text = NSLocalizedString(@"RbxDevPressConnect", nil);
}
}
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[UIViewController attemptRotationToDeviceOrientation];
[self stopConnection];
[self startConnection];
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
isVisible = YES;
}
-(void) viewDidDisappear:(BOOL)animated
{
[[UIApplication sharedApplication] setIdleTimerDisabled:NO];
isVisible = NO;
[super viewDidDisappear:animated];
}
-(void) appDidBecomeActive:(NSNotification *) aNotification
{
if (isVisible)
{
[self startConnection];
}
}
-(void) appDidEnterBackground:(NSNotification *) aNotification
{
if (isVisible)
{
[self stopConnection];
}
}
-(void) pairingDidEnd:(NSNotification *) aNotification
{
NSString* didPair = [[aNotification userInfo] objectForKey:@"didPair"];
if ( [didPair isEqualToString:@"true"] )
{
NSString* ipAddr = [[aNotification userInfo] objectForKey:@"ipString"];
dispatch_async(dispatch_get_main_queue(), ^{
self.connectingLabel.text = NSLocalizedString(@"RbxDevWaiting", nil);
});
if (connectionState == STATE_CONNECTSERVER)
{
if (![self listenForActions:ipAddr])
{
[[StudioConnector sharedInstance] tryToPairWithStudioUsingCurrentCode];
}
}
}
}
- (IBAction) connectToStudioForPlaySession:(UIButton *)sender
{
connectionState = STATE_CONNECTSERVER;
[self.connectToStudioButton setHidden:YES];
[self.loadingSpinner setHidden:false];
[[StudioConnector sharedInstance] tryToPairWithStudioUsingCurrentCode];
}
- (IBAction) closeButtonPressed:(UIBarButtonItem *)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void) gotStartLeaveGameNotification:(NSNotification *)aNotification
{
NSString* userRequestedLeave = [[aNotification userInfo] objectForKey:@"UserRequestedLeave"];
if ( userRequestedLeave && [userRequestedLeave isEqualToString:@"TRUE"] )
{
connectionState = STATE_NONE;
}
}
- (void) gotDidLeaveGameNotification:(NSNotification *)aNotification
{
NSString* userRequestedLeave = [[aNotification userInfo] objectForKey:@"UserRequestedLeave"];
if ( userRequestedLeave && [userRequestedLeave isEqualToString:@"TRUE"] )
{
dispatch_async(dispatch_get_main_queue(), ^{
self.connectingLabel.text = NSLocalizedString(@"RbxDevPressConnect", nil);
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
self.connectingLabel.text = NSLocalizedString(@"RbxDevWaiting", nil);
});
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(void) checkForGameShutdownSignal:(NSArray*) words
{
if ( [words count] == 1 && [words[0] isEqualToString:@"!exit"])
{
if ([[PlaceLauncher sharedInstance] getIsCurrentlyPlayingGame])
[[PlaceLauncher sharedInstance] leaveGame];
[self resetStreamsOnMainThread];
connectionState = STATE_CONNECTSERVER;
}
}
-(int) getLoggedInUserId
{
NSNumber* currentUserId = [UserInfo CurrentPlayer].userId;
if (!currentUserId)
{
return 0;
}
if (currentUserId <= 0)
{
return 0;
}
return [currentUserId intValue];
}
-(void) playerAdded:(shared_ptr<RBX::Instance>) newPlayer
{
int userId = [self getLoggedInUserId];
if (userId > 0)
{
if (shared_ptr<RBX::Game> game = [[PlaceLauncher sharedInstance] getCurrentGame])
{
if(RBX::Network::Players* players = game->getDataModel()->create<RBX::Network::Players>())
{
if(RBX::Network::Player* localPlayer = players->getLocalPlayer())
{
if ( RBX::Network::Player* rawNewPlayer = RBX::Instance::fastDynamicCast<RBX::Network::Player>(newPlayer.get()) )
{
if (localPlayer == rawNewPlayer)
{
RBX::Security::Impersonator impersonate(RBX::Security::WebService);
rawNewPlayer->setUserId(userId);
}
}
}
}
}
}
}
-(BOOL) checkForCoreScriptSubstitutionSignal:(NSArray*) words
{
if ( [words count] == 2)
{
if( [words[0] isEqualToString:@"RbxReadyForCoreScripts"] )
{
return ([words[1] isEqualToString:@"true"]);
}
}
return NO;
}
-(void) checkForGameLaunchSignal:(NSArray*) words
{
if ( [words count] == 3 && [words[0] isEqualToString:@"RbxReadyForPlay"])
{
NSString* port = words[1];
NSString* ip = words[2];
if([port length] > 0 && [ip length] > 0 )
{
dispatch_async(dispatch_get_main_queue(), ^{
self.connectingLabel.text = NSLocalizedString(@"RbxDevFinalizing", nil);
[[UIApplication sharedApplication] setStatusBarHidden:YES];
[[PlaceLauncher sharedInstance] startGameLocal:[port intValue] ipAddress:ip controller:self presentGameAutomatically:YES userId:0];
shared_ptr<RBX::Game> game = [[PlaceLauncher sharedInstance] getCurrentGame];
connectionState = STATE_CONNECTSERVER;
if (RBX::Network::Players* players = game->getDataModel()->create<RBX::Network::Players>())
{
BOOL setUserId = NO;
int userId = [self getLoggedInUserId];
if (userId > 0 && players->numChildren() > 0)
{
if(RBX::Network::Player* player = players->getLocalPlayer())
{
RBX::Security::Impersonator impersonate(RBX::Security::WebService);
player->setUserId(userId);
setUserId = YES;
}
}
if (!setUserId)
{
players->onDemandWrite()->childAddedSignal.connect( boostFuncFromSelector_1< shared_ptr<RBX::Instance> >(@selector(playerAdded:), self) );
}
}
});
}
}
}
-(BOOL)directoryAlreadyExists:(NSString *)directoryName Name:(NSString *)name
{
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSString *path = [directoryName stringByAppendingPathComponent:name];
return [fileManager fileExistsAtPath:path];
}
-(void)createDirectory:(NSString *)directoryName atFilePath:(NSString *)filePath
{
if ([self directoryAlreadyExists:filePath Name:directoryName])
{
return;
}
NSString *filePathAndDirectory = [filePath stringByAppendingPathComponent:directoryName];
NSError *error;
if (![[NSFileManager defaultManager] createDirectoryAtPath:filePathAndDirectory
withIntermediateDirectories:NO
attributes:nil
error:&error])
{
NSLog(@"Create directory error: %@", error);
}
}
-(void) createFile:(NSString*) filePath fileData:(NSString*) fileData
{
NSString* fileDirectory = [filePath stringByDeletingLastPathComponent];
if ( ![[NSFileManager defaultManager] fileExistsAtPath:fileDirectory] )
{
NSError *dirError;
if (![[NSFileManager defaultManager] createDirectoryAtPath:fileDirectory
withIntermediateDirectories:NO
attributes:nil
error:&dirError])
{
NSLog(@"Create directory error: %@", dirError);
}
}
NSError *error;
if ( ![fileData writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error])
{
NSLog(@"Write to file error: %@", error);
}
}
-(void) convertStreamToCoreScripts
{
@synchronized(allCoreScriptsData)
{
if ([inputStream hasBytesAvailable])
{
uint8_t *buffer = NULL;
buffer = (uint8_t *) calloc(CORE_SCRIPT_BUFFER_SIZE_BYTES, sizeof(uint8_t));
int len = [inputStream read:buffer maxLength:(sizeof(uint8_t) * CORE_SCRIPT_BUFFER_SIZE_BYTES)];
if (len > 0)
{
NSString *output = [[NSString alloc] initWithBytes:buffer length:len encoding:NSASCIIStringEncoding];
if (allCoreScriptsData == nil)
{
allCoreScriptsData = [[NSMutableString alloc] initWithString:output];
}
else if (output != nil)
{
[allCoreScriptsData appendString:[output copy]];
}
}
free(buffer);
}
if (allCoreScriptsData && ([allCoreScriptsData rangeOfString:CORE_SCRIPT_DONE_TAG].location != NSNotFound))
{
NSMutableDictionary* coreScriptDictionary = [[NSMutableDictionary alloc] init];
NSArray *scripts = [allCoreScriptsData componentsSeparatedByString:CORE_SCRIPT_NAME_TAG];
for (NSString* __strong script in scripts)
{
script = [script stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]];
NSRange scriptNameRange = [script rangeOfString:CORE_SCRIPT_END_TAG];
if (scriptNameRange.location != NSNotFound)
{
NSString* scriptName = [script substringWithRange:NSMakeRange(0,scriptNameRange.location)];
BOOL hasDoneTag = ([script rangeOfString:CORE_SCRIPT_DONE_TAG].location != NSNotFound);
NSRange scriptSourceStartRange = [script rangeOfString:CORE_SCRIPT_SOURCE_TAG];
int endOffset = 0;
if (hasDoneTag)
{
endOffset = [CORE_SCRIPT_DONE_TAG length] + [CORE_SCRIPT_END_TAG length] + 1;
}
else
{
endOffset = [CORE_SCRIPT_END_TAG length];
}
NSString* source = [script substringWithRange:NSMakeRange(scriptSourceStartRange.location + [CORE_SCRIPT_SOURCE_TAG length],
[script length] - endOffset - [CORE_SCRIPT_SOURCE_TAG length] - scriptSourceStartRange.location)];
[coreScriptDictionary setObject:source forKey:scriptName];
}
}
allCoreScriptsData = nil;
NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
[self createDirectory:@"CoreScripts" atFilePath:documentsDirectory];
for (NSString* __strong scriptName in coreScriptDictionary)
{
NSString* scriptSource = [coreScriptDictionary objectForKey:scriptName];
scriptName = [scriptName stringByAppendingString:@".lua"];
// replace any windows style directory stuff with more unix friendly version
scriptName = [scriptName stringByReplacingOccurrencesOfString:@"\\" withString:@"/"];
[self createFile:[documentsDirectory stringByAppendingPathComponent:scriptName] fileData:scriptSource];
}
RBX::ScriptContext::setAdminScriptPath([documentsDirectory UTF8String]);
coreScriptRequestState = REQUEST_OVER;
}
}
}
- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent
{
switch (streamEvent)
{
case NSStreamEventOpenCompleted:
{
dispatch_async(dispatch_get_main_queue(), ^{
self.connectingLabel.text = NSLocalizedString(@"RbxDevStudioFound", nil);
});
break;
}
case NSStreamEventHasSpaceAvailable:
{
if(theStream == outputStream && (connectionState != STATE_LAUNCHGAME) && ![[PlaceLauncher sharedInstance] getIsCurrentlyPlayingGame])
{
dispatch_async(dispatch_get_main_queue(), ^{
self.connectingLabel.text = NSLocalizedString(@"RbxDevConnectionCreated", nil);
});
if (coreScriptRequestState == REQUEST_CORESCRIPT_EXISTS)
{
coreScriptRequestState = REQUEST_CORESCRIPT_WAITING_RESPONSE;
[self writeToOutputStream:@"RbxReadyForCoreScripts"];
}
else if (coreScriptRequestState == REQUEST_CORESCRIPT_NEXT_STREAM)
{
coreScriptRequestState = REQUEST_CORESCRIPT_EXPECT_NOW;
}
}
break;
}
case NSStreamEventHasBytesAvailable:
if (theStream == inputStream)
{
if (coreScriptRequestState == REQUEST_CORESCRIPT_EXPECT_NOW)
{
[self convertStreamToCoreScripts];
if (coreScriptRequestState == REQUEST_OVER)
{
connectionState = STATE_LAUNCHGAME;
[self writeToOutputStream:@"RbxReadyForPlay"];
}
}
else
{
NSArray* words = [self convertStreamToArray];
if(words)
{
if ( coreScriptRequestState == REQUEST_CORESCRIPT_WAITING_RESPONSE )
{
if ([self checkForCoreScriptSubstitutionSignal:words])
{
coreScriptRequestState = REQUEST_CORESCRIPT_NEXT_STREAM;
[self writeToOutputStream:@"RbxSendCoreScripts"];
}
else
{
coreScriptRequestState = REQUEST_OVER;
connectionState = STATE_LAUNCHGAME;
[self writeToOutputStream:@"RbxReadyForPlay"];
}
}
else if (coreScriptRequestState == REQUEST_OVER)
{
if ( ![[PlaceLauncher sharedInstance] getIsCurrentlyPlayingGame] )
{
[self checkForGameLaunchSignal:words];
}
else
{
[self checkForGameShutdownSignal:words];
}
}
}
}
}
break;
case NSStreamEventEndEncountered:
if ([[PlaceLauncher sharedInstance] getIsCurrentlyPlayingGame])
[[PlaceLauncher sharedInstance] leaveGame];
break;
case NSStreamEventErrorOccurred:
[self resetStreams];
connectionState = STATE_NONE;
break;
default:
break;
}
}
@end
+33
View File
@@ -0,0 +1,33 @@
//
// StudioConnector.h
// RobloxMobile
//
// Created by Ben Tkacheff on 12/19/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "GCDAsyncUdpSocket.h"
@interface StudioConnector : NSObject <GCDAsyncUdpSocketDelegate>
{
bool hasPaired;
NSString* codeToUse;
GCDAsyncUdpSocket *udpSocket;
NSData* hostIPAddress;
NSString* hostIPAddressString;
NSString* didPairToHost;
}
+ (id)sharedInstance;
-(void) tryToPairWithStudioUsingCurrentCode;
-(void) tryToPairWithStudio:(NSString*) newCode;
-(void) stopPairing;
-(void) clearPairCode;
-(NSString*) getPairEndedNotificationString;
- (NSString *)hostnameForAddress:(NSString *)address;
@end
+320
View File
@@ -0,0 +1,320 @@
//
// StudioConnector.m
// RobloxMobile
//
// Created by Ben Tkacheff on 12/19/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "StudioConnector.h"
#import <CFNetwork/CFNetwork.h>
#import <netdb.h>
#define RBX_DEV_PORT 1313
@implementation StudioConnector
-(id) init
{
if (self = [super init])
{
codeToUse = [[NSUserDefaults standardUserDefaults] stringForKey:@"RbxPairCode"];
hasPaired = NO;
udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return self;
}
+ (id)sharedInstance
{
static dispatch_once_t studioConnectorPred = 0;
__strong static id _sharedObject = nil;
dispatch_once(&studioConnectorPred, ^{ // Need to use GCD for thread-safe allocation
_sharedObject = [[self alloc] init];
});
return _sharedObject;
}
-(NSString*) getPairEndedNotificationString
{
return @"RbxDevPairEndedNotification";
}
-(void) tryToPairWithStudioUsingCurrentCode
{
codeToUse = [[NSUserDefaults standardUserDefaults] stringForKey:@"RbxPairCode"];
if(!codeToUse)
codeToUse = @"";
[self tryToPairWithStudio:codeToUse];
}
-(void) tryToPairWithStudio:(NSString*) newCode
{
codeToUse = newCode;
[self doPairWithStudio];
}
-(void) checkIfPaired
{
if (!hasPaired)
{
NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:@"",@"ipData",@"",@"ipString",@"false",@"didPair",@"timeout",@"errorReason", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:[self getPairEndedNotificationString] object:self userInfo:dict];
return;
}
}
-(void) doPairWithStudio
{
hasPaired = NO;
if (!udpSocket)
{
udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
NSError* bindErr;
[udpSocket bindToPort:RBX_DEV_PORT error:&bindErr];
if (bindErr)
{
[self stopPairing];
NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:@"",@"ipData",@"",@"ipString",false,@"didPair", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:[self getPairEndedNotificationString] object:self userInfo:dict];
return;
}
NSError* err;
[udpSocket beginReceiving:&err];
if(err)
{
[self stopPairing];
NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:@"",@"ipData",@"",@"ipString",false,@"didPair", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:[self getPairEndedNotificationString] object:self userInfo:dict];
return;
}
}
-(void) stopPairing
{
[udpSocket close];
udpSocket = nil;
dispatch_async(dispatch_get_main_queue(), ^{
hasPaired = NO;
});
}
- (NSString *)hostnameForAddress:(NSString *)address
{
int error;
struct addrinfo *results = NULL;
error = getaddrinfo([address cStringUsingEncoding:NSUTF8StringEncoding], NULL, NULL, &results);
if (error != 0)
{
return @"";
}
for (struct addrinfo *r = results; r; r = r->ai_next)
{
char hostname[NI_MAXHOST] = {0};
error = getnameinfo(r->ai_addr, r->ai_addrlen, hostname, sizeof hostname, NULL, 0 , 0);
if (error != 0)
{
continue; // try next one
}
else
{
return [NSString stringWithCString:hostname encoding:NSUTF8StringEncoding];
}
}
return @"";
}
-(bool) sendPairCodeToServer:(NSString*) messageFromServer serverAddress:(NSString*) address serverPort:(unsigned short) port
{
if ([messageFromServer isEqualToString:@"RbxDevPairServer readyToPair"])
{
if(!codeToUse)
{
NSData *d = [@"RbxDevClient didPair false" dataUsingEncoding:NSUTF8StringEncoding];
[udpSocket sendData:d toHost:address port:port withTimeout:-1 tag:101];
return false;
}
NSString* pairString = [@"RbxDevClient pairCode " stringByAppendingString:codeToUse];
NSData *d = [pairString dataUsingEncoding:NSUTF8StringEncoding];
[udpSocket sendData:d toHost:address port:port withTimeout:-1 tag:11];
return true;
}
return false;
}
-(void) clearPairCode
{
[[NSUserDefaults standardUserDefaults] setObject:@"" forKey:@"RbxPairCode"];
}
-(bool) checkPairStatusFromServer:(NSString*) messageFromServer addressData:(NSData*) data serverAddress:(NSString*) address serverPort:(unsigned short) port
{
NSRange isRange = [messageFromServer rangeOfString:@"RbxDevPairServer didPair" options:NSCaseInsensitiveSearch];
if(isRange.location != NSNotFound)
{
NSMutableArray *array = (NSMutableArray *)[messageFromServer componentsSeparatedByString:@" "];
[array removeObject:@""];
if ([array count] == 5)
{
NSString* didPair = array[2];
hasPaired = YES;
if ([didPair isEqualToString:@"true"])
{
[[NSUserDefaults standardUserDefaults] setObject:codeToUse forKey:@"RbxPairCode"];
hostIPAddressString = array[4];
hostIPAddress = data;
didPairToHost = @"true";
NSData *d = [@"RbxDevClient didPair true" dataUsingEncoding:NSUTF8StringEncoding];
[udpSocket sendData:d toHost:address port:port withTimeout:-1 tag:101];
}
else
{
hostIPAddressString = @"";
hostIPAddress = nil;
didPairToHost = @"false";
NSData *d = [@"RbxDevClient didPair false" dataUsingEncoding:NSUTF8StringEncoding];
[udpSocket sendData:d toHost:address port:port withTimeout:-1 tag:101];
}
return true;
}
}
return false;
}
-(bool) checkGameStartFromServer:(NSString*) messageFromServer serverAddress:(NSString*) address serverPort:(unsigned short) port
{
NSRange isRange = [messageFromServer rangeOfString:@"RbxDevPairServer didStartServer" options:NSCaseInsensitiveSearch];
if(isRange.location != NSNotFound)
{
hasPaired = YES;
NSData* d = [@"RbxDevClient didConnectToServer" dataUsingEncoding:NSUTF8StringEncoding];
[udpSocket sendData:d toHost:address port:port withTimeout:-1 tag:101];
return true;
}
return false;
}
/**
* By design, UDP is a connectionless protocol, and connecting is not needed.
* However, you may optionally choose to connect to a particular host for reasons
* outlined in the documentation for the various connect methods listed above.
*
* This method is called if one of the connect methods are invoked, and the connection is successful.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address
{
}
/**
* By design, UDP is a connectionless protocol, and connecting is not needed.
* However, you may optionally choose to connect to a particular host for reasons
* outlined in the documentation for the various connect methods listed above.
*
* This method is called if one of the connect methods are invoked, and the connection fails.
* This may happen, for example, if a domain name is given for the host and the domain name is unable to be resolved.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError *)error
{
}
/**
* Called when the datagram with the given tag has been sent.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag
{
if (tag == 101)
{
// if we actually paired somewhere, stop the pairing, otherwise keep going
if ([didPairToHost isEqualToString:@"true"])
{
[self stopPairing];
}
NSDictionary* dict = [[NSDictionary alloc] initWithObjectsAndKeys:hostIPAddress,@"ipData",hostIPAddressString,@"ipString",didPairToHost,@"didPair", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:[self getPairEndedNotificationString] object:self userInfo:dict];
}
}
/**
* Called if an error occurs while trying to send a datagram.
* This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError *)error
{
}
/**
* Called when the socket has received the requested datagram.
**/
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
fromAddress:(NSData *)address
withFilterContext:(id)filterContext
{
if (hasPaired)
{
return;
}
NSString* message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSString *host = [GCDAsyncUdpSocket hostFromAddress:address];
NSArray* ipComponents = [host componentsSeparatedByString:@":"];
NSString* ipv4Address = [ipComponents lastObject];
const unsigned short port = [GCDAsyncUdpSocket portFromAddress:address];
// 1) check the broadcast we got from server, see if we should send code
if( [self sendPairCodeToServer:message serverAddress:ipv4Address serverPort:port] )
{
return;
}
// 2) check if our pair code was verified from server
if ( [self checkPairStatusFromServer:message addressData:address serverAddress:ipv4Address serverPort:port])
{
return;
}
// 3) check if server spun up a game instance we should connect to
if ( [self checkGameStartFromServer:message serverAddress:ipv4Address serverPort:port] )
{
return;
}
}
/**
* Called when the socket is closed.
**/
- (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError *)error
{
}
@end
@@ -0,0 +1,40 @@
//
// StudioPairViewController.h
// RobloxMobile
//
// Created by Ben Tkacheff on 12/13/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "TcpViewController.h"
@interface StudioPairViewController : UIViewController <UITextFieldDelegate, UIAlertViewDelegate>
{
BOOL shouldTryConnect;
BOOL isVisible;
}
@property (nonatomic) BOOL shouldTryConnect;
@property (retain, nonatomic) IBOutlet UIButton *pairToStudioHelpButton;
@property (retain, nonatomic) IBOutlet UIButton *pairToStudioButton;
@property (retain, nonatomic) IBOutlet UILabel *pairingLabel;
@property (retain, nonatomic) IBOutlet UIButton *enterPairCodeButton;
@property (retain, nonatomic) IBOutlet UITextField *codeField;
@property (retain, nonatomic) IBOutlet UIImageView *loadingSpinner;
@property (retain, nonatomic) IBOutlet UIImageView *pairingVignette;
@property (retain, nonatomic) IBOutlet UIBarButtonItem *clearButton;
@property (retain, nonatomic) IBOutlet NSLayoutConstraint *topMostConstraint;
@property (retain, nonatomic) IBOutlet NSLayoutConstraint *devCodeConstraint;
@property (retain, nonatomic) IBOutlet NSLayoutConstraint *pairButtonConstraint;
- (IBAction) clearPairCode:(UIBarButtonItem *)sender;
- (IBAction) pairWithStudio:(UIButton *)sender;
- (IBAction) codeFieldDidEndOnExit:(UITextField *)sender;
- (IBAction) devCodeAreaPressed:(UIButton *) sender;
- (IBAction) helpButtonPressed:(UIButton *) sender;
- (IBAction) closeButtonPressed:(UIButton *)sender;
@end
@@ -0,0 +1,394 @@
//
// StudioPairViewController.m
// RobloxMobile
//
// Created by Ben Tkacheff on 12/13/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "StudioPairViewController.h"
#import "StudioConnector.h"
#import "RobloxAlert.h"
#import "UIStyleConverter.h"
#import "PairTutorialDataController.h"
#import "StudioConnectionViewController.h"
#import "RobloxGoogleAnalytics.h"
#import "UIScreen+PortraitBounds.h"
#define PAIR_BUTTON_OFFSET 150
#define PAIR_BUTTON_ANIMATION_TIME 0.2
@interface StudioPairViewController ()
@end
@implementation StudioPairViewController
@synthesize shouldTryConnect;
- (id) initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
shouldTryConnect = YES;
isVisible = NO;
}
return self;
}
-(void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.translatesAutoresizingMaskIntoConstraints = YES;
[self.codeField setDelegate:self];
[self localizeStrings];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(pairingDidEnd:)
name:[[StudioConnector sharedInstance] getPairEndedNotificationString]
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[UIStyleConverter convertToButtonBlueStyle:self.pairToStudioButton];
[UIStyleConverter convertToBorderlessButtonStyle:self.pairToStudioHelpButton];
[UIStyleConverter convertToLoadingStyle:self.loadingSpinner];
[UIStyleConverter convertToTextFieldStyle:self.codeField];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[UIStyleConverter convertToNavigationBarStyle];
}
NSString* storedCode = [[NSUserDefaults standardUserDefaults] stringForKey:@"RbxPairCode"];
if(storedCode && storedCode.length > 0)
{
[self.codeField setText:storedCode];
[self.pairToStudioButton setAlpha:1];
[self.pairToStudioButton setEnabled:YES];
}
else
{
[self.pairToStudioButton setAlpha:0.5];
[self.pairToStudioButton setEnabled:NO];
}
// Setup pairing text to be bold for part of it
UIFont *regularFont = [UIFont fontWithName:@"SourceSansPro-Regular" size:18.0f];
UIFont *boldFont = [UIFont fontWithName:@"SourceSansPro-Semibold" size:18.0f];
UIColor *foregroundColor = [UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1];
// Create the attributes
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
regularFont, NSFontAttributeName,
foregroundColor, NSForegroundColorAttributeName, nil];
NSDictionary *subAttrs = [NSDictionary dictionaryWithObjectsAndKeys:
boldFont, NSFontAttributeName, nil];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
self.pairingLabel.text = [self.pairingLabel.text stringByReplacingOccurrencesOfString:@"\n" withString:@""];
}
NSString* textTime = self.pairingLabel.text;
const NSRange range = [textTime rangeOfString:@"ROBLOX Dev Code"];
// Create the attributed string (text + attributes)
NSMutableAttributedString *attributedText =
[[NSMutableAttributedString alloc] initWithString:self.pairingLabel.text
attributes:attrs];
[attributedText setAttributes:subAttrs range:range];
[self.pairingLabel setAttributedText:attributedText];
self.enterPairCodeButton.titleLabel.font = [UIFont fontWithName:@"SourceSansPro-Semibold" size:18.0f];
[self.enterPairCodeButton setTitleColor:[UIColor colorWithRed:71.0f/255.0f green:71.0f/255.0f blue:71.0f/255.0f alpha:1.0f] forState:UIControlStateNormal];
self.enterPairCodeButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
CGRect rect = [[UIScreen mainScreen] portraitBounds];
if (rect.size.height > 480)
{
self.topMostConstraint.constant += 30;
self.pairButtonConstraint.constant += 15;
self.devCodeConstraint.constant += 20;
}
}
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[RobloxGoogleAnalytics setPageViewTracking:@"Pair"];
NSString* storedCode = [[NSUserDefaults standardUserDefaults] stringForKey:@"RbxPairCode"];
if(storedCode && storedCode.length > 0)
{
[self.codeField setText:storedCode];
}
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
}
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
isVisible = YES;
}
-(void) viewDidDisappear:(BOOL)animated
{
[[UIApplication sharedApplication] setIdleTimerDisabled:NO];
isVisible = NO;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super viewDidDisappear:animated];
}
-(void) appDidBecomeActive:(NSNotification *) aNotification
{
if (isVisible)
{
// todo: maybe we need that
}
}
-(void) appDidEnterBackground:(NSNotification *) aNotification
{
if (isVisible)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self stopPairing];
});
}
}
-(void) localizeStrings
{
[self.codeField setPlaceholder:NSLocalizedString(@"EnterHerePhrase", nil)];
[self.pairToStudioButton setTitle:NSLocalizedString(@"PairWord", nil) forState:UIControlStateNormal];
[self.pairingLabel setText:NSLocalizedString(@"RbxDevPairingInstructions", nil)];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void) pairingDidEnd:(NSNotification *)aNotification
{
if (self.navigationController.topViewController != self)
return;
NSDictionary* pairingEndedDict = aNotification.userInfo;
if (pairingEndedDict)
{
NSString* didPair = [pairingEndedDict objectForKey:@"didPair"];
if ([didPair boolValue]) // if we paired, show an alert indicating success
{
NSData* ipAddress = [pairingEndedDict objectForKey:@"ipData"];
if (ipAddress && ipAddress.length > 0)
{
NSString* pairedAlertString = NSLocalizedString(@"RobloxDevSuccessfulPair", nil);
NSString *host = nil;
uint16_t port = 0;
[GCDAsyncUdpSocket getHost:&host port:&port fromAddress:ipAddress];
NSString* hostName = [[StudioConnector sharedInstance] hostnameForAddress:host];
if (hostName && hostName.length > 0)
{
pairedAlertString = [NSString stringWithFormat:pairedAlertString, hostName];
}
else if (NSString* ipString = [pairingEndedDict objectForKey:@"ipString"])
{
pairedAlertString = [NSString stringWithFormat:pairedAlertString, ipString];
}
[RobloxAlert RobloxOKAlertWithMessageAndDelegate:pairedAlertString Delegate:self];
[RobloxGoogleAnalytics setPageViewTracking:@"Pair/Success"];
}
else
{
[RobloxGoogleAnalytics setPageViewTracking:@"Pair/Fail/NoIP"];
}
}
else // did not pair, let user know why
{
NSString* errorReason = [pairingEndedDict objectForKey:@"errorReason"];
if (errorReason && errorReason.length > 0)
{
if ([errorReason isEqualToString:@"timeout"])
{
[RobloxAlert RobloxOKAlertWithMessageAndDelegate:NSLocalizedString(@"RobloxDevPairFailureTimeout", nil) Delegate:self];
return;
}
}
[RobloxGoogleAnalytics setPageViewTracking:@"Pair/Fail"];
}
}
}
-(void) stopPairingUI
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.loadingSpinner setHidden:YES];
[self.pairingVignette setHidden:YES];
[self.pairToStudioButton setTitle:NSLocalizedString(@"PairWord", nil) forState:UIControlStateNormal];
[self.codeField setEnabled:YES];
});
}
-(void) startPairingUI
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.loadingSpinner setHidden:NO];
[self.pairingVignette setHidden:NO];
[self.pairToStudioButton setTitle:NSLocalizedString(@"CancelWord", nil) forState:UIControlStateNormal];
[self.codeField setEnabled:NO];
});
}
-(void) startPairing
{
[self startPairingUI];
[[StudioConnector sharedInstance] tryToPairWithStudio:self.codeField.text];
[RobloxGoogleAnalytics setPageViewTracking:@"Pair/Start"];
}
-(void) stopPairing
{
[self stopPairingUI];
[[StudioConnector sharedInstance] stopPairing];
[RobloxGoogleAnalytics setPageViewTracking:@"Pair/Fail/UserPressedCancel"];
}
- (IBAction) pairWithStudio:(UIButton *)sender
{
if ([self.pairToStudioButton.titleLabel.text isEqualToString:NSLocalizedString(@"PairWord", nil)])
{
[self startPairing];
}
else
{
[self stopPairing];
}
}
- (IBAction) codeFieldDidEndOnExit:(UITextField *)sender
{
shouldTryConnect = YES;
[self pairWithStudio:nil];
}
- (IBAction) devCodeAreaPressed:(UIButton *) sender
{
[self.codeField becomeFirstResponder];
}
// UITextFieldDelegate Methods
- (BOOL) textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
// only allow numbers to be input
if (string.length == 0)
{
[self.pairToStudioButton setEnabled:NO];
[self.pairToStudioButton setAlpha:0.5];
return YES;
}
if ([string isEqualToString:@"\n"])
{
return YES;
}
if ( [textField.text length] >= 4 || NSEqualRanges([string rangeOfCharacterFromSet:[NSCharacterSet decimalDigitCharacterSet]], NSMakeRange(NSNotFound, 0)) )
return NO;
if ( [textField.text length] >= 3)
{
[self.pairToStudioButton setEnabled:YES];
[self.pairToStudioButton setAlpha:1];
}
else
{
[self.pairToStudioButton setEnabled:NO];
[self.pairToStudioButton setAlpha:0.5];
}
return YES;
}
- (IBAction) helpButtonPressed:(UIButton *) sender
{
PairTutorialDataController *pairTutorialDataController = [self.storyboard instantiateViewControllerWithIdentifier:@"PairTutorialDataController"];
[self.navigationController pushViewController:pairTutorialDataController animated:YES];
}
- (IBAction) clearPairCode:(UIBarButtonItem *)sender
{
[[StudioConnector sharedInstance] stopPairing];
[[StudioConnector sharedInstance] clearPairCode];
self.codeField.text = @"";
shouldTryConnect = NO;
[RobloxAlert RobloxOKAlertWithMessageAndDelegate:@"Dev Code removed from device" Delegate:self];
}
- (IBAction) closeButtonPressed:(UIButton *)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
// UIAlertViewDelegate
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if ( buttonIndex == 0 )
{
// need to check alertView message against formatted success message to see if we failed pairing
NSString* successfulPairMessage = NSLocalizedString(@"RobloxDevSuccessfulPair", nil);
successfulPairMessage = [successfulPairMessage stringByReplacingOccurrencesOfString:@"%@" withString:@""];
successfulPairMessage = [successfulPairMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
[self stopPairingUI];
[[StudioConnector sharedInstance] stopPairing];
if (shouldTryConnect) // pairing succeeded, go to connection view and dump this controller along the way (if we are in a mode that supports this)
{
dispatch_async(dispatch_get_main_queue(), ^{
StudioConnectionViewController *studioConnectionViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"StudioConnectionViewController"];
UINavigationController *navigationController = self.navigationController;
[navigationController popToRootViewControllerAnimated:NO];
[navigationController pushViewController:studioConnectionViewController animated:YES];
});
}
}
}
@end
+28
View File
@@ -0,0 +1,28 @@
//
// TcpViewController.h
// RobloxMobile
//
// Created by Ben Tkacheff on 12/17/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface TcpViewController : UIViewController <NSStreamDelegate>
{
NSInputStream* inputStream;
NSOutputStream* outputStream;
NSString* host;
}
-(void) writeToOutputStream:(NSString*) str;
-(NSArray*) convertStreamToArray;
-(BOOL) listenForActions:(NSString*) host;
-(void) resetStreams;
-(void) resetStreamsOnMainThread;
-(void) tryToDestroyStreams;
@end
+166
View File
@@ -0,0 +1,166 @@
//
// TcpViewController.m
// RobloxMobile
//
// Created by Ben Tkacheff on 12/17/13.
// Copyright (c) 2013 ROBLOX. All rights reserved.
//
#import "TcpViewController.h"
@interface TcpViewController ()
@end
@implementation TcpViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
-(void) dealloc
{
[self tryToDestroyStreams];
}
- (void)viewDidLoad
{
[super viewDidLoad];
host = @"";
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(void) resetStreamsOnMainThread
{
dispatch_async(dispatch_get_main_queue(), ^{
[self resetStreams];
});
}
-(void) resetStreams
{
[self tryToDestroyStreams];
[self listenForActions:host];
}
-(void) tryToDestroyStreams
{
if(inputStream)
{
[inputStream close];
[inputStream setDelegate:nil];
inputStream = nil;
}
if(outputStream)
{
[outputStream close];
[outputStream setDelegate:nil];
outputStream = nil;
}
}
-(BOOL) listenForActions:(NSString*) newHost
{
if (newHost == nil)
{
return NO;
}
if ((NSNull *) newHost == [NSNull null])
{
return NO;
}
if (newHost.length <= 0)
{
return NO;
}
if (outputStream || inputStream)
{
[self tryToDestroyStreams];
}
host = newHost;
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (__bridge CFStringRef)host, 1315, &readStream, &writeStream);
if (!writeStream || !readStream)
{
return NO;
}
CFWriteStreamOpen(writeStream);
CFReadStreamOpen(readStream);
inputStream = objc_unretainedObject(readStream);
outputStream = objc_unretainedObject(writeStream);
[inputStream setDelegate:self];
[outputStream setDelegate:self];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
[outputStream open];
CFRelease(writeStream);
CFRelease(readStream);
return YES;
}
-(NSArray*) convertStreamToArray
{
uint8_t buffer[1024];
if ([inputStream hasBytesAvailable])
{
int len = [inputStream read:buffer maxLength:sizeof(buffer)];
if (len > 0)
{
NSString *output = [[NSString alloc] initWithBytes:buffer length:len encoding:NSASCIIStringEncoding];
if (nil != output)
{
NSMutableCharacterSet* separatorSet = [NSMutableCharacterSet whitespaceAndNewlineCharacterSet];
NSMutableArray *words = [[output componentsSeparatedByCharactersInSet:separatorSet] mutableCopy];
return words;
}
}
}
return nil;
}
-(void) writeToOutputStream:(NSString*) str
{
if (!outputStream)
{
return;
}
NSData* data = [str dataUsingEncoding:NSUTF8StringEncoding];//str is my string to send
int byteIndex = 0;
uint8_t *readBytes = (uint8_t *)[data bytes];
readBytes += byteIndex; // instance variable to move pointer
int data_len = [data length];
unsigned int len = ((data_len - byteIndex >= 1024) ?
1024 : (data_len-byteIndex));
uint8_t buf[len];
(void)memcpy(buf, readBytes, len);
len = [outputStream write:(const uint8_t *)buf maxLength:len];
byteIndex += len;
}
@end
@@ -0,0 +1,31 @@
//
// TestAccountSigninView.h
// RobloxMobile
//
// Created by Ben Tkacheff on 7/22/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface TestAccountSigninViewController : UIViewController
{
}
@property (retain, nonatomic) IBOutlet UIView *loggingInView;
@property (retain, nonatomic) IBOutlet UITextField *username;
@property (retain, nonatomic) IBOutlet UITextField *password;
@property (retain, nonatomic) IBOutlet UIButton *loginButton;
@property (retain, nonatomic) IBOutlet UIButton *forgotPasswordButton;
@property (retain, nonatomic) IBOutlet UIImageView *loadingSpinner;
@property (retain, nonatomic) IBOutlet UIBarButtonItem *logoutButton;
- (IBAction) closeButtonPressed:(UIBarButtonItem *)sender;
- (IBAction) logoutButtonPressed:(UIBarButtonItem *)sender;
- (IBAction) loginButtonPressed:(UIButton *)sender;
- (IBAction) usernameDidEndOnExit:(UITextField *)sender;
- (IBAction) passwordDidEndOnExit:(UITextField *)sender;
@end
@@ -0,0 +1,158 @@
//
// TestAccountSigninView.m
// RobloxMobile
//
// Created by Ben Tkacheff on 7/22/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBXFunctions.h"
#import "TestAccountSigninViewController.h"
#import "LoginManager.h"
#import "UserInfo.h"
#import "RobloxAlert.h"
#import "PlaceLauncher.h"
#import "UIStyleConverter.h"
#import "RobloxInfo.h"
#import "ResetPasswordViewController.h"
#import "RobloxGoogleAnalytics.h"
#import "RobloxNotifications.h"
@interface TestAccountSigninViewController ()
@end
@implementation TestAccountSigninViewController
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self == [super initWithCoder:aDecoder])
{
// initialize here
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[UIStyleConverter convertToBoldTextFieldStyle:self.username];
[UIStyleConverter convertToBoldTextFieldStyle:self.password];
[UIStyleConverter convertToButtonBlueStyle:self.loginButton];
[UIStyleConverter convertToBorderlessButtonStyle:self.forgotPasswordButton];
[UIStyleConverter convertToLoadingStyle:self.loadingSpinner];
self.username.placeholder = NSLocalizedString(@"UsernameWord", nil);
self.password.placeholder = NSLocalizedString(@"PasswordWord", nil);
[self.loginButton setTitle:NSLocalizedString(@"LoginWord", nil) forState:UIControlStateNormal];
[self.loginButton setTitle:NSLocalizedString(@"LoginWord", nil) forState:UIControlStateSelected];
[self.loginButton setTitle:NSLocalizedString(@"LoginWord", nil) forState:UIControlStateDisabled];
[self.forgotPasswordButton setTitle:NSLocalizedString(@"Forgot Password?", nil) forState:UIControlStateNormal];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[UIStyleConverter convertToBlueNavigationBarStyle:self.navigationController.navigationBar];
}
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[RobloxGoogleAnalytics setPageViewTracking:@"Login"];
self.username.text = [UserInfo CurrentPlayer].username;
self.password.text = [UserInfo CurrentPlayer].password;
// small color bug fix for iPad
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[self.logoutButton setTitleTextAttributes:
[NSDictionary dictionaryWithObjectsAndKeys:
[UIColor whiteColor], NSForegroundColorAttributeName,nil]
forState:UIControlStateNormal];
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction) closeButtonPressed:(UIBarButtonItem *)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction) logoutButtonPressed:(UIBarButtonItem *)sender
{
[[LoginManager sharedInstance] logoutRobloxUser];
self.username.text = @"";
self.password.text = @"";
dispatch_async(dispatch_get_main_queue(), ^{
self.loggingInView.hidden = true;
[self.username resignFirstResponder];
[self.password resignFirstResponder];
[RobloxAlert RobloxOKAlertWithMessageAndDelegate:@"You have been logged out." Delegate:self];
});
}
- (IBAction) loginButtonPressed:(UIButton *)sender
{
[self doLogin];
}
- (IBAction) usernameDidEndOnExit:(UITextField *)sender
{
[self.password becomeFirstResponder];
}
- (IBAction) passwordDidEndOnExit:(UITextField *)sender
{
[self doLogin];
}
-(void) doLogin
{
self.loggingInView.hidden = false;
[[LoginManager sharedInstance] loginWithUsername:self.username.text password:self.password.text completionBlock:^(NSError *loginError) {
if ([RBXFunctions isEmpty:loginError]) {
// login successful
[RBXFunctions dispatchOnMainThread:^{
[RobloxGoogleAnalytics setPageViewTracking:@"Login/Success"];
self.loggingInView.hidden = true;
if (self.username.isFirstResponder)
{
[self.username resignFirstResponder];
}
if (self.password.isFirstResponder)
{
[self.password resignFirstResponder];
}
[RobloxAlert RobloxOKAlertWithMessageAndDelegate:@"Login Succedeed!" Delegate:self];
}];
} else {
// login failure
[RBXFunctions dispatchOnMainThread:^{
[RobloxGoogleAnalytics setPageViewTracking:@"Login/Failure"];
self.loggingInView.hidden = true;
[RobloxAlert RobloxAlertWithMessage:loginError.domain];
}];
}
}];
[RobloxGoogleAnalytics setPageViewTracking:@"Login/Try"];
}
@end
+36
View File
@@ -0,0 +1,36 @@
//
// UIStyleConverter.h
// RobloxMobile
//
// Created by Ben Tkacheff on 8/13/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface UIStyleConverter : NSObject
+(void) convertToButtonStyle:(UIButton*) button;
+(void) convertToButtonBlueStyle:(UIButton*) button;
+(void) convertToBorderlessButtonStyle:(UIButton*) button;
+(void) convertToTitleStyle:(UILabel*) label;
+(void) convertToBlueTitleStyle:(UILabel*) label;
+(void) convertToLabelStyle:(UILabel*) label;
+(void) convertToLargeLabelStyle:(UILabel*) label;
+(void) convertToLoadingStyle:(UIImageView*) imageView;
+(void) convertToTextFieldStyle:(UITextField*) textField;
+(void) convertToBoldTextFieldStyle:(UITextField*) textField;
+(void) convertUIButtonToLabelStyle:(UIButton*) button;
+(void) convertToHyperlinkStyle:(UIButton*) button;
+(void) convertToBlueNavigationBarStyle:(UINavigationBar*) bar;
+(void) convertToNavigationBarStyle;
+(void) convertToPagingStyle;
+(void) convertToTexturedBackgroundStyle:(UIView*) view;
@end
+225
View File
@@ -0,0 +1,225 @@
//
// UIStyleConverter.m
// RobloxMobile
//
// Created by Ben Tkacheff on 8/13/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import "UIStyleConverter.h"
@implementation UIStyleConverter
+(void) convertToLoadingStyle:(UIImageView*) imageView
{
[imageView setImage:[UIImage animatedImageNamed:@"loading-" duration:0.6f]];
}
+(void) convertToTitleStyle:(UILabel*) label
{
label.font = [UIFont fontWithName:@"SourceSansPro-Semibold" size:24.0f];
label.textColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1];
}
+(void) convertToBlueTitleStyle:(UILabel*) label
{
label.font = [UIFont fontWithName:@"SourceSansPro" size:18.0f];
label.textColor = [UIColor colorWithRed:37.0/255.0 green:80.0/255.0 blue:148.0/255.0 alpha:1];
}
+(void) convertUIButtonToLabelStyle:(UIButton*) button
{
[button.titleLabel setFont:[UIFont fontWithName:@"SourceSansPro-Regular" size:18.0f]];
[button setTitleColor:[UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1] forState:UIControlStateNormal];
[button setTitleColor:[UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1] forState:UIControlStateHighlighted];
[button setTitleColor:[UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1] forState:UIControlStateSelected];
button.layer.backgroundColor = [[UIColor whiteColor] CGColor];
}
+(void) convertToHyperlinkStyle:(UIButton*) button
{
button.layer.backgroundColor = [[UIColor clearColor] CGColor];
[button setTitleColor:[UIColor colorWithRed:240.0/255.0 green:240.0/255.0 blue:240.0/255.0 alpha:1] forState:UIControlStateNormal];
[button setTitleColor:[UIColor colorWithRed:240.0/255.0 green:240.0/255.0 blue:240.0/255.0 alpha:1] forState:UIControlStateHighlighted];
[button setTitleColor:[UIColor colorWithRed:240.0/255.0 green:240.0/255.0 blue:240.0/255.0 alpha:1] forState:UIControlStateSelected];
[button.titleLabel setFont:[UIFont fontWithName:@"SourceSansPro-Regular" size:14.0f]];
}
+(void) convertToLabelStyle:(UILabel*) label
{
label.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:18.0f];
label.textColor = [UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1];
}
+(void) convertToLargeLabelStyle:(UILabel*) label
{
label.font = [UIFont fontWithName:@"SourceSansPro-Light" size:24.0f];
label.textColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:1];
}
+(void) convertToButtonStyle:(UIButton*) button
{
button.titleLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:18.0f];
button.showsTouchWhenHighlighted = YES;
button.adjustsImageWhenHighlighted = NO;
button.adjustsImageWhenDisabled = YES;
[button setTitleColor:[UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1] forState:UIControlStateNormal];
[button setTitleColor:[UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1] forState:UIControlStateHighlighted];
[button setTitleColor:[UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1] forState:UIControlStateSelected];
button.layer.borderWidth = 1.0f;
button.layer.borderColor = [[UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:0] CGColor];
button.layer.cornerRadius = 4.0f;
button.layer.backgroundColor = [[UIColor whiteColor] CGColor];
button.layer.shadowOpacity = 0.2f;
button.layer.shadowOffset = CGSizeMake(0, 1);
button.layer.shadowRadius = 2.0f;
}
+(void) convertToBorderlessButtonStyle:(UIButton*) button
{
button.titleLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:14.0f];
[button setTitleColor:[UIColor colorWithRed:37.0/255.0 green:80.0/255.0 blue:148.0/255.0 alpha:1] forState:UIControlStateNormal];
[button setTitleColor:[UIColor colorWithRed:37.0/255.0 green:80.0/255.0 blue:148.0/255.0 alpha:1] forState:UIControlStateHighlighted];
[button setTitleColor:[UIColor colorWithRed:37.0/255.0 green:80.0/255.0 blue:148.0/255.0 alpha:1] forState:UIControlStateSelected];
}
+(void) convertToButtonBlueStyle:(UIButton*) button
{
button.showsTouchWhenHighlighted = YES;
button.adjustsImageWhenHighlighted = NO;
button.adjustsImageWhenDisabled = YES;
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected];
[button setTitleColor:[UIColor colorWithRed:169.0/255.0 green:169.0/255.0 blue:169.0/255.0 alpha:1] forState:UIControlStateDisabled];
button.titleLabel.font = [UIFont fontWithName:@"SourceSansPro-Semibold" size:18.0f];
button.layer.borderWidth = 1.0f;
button.layer.borderColor = [[UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:0] CGColor];
button.layer.cornerRadius = 4.0f;
button.layer.backgroundColor = [[UIColor colorWithRed:37.0/255.0 green:80.0/255.0 blue:148.0/255.0 alpha:1] CGColor];
button.layer.shadowOpacity = 0.2f;
button.layer.shadowOffset = CGSizeMake(0, 1);
button.layer.shadowRadius = 2.0f;
}
+(void) convertToBlueNavigationBarStyle:(UINavigationBar*) bar
{
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[UIFont
fontWithName:@"SourceSansPro-Semibold" size:18.0], NSFontAttributeName,
[UIColor whiteColor], NSForegroundColorAttributeName,
nil];
[bar setTitleTextAttributes:attributes];
NSDictionary *textAttributes = @{ NSForegroundColorAttributeName : [UIColor whiteColor],
NSFontAttributeName : [UIFont fontWithName:@"SourceSansPro-Regular" size:16.0]
};
[[UIBarButtonItem appearanceWhenContainedIn: [UINavigationController class],nil]
setTitleTextAttributes:textAttributes
forState:UIControlStateNormal];
[bar setTintColor:[UIColor whiteColor]];
[bar setBarTintColor:[UIColor colorWithRed:37.0/255.0f green:80.0/255.0 blue:148.0/255.0 alpha:1]];
}
+(void) convertToNavigationBarStyle
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[UIFont
fontWithName:@"SourceSansPro-Semibold" size:18.0], NSFontAttributeName,
[UIColor whiteColor], NSForegroundColorAttributeName,
nil];
[[UINavigationBar appearance] setTitleTextAttributes:attributes];
// your bar button text attributes dictionary
NSDictionary *textAttributes = @{ NSForegroundColorAttributeName : [UIColor whiteColor],
NSFontAttributeName : [UIFont fontWithName:@"SourceSansPro-Regular" size:16.0]
};
[[ UIBarButtonItem appearanceWhenContainedIn: [UINavigationController class],nil]
setTitleTextAttributes:textAttributes
forState:UIControlStateNormal];
[[UINavigationBar appearance] setTintColor:[UIColor whiteColor]];
[[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:37.0/255.0f green:80.0/255.0 blue:148.0/255.0 alpha:1]];
}
else
{
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[UIFont
fontWithName:@"SourceSansPro-Regular" size:18.0], NSFontAttributeName,
[UIColor colorWithRed:37.0/255.0 green:80.0/255.0 blue:148.0/255.0 alpha:1], NSForegroundColorAttributeName,
nil];
[[UINavigationBar appearance] setTitleTextAttributes:attributes];
// your bar button text attributes dictionary
NSDictionary *textAttributes = @{ NSForegroundColorAttributeName : [UIColor colorWithRed:37.0/255.0 green:80.0/255.0 blue:1480.0/255.0 alpha:1],
NSFontAttributeName : [UIFont fontWithName:@"SourceSansPro-Regular" size:16.0]
};
[[ UIBarButtonItem appearanceWhenContainedIn: [UINavigationController class],nil]
setTitleTextAttributes:textAttributes
forState:UIControlStateNormal];
[[UINavigationBar appearance] setTintColor:[UIColor colorWithRed:37.0/255.0 green:80.0/255.0 blue:148.0/255.0 alpha:1]];
[[UINavigationBar appearance] setBarTintColor:[UIColor whiteColor]];
}
}
+(void) convertToBoldTextFieldStyle:(UITextField*) textField
{
textField.font = [UIFont fontWithName:@"SourceSansPro-Semibold" size:18.0f];
[textField setTextColor:[UIColor colorWithRed:50.0/255.0 green:50.0/255.0 blue:50.0/255.0 alpha:1]];
}
+(void) convertToTextFieldStyle:(UITextField*) textField
{
// store original text to restore after we set attributes
NSString* origText = textField.text;
textField.attributedText =
[[NSAttributedString alloc] initWithString:@""
attributes:@{
NSForegroundColorAttributeName: [UIColor colorWithRed:169.0/255.0 green:169.0/255.0 blue:169.0/255.0 alpha:1],
NSFontAttributeName : [UIFont fontWithName:@"SourceSansPro-Regular" size:18.0f]
}];
[textField setTintColor:[UIColor colorWithRed:37.0/255.0f green:80.0/255.0 blue:148.0/255.0 alpha:1]];
if (origText.length > 0)
{
textField.text = origText;
}
}
+(void) convertToPagingStyle
{
UIPageControl *pageControl = [UIPageControl appearance];
pageControl.pageIndicatorTintColor = [UIColor lightGrayColor];
pageControl.currentPageIndicatorTintColor = [UIColor blackColor];
pageControl.backgroundColor = [UIColor whiteColor];
}
+(void) convertToTexturedBackgroundStyle:(UIView*) view
{
[view setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:@"bg_pattern"]]];
}
@end
+15
View File
@@ -0,0 +1,15 @@
//
// main.cpp
// RobloxMobile
//
// Created by Kyler Mulherin on 10/28/15.
// Copyright © 2015 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
int main(int argc, char *argv[])
{
int retVal = UIApplicationMain(argc, argv, @"UIApplication", @"AppDelegate");
return retVal;
}