CustomOSC first integration

This commit is contained in:
True Zhuanjia committed 2024-06-24 23:15:55 +08:00
1 parent 457a4e8725
commit 0bac5f1ae9
23 files changed
+600 -33

No files matched your search

@@ -0,0 +1,35 @@
//
// LayoutOnScreenControls.h
// Moonlight
//
// Created by Long Le on 9/26/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "OnScreenControls.h"
NS_ASSUME_NONNULL_BEGIN
/**
This object is a subclass of 'OnScreenControls' and adds additional properties and functions which allow the user to drag and drop each of the 19 on screen controller buttons in order to change their positions on screen. The object is used in the 'LayoutOnScreenControlsViewController' thus allowing the user to drag, drop, hide, unhide, on screen controller buttons.
Note that this is in contrast to the game stream view which displays an 'OnScreenControls' object on screen that allows the app to register taps on each button as controller input. It does not (and naturally should not) allow the user to move the buttons around the screen.
*/
@interface LayoutOnScreenControls : OnScreenControls
@property UIView* _view;
@property NSMutableArray *layoutChanges;
@property CALayer *layerBeingDragged;
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport
streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel;
- (CALayer*) buttonLayerFromName:(NSString*)name;
- (BOOL) isLayer:(CALayer*)layer hoveringOverButton:(UIButton*)button;
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,239 @@
//
// LayoutOnScreenControls.m
// Moonlight
//
// Created by Long Le on 9/26/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import "LayoutOnScreenControls.h"
#import "OSCProfilesTableViewController.h"
#import "OnScreenButtonState.h"
#import "OSCProfilesManager.h"
@interface LayoutOnScreenControls ()
@end
@implementation LayoutOnScreenControls {
UIButton *trashCanButton;
UIView *horizontalGuideline;
UIView *verticalGuideline;
}
@synthesize layerBeingDragged;
@synthesize _view;
@synthesize layoutChanges;
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel {
_view = view;
_view.multipleTouchEnabled = false;
self = [super initWithView:view controllerSup:controllerSupport streamConfig:streamConfig];
self._level = oscLevel;
layoutChanges = [[NSMutableArray alloc] init]; // will contain OSC button layout changes the user has made for this profile
[self drawButtons];
[self drawGuidelines]; // add the blue guidelines that appear when button is being tapped and dragged
return self;
}
#pragma mark - Drawing
/**
* This method overrides the superclass's drawButtons method. The purpose of this method is to create a dPad parent layer, and add the four dPad buttons to it so that the user can drag the entire dPad around, directional buttons and all, as one unit as is the expected behavior. Note that we do not want the four dPad buttons to be child layers of a CALayer parent layer on the game stream view since the touch logic implemented for the four dPad buttons on the game stream view is written assuming the dPad buttons are not children of another parent CALayer
*/
- (void) drawButtons {
[super setDPadCenter]; // Set custom position for D-Pad here
[super setAnalogStickPositions]; // Set custom position for analog sticks here
[super drawButtons];
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
// create dPad background layer
self._dPadBackground = [CALayer layer];
self._dPadBackground.name = @"dPad";
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
self.D_PAD_CENTER_Y,
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
self._leftButton.frame.size.width * 2 + BUTTON_DIST);
self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here
[self.OSCButtonLayers addObject:self._dPadBackground];
[_view.layer addSublayer:self._dPadBackground];
/* add dPad buttons to parent layer */
[self._dPadBackground addSublayer:self._downButton];
[self._dPadBackground addSublayer:self._rightButton];
[self._dPadBackground addSublayer:self._upButton];
[self._dPadBackground addSublayer:self._leftButton];
/* reposition each dPad button within their parent dPadBackground layer */
self._downButton.frame = CGRectMake(self._dPadBackground.frame.size.width/3, self._dPadBackground.frame.size.height/2 + D_PAD_DIST, downButtonImage.size.width, downButtonImage.size.height);
self._rightButton.frame = CGRectMake(self._dPadBackground.frame.size.width/2 + D_PAD_DIST, self._dPadBackground.frame.size.height/3, rightButtonImage.size.width, rightButtonImage.size.height);
self._upButton.frame = CGRectMake(self._dPadBackground.frame.size.width/3, 0, upButtonImage.size.width, upButtonImage.size.height);
self._leftButton.frame = CGRectMake(0, self._dPadBackground.frame.size.height/3, leftButtonImage.size.width, leftButtonImage.size.height);
}
/**
* draws a horizontal and vertical line that is made visible and positioned over whichever button the user is dragging around the screen
*/
- (void) drawGuidelines {
horizontalGuideline = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self._view.frame.size.width * 2, 2)];
horizontalGuideline.backgroundColor = [UIColor blueColor];
horizontalGuideline.hidden = YES;
[self._view addSubview: horizontalGuideline];
verticalGuideline = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 2, self._view.frame.size.height * 2)];
verticalGuideline.backgroundColor = [UIColor blueColor];
verticalGuideline.hidden = YES;
[self._view addSubview: verticalGuideline];
}
#pragma mark - Queries
/* used to determine whether user is dragging an OSC button (of type CALayer) over the trash can with the intent of hiding that button */
- (BOOL) isLayer:(CALayer *)layer hoveringOverButton:(UIButton *)button {
CGRect buttonConvertedRect = [self._view convertRect:button.imageView.frame fromView:button.superview];
if (CGRectIntersectsRect(layer.frame, buttonConvertedRect)) {
return YES;
}
else {
return NO;
}
}
/* returns reference to button layer object given the button's name*/
- (CALayer*)buttonLayerFromName: (NSString*)name {
for (CALayer *buttonLayer in self.OSCButtonLayers) {
if ([buttonLayer.name isEqualToString:name]) {
return buttonLayer;
}
}
return nil;
}
#pragma mark - Touch
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Reset variables related to on screen controller button drag and drop routine. These variables should be reset in 'touchesCancelled' or 'touchesEnded' but these may not be called in unaccounted-for edge cases such as when the user opens various OS-level control center related views by dragging down from the top of the screen, or dragging up from the bottom of the screen. Since Apple likes to add new control centers and new ways of opening them (i.e. Dynamic Island on iPhone 14 Pro) it's best to reset these variables here when the user is beginning a new on screen controller button drag routine */
layerBeingDragged = nil;
horizontalGuideline.backgroundColor = [UIColor blueColor];
verticalGuideline.backgroundColor = [UIColor blueColor];
for (UITouch* touch in touches) { // Process touch
CGPoint touchLocation = [touch locationInView:_view];
touchLocation = [[touch view] convertPoint:touchLocation toView:nil];
CALayer *layer = [_view.layer hitTest:touchLocation];
/* Don't let user drag and move anything other than on screen controller buttons, which are CALayer types. The reason is that 'LayoutOnScreenControls' should only be responsible for managing and letting users move on screen controller buttons. Since this class's view is currently set to be set equal to the 'LayoutOnScreenControlsViewController' view it belongs to, we need to make sure touches on the VC's objects don't propagate down to 'LayoutOnScreenControls'. Weird stuff can happen to the UI buttons that belong to that VC (trash can button, undo button, save button, etc), such as them being dragged around the screen with the user's touches */
for (UIView *subview in self._view.subviews) {
if (CGRectContainsPoint(subview.frame, touchLocation)) {
if (![subview isKindOfClass:[CALayer class]]) {
return;
}
}
}
if (layer == self._upButton ||
layer == self._downButton ||
layer == self._leftButton ||
layer == self._rightButton) { // don't let user drag individual dPad buttons
layerBeingDragged = self._dPadBackground;
}
else if (layer == self._rightStick) { // only let user drag right stick's background, not the inner analog stick itself
layerBeingDragged = self._rightStickBackground;
}
else if (layer == self._leftStick) { // only let user drag left stick's background, not the inner analog stick itself
layerBeingDragged = self._leftStickBackground;
}
else { // let user drag whatever other valid button they're touching
layerBeingDragged = layer;
}
/* save the name, position, and visibility of button being touched in array in case user wants to undo the change later */
OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name isHidden:layerBeingDragged.isHidden andPosition:layerBeingDragged.position];
[layoutChanges addObject:onScreenButtonState];
[[NSNotificationCenter defaultCenter] postNotificationName:@"OSCLayoutChanged" object:self]; // lets the view controller know whether to fade the undo button in or out depending on whether there are any further OSC layout changes the user is allowed to undo
/* make guide lines visible and position them over the button the user is touching */
horizontalGuideline.center = layerBeingDragged.position;
horizontalGuideline.hidden = NO;
verticalGuideline.center = layerBeingDragged.position;
verticalGuideline.hidden = NO;
}
}
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:_view];
layerBeingDragged.position = touchLocation; // move object to touch location
/* have guidelines follow wherever the user is touching on the screen */
horizontalGuideline.center = layerBeingDragged.position;
verticalGuideline.center = layerBeingDragged.position;
/*
Telegraph to the user whether the horizontal and/or vertical guidelines line up with one or more of the buttons on screen by doing the following:
-Change horizontal guideline color to white if its y-position is almost equal to that of one of the buttons on screen.
-Change vertical guideline color to white if its x-position is almost equal to that of one of the buttons on screen.
*/
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
if ((layerBeingDragged != button) && !button.isHidden) {
if ((horizontalGuideline.center.y < button.position.y + 1) &&
(horizontalGuideline.center.y > button.position.y - 1)) {
horizontalGuideline.backgroundColor = [UIColor whiteColor];
break;
}
}
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
}
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
if ((layerBeingDragged != button) && !button.isHidden) {
if ((verticalGuideline.center.x < button.position.x + 1) &&
(verticalGuideline.center.x > button.position.x - 1)) {
verticalGuideline.backgroundColor = [UIColor whiteColor];
break;
}
}
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
}
}
- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
layerBeingDragged = nil;
horizontalGuideline.hidden = YES;
verticalGuideline.hidden = YES;
horizontalGuideline.backgroundColor = [UIColor blueColor];
verticalGuideline.backgroundColor = [UIColor blueColor];
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
layerBeingDragged = nil;
horizontalGuideline.hidden = YES;
verticalGuideline.hidden = YES;
horizontalGuideline.backgroundColor = [UIColor blueColor];
verticalGuideline.backgroundColor = [UIColor blueColor];
}
@end
+32
View File
@@ -0,0 +1,32 @@
//
// OSCProfile.h
// Moonlight
//
// Created by Long Le on 12/22/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "OnScreenButtonState.h"
NS_ASSUME_NONNULL_BEGIN
/**
This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults.
Additionally the 'OSCProfile' object adopts encoding and decoding protocols so that we can encode the object before saving it to NSUserDefaults. By saving this object to NSUserDefaults we allow the user to save and load their custom on screen controller button layouts between app launches
*/
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding>
@property NSString *name;
@property NSMutableArray <OnScreenButtonState *> *buttonStates;
@property BOOL isSelected;
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
+ (BOOL) supportsSecureCoding;
- (id) initWithCoder:(NSCoder*)decoder;
- (void) encodeWithCoder:(NSCoder*)encoder;
@end
NS_ASSUME_NONNULL_END
+43
View File
@@ -0,0 +1,43 @@
//
// OSCProfile.m
// Moonlight
//
// Created by Long Le on 12/22/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import "OSCProfile.h"
@implementation OSCProfile
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
if ((self = [self init])) {
self.name = name;
self.buttonStates = buttonStates;
self.isSelected = isSelected;
}
return self;
}
+ (BOOL) supportsSecureCoding {
return YES;
}
- (void) encodeWithCoder:(NSCoder*)encoder {
[encoder encodeObject:self.name forKey:@"name"];
[encoder encodeObject:self.buttonStates forKey:@"buttonStates"];
[encoder encodeBool:self.isSelected forKey:@"isSelected"];
}
- (id) initWithCoder:(NSCoder*)decoder {
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:@"name"];
self.buttonStates = [decoder decodeObjectForKey:@"buttonStates"];
self.isSelected = [decoder decodeBoolForKey:@"isSelected"];
}
return self;
}
@end
@@ -0,0 +1,63 @@
//
// OSCProfilesManager.h
// Moonlight
//
// Created by Long Le on 1/1/23.
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "OSCProfile.h"
NS_ASSUME_NONNULL_BEGIN
/**
This singleton object can be accessed from any class and provides methods to get and set on screen controller profile related data.
Note that the implementation file contains a number of 'Helper' methods. These helper methods are only used in this class's implementation file and help to reduce re-writing large blocks of code that are called multiple times throughout the file
*/
@interface OSCProfilesManager : NSObject
+ (OSCProfilesManager *) sharedManager;
#pragma mark - Getters
/**
* Returns an array of decoded profile objects
*/
- (NSMutableArray *) getAllProfiles;
/**
* Returns the OSC Profile that is currently selected to be displayed on screen during game streaming
*/
- (OSCProfile *) getSelectedProfile;
/**
* Returns the index of the 'selected' profile within the array it's in
*/
- (NSInteger) getIndexOfSelectedProfile;
#pragma mark - Setters
/**
* Sets the profile object with the particular 'name' as the selected profile to be displayed on screen during game streaming
*/
- (void) setProfileToSelected:(NSString *)name;
/**
* Saves a profile object with a particular 'name' and an array of button layers (the CALayer button layers are the objects currently visible on screen) to persistent storage
*/
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)buttonLayers;
#pragma mark - Queries
/**
* Lets the caller of this method know whether a profile with a given name already exists in persistent storage
*/
- (BOOL) profileNameAlreadyExist:(NSString*)name;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,215 @@
//
// OSCProfilesManager.m
// Moonlight
//
// Created by Long Le on 1/1/23.
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
//
#import "OSCProfilesManager.h"
@implementation OSCProfilesManager
#pragma mark - Initializer
+ (OSCProfilesManager *) sharedManager {
static OSCProfilesManager *_sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedManager = [[self alloc] init];
});
return _sharedManager;
}
#pragma mark - Class Helper Methods
/**
* Returns the profile whose 'name' property has a value that is equal to the 'name' passed into the method
*/
- (OSCProfile *) OSCProfileWithName:(NSString*)name {
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
if ([profile.name isEqualToString:name]) {
return profile;
}
}
return nil;
}
/**
* Returns an array of encoded 'OSCProfile' objects from the array of decoded 'OSCProfile' objects passed into this method
*/
- (NSMutableArray *) encodedProfilesFromArray:(NSMutableArray *)profiles {
NSMutableArray *profilesEncoded = [[NSMutableArray alloc] init];
/* encode each profile and add them back into an array */
for (OSCProfile *profile in profiles) { //
NSData *profileEncoded = [NSKeyedArchiver archivedDataWithRootObject:profile requiringSecureCoding:YES error:nil];
[profilesEncoded addObject:profileEncoded];
}
return profilesEncoded;
}
/**
* Replaces one 'OSCProfile' object for another in the 'OSCProfile' objects array stored in persistent storage
*/
- (void) replaceProfile:(OSCProfile*)oldProfile withProfile:(OSCProfile*)newProfile {
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) { // set all profiles' 'isSelected' property to NO since the new profile we're saving over the old profile with will be the 'selected' profile
profile.isSelected = NO;
}
/* Set the new profile as the selected one. The reasoning behind this is that this method is currently being used when the user saves over an existing profile with another profile that has the same name. The expected behavior is that the newly saved profile becomes the selected profile which will show on screen when they launch the game stream view */
newProfile.isSelected = YES;
/* Remove the old profile from the array and insert the new profile into its place */
int index = 0;
for (int i = 0; i < profiles.count; i++) {
if ([[profiles[i] name] isEqualToString: oldProfile.name]) {
index = i;
}
}
[profiles removeObjectAtIndex:index];
[profiles insertObject:newProfile atIndex:index];
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
/* Encode the array itself, NOT the objects in the array, which are already encoded. Save array to persistent storage */
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#pragma mark - Globally Accessible Methods
#pragma mark - Getters
- (NSMutableArray *) getAllProfiles {
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
NSMutableArray *profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:profilesArrayEncoded error:nil]; // Decode the encoded array itself, NOT the objects contained in the array
/* Decode each of the encoded profiles, place them into an array, then return the array */
NSMutableArray *profilesDecoded = [[NSMutableArray alloc] init];
OSCProfile *profileDecoded;
for (NSData *profileEncoded in profilesEncoded) {
profileDecoded = [NSKeyedUnarchiver unarchivedObjectOfClasses: classes fromData:profileEncoded error: nil];
[profilesDecoded addObject: profileDecoded];
}
return profilesDecoded;
}
- (OSCProfile *) getSelectedProfile {
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
if (profile.isSelected) {
return profile;
}
}
return nil;
}
- (NSInteger) getIndexOfSelectedProfile {
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
if (profile.isSelected == YES) {
return [profiles indexOfObject:profile];
}
}
return 0; // if none of the profiles in the array have their 'isSelected' property set to YES (which should not be possible) return the 'Default' profile as the 'selected' profile
}
#pragma mark - Setters
- (void) setProfileToSelected:(NSString *)name {
NSMutableArray *profiles = [self getAllProfiles];
/* Iterate through each profile. If its name equals the value of the 'name' parameter passed into this method then set the profile's 'isSelected' property to YES, otherwise set the value to NO */
for (OSCProfile *profile in profiles) {
if ([profile.name isEqualToString:name]) {
profile.isSelected = YES;
}
else {
profile.isSelected = NO;
}
}
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
/* Encode the array itself, NOT the objects inside the array, which have already been encoded by this point */
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)buttonLayers {
/* iterate through each OSC button the user sees on screen, create an 'OnScreenButtonState' object from each button, encode each object, and then add each object to an array */
NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init];
for (CALayer *buttonLayer in buttonLayers) {
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:buttonLayer.name isHidden:buttonLayer.isHidden andPosition:buttonLayer.position];
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
[buttonStatesEncoded addObject: buttonStateEncoded];
}
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:name
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
NSMutableArray *profiles = [self getAllProfiles];
for (OSCProfile *profile in profiles) {
profile.isSelected = NO;
}
if ([self profileNameAlreadyExist:name]) { // if a saved profile with the same 'name' already exists in persistent storage then overwrite it and save the change to persistent storage
[self replaceProfile:[self OSCProfileWithName:name] withProfile:newProfile];
}
else { // otherwise encode then add the new profile to the end of the OSCProfiles array
NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil];
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
[profilesEncoded addObject:newProfileEncoded];
/* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
#pragma mark - Queries
- (BOOL) profileNameAlreadyExist:(NSString*)name {
NSMutableArray *profiles = [self getAllProfiles];
/* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */
for (OSCProfile *profile in profiles) {
if ([profile.name isEqualToString:name]) {
return YES;
}
}
return NO;
}
@end
@@ -0,0 +1,32 @@
//
// OnScreenButtonState.h
// Moonlight
//
// Created by Long Le on 10/20/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
This object is used to save positional and visibility information for any particular on screen virtual controller button.
We are able to associate this 'OnScreenButtonState' object and its corresponding CALayer onscreen controller button by setting their 'name' properties equal; in our particular case we give them descriptive names such as 'aButton', 'upButton', 'leftStick', etc. By keeping references to the 19 on screen controller buttons (CALayers) in an array, and creating 19 'OnScreenButtonState' objects with names corresponding to these 19 CALayers and keeping them in an array, we can iterate through both arrays to find OSC buttons (CALayers) with the same name as one of the 'OnScreenButtonStateObjects' and then set the CALayer on screen control button's 'position' and 'hidden' property according to the value of the 'OnScreenButtonState' objects 'position' and 'isHidden' properties.
Naturally we would like the user to be able to save their controller layout configurations so that they can load them between app launches, so we adopt encoding/decoding related protocols so we encode these 'OnScreenButtonState' objects and save them to NSUserDefaults
*/
@interface OnScreenButtonState : NSObject <NSCoding, NSSecureCoding>
@property NSString *name;
@property CGPoint position;
@property BOOL isHidden;
- (id) initWithButtonName:(NSString*)name isHidden:(BOOL)isHidden andPosition:(CGPoint)position;
+ (BOOL) supportsSecureCoding;
- (void) encodeWithCoder:(NSCoder*)encoder;
- (id) initWithCoder:(NSCoder*)decoder;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,43 @@
//
// OnScreenButtonState.m
// Moonlight
//
// Created by Long Le on 10/20/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import "OnScreenButtonState.h"
@implementation OnScreenButtonState
- (id) initWithButtonName:(NSString*)name isHidden:(BOOL)isHidden andPosition:(CGPoint)position {
if ((self = [self init])) {
self.name = name;
self.isHidden = isHidden;
self.position = position;
}
return self;
}
+ (BOOL) supportsSecureCoding {
return YES;
}
- (void) encodeWithCoder:(NSCoder*)encoder {
[encoder encodeObject:self.name forKey:@"name"];
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
[encoder encodeCGPoint:self.position forKey:@"position"];
}
- (id) initWithCoder:(NSCoder*)decoder {
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:@"name"];
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
self.position = [decoder decodeCGPointForKey:@"position"];
}
return self;
}
@end