Merge branch 'release/beta5'

This commit is contained in:
Riley Testut 2017-10-16 13:47:49 -07:00
commit 93ffb83e94
183 changed files with 15651 additions and 2721 deletions

3
.gitmodules vendored
View File

@ -10,3 +10,6 @@
[submodule "Cores/GBADeltaCore"]
path = Cores/GBADeltaCore
url = git@github.com:rileytestut/GBADeltaCore.git
[submodule "Cores/GBCDeltaCore"]
path = Cores/GBCDeltaCore
url = git@github.com:rileytestut/GBCDeltaCore.git

5168
Artwork/Icon.ai Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -1 +1 @@
Subproject commit 99da200c440a8dad93d9b92314f91b933b659bf8
Subproject commit d7213260852683ea5a3f6c2274733bbd65d2ec92

@ -1 +1 @@
Subproject commit ae5ab38dbe6a5fc995f4f3a8cb7075dad3f8f6d5
Subproject commit 7babfcf93a32916ad06989c9a63681acf7f07d50

1
Cores/GBCDeltaCore Submodule

@ -0,0 +1 @@
Subproject commit 9d714254ace7bd5d63805d434f4ccd1f2a947951

@ -1 +1 @@
Subproject commit e49d318e2e18034b2bccea34047800956298de4a
Subproject commit a354c3700fc4576bbefb6a8324bcd7a121d9b934

View File

@ -27,12 +27,12 @@
BF0418151D01E93400E85BCF /* GBADeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF0418131D01E93400E85BCF /* GBADeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */; };
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; };
BF11734D1DA32A5200047DF8 /* GameType+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF11734C1DA32A5200047DF8 /* GameType+Localization.swift */; };
BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */; };
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */; };
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */; };
BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF15AF831F54B43B009B6AAB /* ActionInput.swift */; };
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */; };
BF1DAD5D1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift */; };
BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */; };
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; };
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; };
BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF31878A1D489AAA00BD020D /* CheatValidator.swift */; };
@ -40,19 +40,20 @@
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA101CF1899D006624C7 /* CheatTextView.swift */; };
BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; };
BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF353FF41C5D837600C1184C /* PauseMenu.storyboard */; };
BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF81C5D870B00C1184C /* PauseItem.swift */; };
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF81C5D870B00C1184C /* MenuItem.swift */; };
BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */; };
BF3540001C5DA3C500C1184C /* PausePresentationControllerContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */; };
BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */; };
BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */; };
BF59425C1E09BB810051894B /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942581E09BB810051894B /* Action.swift */; };
BF4828841F9027B600028B97 /* Delta.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BF4828811F9027B600028B97 /* Delta.xcdatamodeld */; };
BF4828861F9028F500028B97 /* System.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4828851F9028F500028B97 /* System.swift */; };
BF4828881F90290F00028B97 /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4828871F90290F00028B97 /* Action.swift */; };
BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942611E09BBB10051894B /* LoadControllerSkinImageOperation.swift */; };
BF5942661E09BBB10051894B /* LoadImageURLOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942631E09BBB10051894B /* LoadImageURLOperation.swift */; };
BF59426A1E09BBD00051894B /* GridCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942681E09BBD00051894B /* GridCollectionViewCell.swift */; };
BF59426B1E09BBD00051894B /* GridCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942691E09BBD00051894B /* GridCollectionViewLayout.swift */; };
BF59426F1E09BC5D0051894B /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF59426D1E09BC5D0051894B /* DatabaseManager.swift */; };
BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF59426E1E09BC5D0051894B /* GamesDatabase.swift */; };
BF5942731E09BC700051894B /* Model.xcdatamodel in Sources */ = {isa = PBXBuildFile; fileRef = BF5942721E09BC700051894B /* Model.xcdatamodel */; };
BF59427C1E09BC830051894B /* Cheat.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942771E09BC830051894B /* Cheat.swift */; };
BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942781E09BC830051894B /* ControllerSkin.swift */; };
BF59427E1E09BC830051894B /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942791E09BC830051894B /* Game.swift */; };
@ -69,13 +70,26 @@
BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */; };
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */; };
BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF5E7F451B9A652600AE44F8 /* Settings.storyboard */; };
BF6424831F5B8F3F00D6AB44 /* ListMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6424821F5B8F3F00D6AB44 /* ListMenuViewController.swift */; };
BF6424851F5CBDC900D6AB44 /* UIView+ParentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */; };
BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */; };
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF696B7F1D9B2B02009639E0 /* Theme.swift */; };
BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3121EB7E47F008E83CD /* ImportOption.swift */; };
BF6BF3181EB82111008E83CD /* iTunesImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */; };
BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */; };
BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */; };
BF6BF3211EB82362008E83CD /* GamesDatabase.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF6BF31F1EB82362008E83CD /* GamesDatabase.storyboard */; };
BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */; };
BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */; };
BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */; };
BF70798C1B6B464B0019077C /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; };
BF70798D1B6B464B0019077C /* ZipZap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */; };
BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */; };
BF7AE8081C2E858400B1B5BC /* GridMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */; };
BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */; };
BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */; };
BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CA9351F5F651900499FDD /* PopoverMenuController.swift */; };
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */; };
BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2761E4977BF0030E7AD /* GameMetadata.swift */; };
BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */; };
BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */; };
@ -85,14 +99,21 @@
BF9F4FD01AAD7B87004C9500 /* DeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63BDE91D389EEB00FCB040 /* GameViewController.swift */; };
BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FEC1B8AA4FA00495943 /* Settings.swift */; };
BFC314771E0C8CFC0056E3A8 /* GameType+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC314761E0C8CFC0056E3A8 /* GameType+Delta.swift */; };
BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */; };
BFC6F7B81F435BC500221B96 /* Input+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC6F7B71F435BC500221B96 /* Input+Display.swift */; };
BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */; };
BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */; };
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */; };
BFDC7B9F1F7DE0CF0052A7C5 /* Delta.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BFDC7B9E1F7DE0CF0052A7C5 /* Delta.xcmappingmodel */; };
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */; };
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */; };
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; };
BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; };
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */; };
BFF0742C1E9DC17500ACDF4A /* GBCDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */; };
BFF0742D1E9DC17500ACDF4A /* GBCDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BFF6452E1F7CC5060056533E /* GameControllerInputMappingTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6B82A41F7CC2A300042BFB /* GameControllerInputMappingTransformer.swift */; };
BFF93AA01E0FB036005EC865 /* InputStreamOutputWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */; };
BFFA4C091E8A24D600D87934 /* GameMetadataTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA4C081E8A24D600D87934 /* GameMetadataTableViewCell.swift */; };
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; };
@ -113,6 +134,7 @@
dstSubfolderSpec = 10;
files = (
BF9F4FD01AAD7B87004C9500 /* DeltaCore.framework in Embed Frameworks */,
BFF0742D1E9DC17500ACDF4A /* GBCDeltaCore.framework in Embed Frameworks */,
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */,
BF70798D1B6B464B0019077C /* ZipZap.framework in Embed Frameworks */,
BF0418151D01E93400E85BCF /* GBADeltaCore.framework in Embed Frameworks */,
@ -129,88 +151,109 @@
A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.release.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.release.xcconfig"; sourceTree = "<group>"; };
BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = openvgdb.sqlite; sourceTree = "<group>"; };
BF0418131D01E93400E85BCF /* GBADeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBADeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkinsViewController.swift; path = "Controller Skins/ControllerSkinsViewController.swift"; sourceTree = "<group>"; };
BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerSkinsViewController.swift; sourceTree = "<group>"; };
BF090CF11B490D8300DCAB45 /* Delta-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Delta-Bridging-Header.h"; sourceTree = "<group>"; };
BF107EC31BF413F000E0C32C /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = "<group>"; };
BF11734C1DA32A5200047DF8 /* GameType+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GameType+Localization.swift"; sourceTree = "<group>"; };
BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllersSettingsViewController.swift; path = Controllers/ControllersSettingsViewController.swift; sourceTree = "<group>"; };
BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllersSettingsViewController.swift; sourceTree = "<group>"; };
BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewGameViewController.swift; sourceTree = "<group>"; };
BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmulatorCore+Cheats.swift"; sourceTree = "<group>"; };
BF15AF831F54B43B009B6AAB /* ActionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionInput.swift; sourceTree = "<group>"; };
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Importing.swift"; sourceTree = "<group>"; };
BF1DAD5C1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameTypeControllerSkinsViewController.swift; path = "Controller Skins/GameTypeControllerSkinsViewController.swift"; sourceTree = "<group>"; };
BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemControllerSkinsViewController.swift; sourceTree = "<group>"; };
BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = "<group>"; };
BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = "<group>"; };
BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; };
BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesCollectionHeaderView.swift; path = "Pause Menu/Save States/SaveStatesCollectionHeaderView.swift"; sourceTree = "<group>"; };
BF31878A1D489AAA00BD020D /* CheatValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatValidator.swift; path = "Pause Menu/Cheats/CheatValidator.swift"; sourceTree = "<group>"; };
BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditCheatViewController.swift; path = "Pause Menu/Cheats/EditCheatViewController.swift"; sourceTree = "<group>"; };
BF34FA101CF1899D006624C7 /* CheatTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatTextView.swift; path = "Pause Menu/Cheats/CheatTextView.swift"; sourceTree = "<group>"; };
BF353FF11C5D7FB000C1184C /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseViewController.swift; path = "Pause Menu/PauseViewController.swift"; sourceTree = "<group>"; };
BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesCollectionHeaderView.swift; sourceTree = "<group>"; };
BF31878A1D489AAA00BD020D /* CheatValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatValidator.swift; sourceTree = "<group>"; };
BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditCheatViewController.swift; sourceTree = "<group>"; };
BF34FA101CF1899D006624C7 /* CheatTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatTextView.swift; sourceTree = "<group>"; };
BF353FF11C5D7FB000C1184C /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PauseViewController.swift; sourceTree = "<group>"; };
BF353FF51C5D837600C1184C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PauseMenu.storyboard; sourceTree = "<group>"; };
BF353FF81C5D870B00C1184C /* PauseItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseItem.swift; path = "Pause Menu/PauseItem.swift"; sourceTree = "<group>"; };
BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PausePresentationController.swift; path = "Pause Menu/Presentation Controller/PausePresentationController.swift"; sourceTree = "<group>"; };
BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = PausePresentationControllerContentView.xib; path = "Pause Menu/Presentation Controller/PausePresentationControllerContentView.xib"; sourceTree = "<group>"; };
BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseStoryboardSegue.swift; path = "Pause Menu/Segues/PauseStoryboardSegue.swift"; sourceTree = "<group>"; };
BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesViewController.swift; path = "Pause Menu/Save States/SaveStatesViewController.swift"; sourceTree = "<group>"; };
BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseTransitionCoordinator.swift; path = "Pause Menu/Segues/PauseTransitionCoordinator.swift"; sourceTree = "<group>"; };
BF5942581E09BB810051894B /* Action.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Action.swift; path = Components/Action.swift; sourceTree = "<group>"; };
BF5942611E09BBB10051894B /* LoadControllerSkinImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadControllerSkinImageOperation.swift; path = Components/Loading/LoadControllerSkinImageOperation.swift; sourceTree = "<group>"; };
BF5942631E09BBB10051894B /* LoadImageURLOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadImageURLOperation.swift; path = Components/Loading/LoadImageURLOperation.swift; sourceTree = "<group>"; };
BF5942681E09BBD00051894B /* GridCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewCell.swift; path = "Components/Collection View/GridCollectionViewCell.swift"; sourceTree = "<group>"; };
BF5942691E09BBD00051894B /* GridCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewLayout.swift; path = "Components/Collection View/GridCollectionViewLayout.swift"; sourceTree = "<group>"; };
BF59426D1E09BC5D0051894B /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DatabaseManager.swift; path = Database/DatabaseManager.swift; sourceTree = "<group>"; };
BF59426E1E09BC5D0051894B /* GamesDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabase.swift; path = Database/OpenVGDB/GamesDatabase.swift; sourceTree = "<group>"; };
BF5942721E09BC700051894B /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; name = Model.xcdatamodel; path = Database/Model/Model.xcdatamodel; sourceTree = "<group>"; };
BF5942771E09BC830051894B /* Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Cheat.swift; path = Database/Model/Human/Cheat.swift; sourceTree = "<group>"; };
BF5942781E09BC830051894B /* ControllerSkin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkin.swift; path = Database/Model/Human/ControllerSkin.swift; sourceTree = "<group>"; };
BF5942791E09BC830051894B /* Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Game.swift; path = Database/Model/Human/Game.swift; sourceTree = "<group>"; };
BF59427A1E09BC830051894B /* GameCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameCollection.swift; path = Database/Model/Human/GameCollection.swift; sourceTree = "<group>"; };
BF59427B1E09BC830051894B /* SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveState.swift; path = Database/Model/Human/SaveState.swift; sourceTree = "<group>"; };
BF5942811E09BC8B0051894B /* _Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = _Cheat.swift; path = Database/Model/Machine/_Cheat.swift; sourceTree = "<group>"; };
BF5942821E09BC8B0051894B /* _ControllerSkin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = _ControllerSkin.swift; path = Database/Model/Machine/_ControllerSkin.swift; sourceTree = "<group>"; };
BF5942831E09BC8B0051894B /* _Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = _Game.swift; path = Database/Model/Machine/_Game.swift; sourceTree = "<group>"; };
BF5942841E09BC8B0051894B /* _GameCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = _GameCollection.swift; path = Database/Model/Machine/_GameCollection.swift; sourceTree = "<group>"; };
BF5942851E09BC8B0051894B /* _SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = _SaveState.swift; path = Database/Model/Machine/_SaveState.swift; sourceTree = "<group>"; };
BF59428B1E09BC930051894B /* ControllerSkinConfigurations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ControllerSkinConfigurations.h; path = Database/Model/Misc/ControllerSkinConfigurations.h; sourceTree = "<group>"; };
BF59428D1E09BCFB0051894B /* ImportController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImportController.swift; path = Components/Importing/ImportController.swift; sourceTree = "<group>"; };
BF353FF81C5D870B00C1184C /* MenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuItem.swift; sourceTree = "<group>"; };
BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PausePresentationController.swift; sourceTree = "<group>"; };
BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = PausePresentationControllerContentView.xib; path = Delta/Base.lproj/PausePresentationControllerContentView.xib; sourceTree = SOURCE_ROOT; };
BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PauseStoryboardSegue.swift; sourceTree = "<group>"; };
BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesViewController.swift; sourceTree = "<group>"; };
BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PauseTransitionCoordinator.swift; sourceTree = "<group>"; };
BF4828821F9027B600028B97 /* Delta 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 2.xcdatamodel"; sourceTree = "<group>"; };
BF4828831F9027B600028B97 /* Delta.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Delta.xcdatamodel; sourceTree = "<group>"; };
BF4828851F9028F500028B97 /* System.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = System.swift; sourceTree = "<group>"; };
BF4828871F90290F00028B97 /* Action.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
BF5942611E09BBB10051894B /* LoadControllerSkinImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadControllerSkinImageOperation.swift; sourceTree = "<group>"; };
BF5942631E09BBB10051894B /* LoadImageURLOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadImageURLOperation.swift; sourceTree = "<group>"; };
BF5942681E09BBD00051894B /* GridCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridCollectionViewCell.swift; sourceTree = "<group>"; };
BF5942691E09BBD00051894B /* GridCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridCollectionViewLayout.swift; sourceTree = "<group>"; };
BF59426D1E09BC5D0051894B /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
BF59426E1E09BC5D0051894B /* GamesDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesDatabase.swift; sourceTree = "<group>"; };
BF5942771E09BC830051894B /* Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cheat.swift; sourceTree = "<group>"; };
BF5942781E09BC830051894B /* ControllerSkin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerSkin.swift; sourceTree = "<group>"; };
BF5942791E09BC830051894B /* Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = "<group>"; };
BF59427A1E09BC830051894B /* GameCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollection.swift; sourceTree = "<group>"; };
BF59427B1E09BC830051894B /* SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveState.swift; sourceTree = "<group>"; };
BF5942811E09BC8B0051894B /* _Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _Cheat.swift; sourceTree = "<group>"; };
BF5942821E09BC8B0051894B /* _ControllerSkin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _ControllerSkin.swift; sourceTree = "<group>"; };
BF5942831E09BC8B0051894B /* _Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _Game.swift; sourceTree = "<group>"; };
BF5942841E09BC8B0051894B /* _GameCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _GameCollection.swift; sourceTree = "<group>"; };
BF5942851E09BC8B0051894B /* _SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _SaveState.swift; sourceTree = "<group>"; };
BF59428B1E09BC930051894B /* ControllerSkinConfigurations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ControllerSkinConfigurations.h; sourceTree = "<group>"; };
BF59428D1E09BCFB0051894B /* ImportController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportController.swift; sourceTree = "<group>"; };
BF59428F1E09BD1A0051894B /* NSFetchedResultsController+Conveniences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSFetchedResultsController+Conveniences.h"; sourceTree = "<group>"; };
BF5942901E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFetchedResultsController+Conveniences.m"; sourceTree = "<group>"; };
BF5942911E09BD1A0051894B /* NSManagedObject+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Conveniences.swift"; sourceTree = "<group>"; };
BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; };
BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
BF5E7F451B9A652600AE44F8 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = "<group>"; };
BF5E7F451B9A652600AE44F8 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Settings.storyboard; path = Delta/Base.lproj/Settings.storyboard; sourceTree = SOURCE_ROOT; };
BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerInputsViewController.swift; sourceTree = "<group>"; };
BF63BDE91D389EEB00FCB040 /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = "<group>"; };
BF6424821F5B8F3F00D6AB44 /* ListMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListMenuViewController.swift; sourceTree = "<group>"; };
BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+ParentViewController.swift"; sourceTree = "<group>"; };
BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ControllerSkin+Configuring.swift"; sourceTree = "<group>"; };
BF696B7F1D9B2B02009639E0 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = Theming/Theme.swift; sourceTree = "<group>"; };
BF696B7F1D9B2B02009639E0 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
BF6B82A41F7CC2A300042BFB /* GameControllerInputMappingTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerInputMappingTransformer.swift; sourceTree = "<group>"; };
BF6BB2451BB73FE800CCF94A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
BF6BF3121EB7E47F008E83CD /* ImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportOption.swift; sourceTree = "<group>"; };
BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iTunesImportOption.swift; sourceTree = "<group>"; };
BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClipboardImportOption.swift; sourceTree = "<group>"; };
BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesDatabaseImportOption.swift; sourceTree = "<group>"; };
BF6BF3201EB82362008E83CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/GamesDatabase.storyboard; sourceTree = "<group>"; };
BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoLibraryImportOption.swift; sourceTree = "<group>"; };
BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _GameControllerInputMapping.swift; sourceTree = "<group>"; };
BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerInputMapping.swift; sourceTree = "<group>"; };
BF70798B1B6B464B0019077C /* ZipZap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ZipZap.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+FontSize.swift"; sourceTree = "<group>"; };
BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseMenuViewController.swift; path = "Pause Menu/PauseMenuViewController.swift"; sourceTree = "<group>"; };
BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMenuViewController.swift; sourceTree = "<group>"; };
BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Delta.swift"; sourceTree = "<group>"; };
BF95E2761E4977BF0030E7AD /* GameMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameMetadata.swift; path = Database/OpenVGDB/GameMetadata.swift; sourceTree = "<group>"; };
BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabaseBrowserViewController.swift; path = Database/OpenVGDB/GamesDatabaseBrowserViewController.swift; sourceTree = "<group>"; };
BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkinTableViewCell.swift; path = "Controller Skins/ControllerSkinTableViewCell.swift"; sourceTree = "<group>"; };
BF8CA9351F5F651900499FDD /* PopoverMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverMenuController.swift; sourceTree = "<group>"; };
BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputCalloutView.swift; sourceTree = "<group>"; };
BF95E2761E4977BF0030E7AD /* GameMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameMetadata.swift; sourceTree = "<group>"; };
BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesDatabaseBrowserViewController.swift; sourceTree = "<group>"; };
BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerSkinTableViewCell.swift; sourceTree = "<group>"; };
BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BFAA1FEC1B8AA4FA00495943 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeltaCoreProtocol+Delta.swift"; sourceTree = "<group>"; };
BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SNESDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BFC314761E0C8CFC0056E3A8 /* GameType+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GameType+Delta.swift"; sourceTree = "<group>"; };
BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatsViewController.swift; path = "Pause Menu/Cheats/CheatsViewController.swift"; sourceTree = "<group>"; };
BFC6F7B71F435BC500221B96 /* Input+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Input+Display.swift"; sourceTree = "<group>"; };
BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatsViewController.swift; sourceTree = "<group>"; };
BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewControllerContextTransitioning+Conveniences.swift"; sourceTree = "<group>"; };
BFDC7B9E1F7DE0CF0052A7C5 /* Delta.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Delta.xcmappingmodel; sourceTree = "<group>"; };
BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollectionViewController.swift; sourceTree = "<group>"; };
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesStoryboardSegue.swift; path = Segues/SaveStatesStoryboardSegue.swift; sourceTree = "<group>"; };
BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverMenuButton.swift; sourceTree = "<group>"; };
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesStoryboardSegue.swift; sourceTree = "<group>"; };
BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InputStreamOutputWriter.swift; path = Components/Importing/InputStreamOutputWriter.swift; sourceTree = "<group>"; };
BFFA4C081E8A24D600D87934 /* GameMetadataTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameMetadataTableViewCell.swift; path = Database/OpenVGDB/GameMetadataTableViewCell.swift; sourceTree = "<group>"; };
BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveStateMigrationPolicy.swift; sourceTree = "<group>"; };
BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBCDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputStreamOutputWriter.swift; sourceTree = "<group>"; };
BFFA4C081E8A24D600D87934 /* GameMetadataTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameMetadataTableViewCell.swift; sourceTree = "<group>"; };
BFFA71D71AAC406100EE9DD1 /* Delta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Delta.app; sourceTree = BUILT_PRODUCTS_DIR; };
BFFA71DB1AAC406100EE9DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
BFFA71E11AAC406100EE9DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesPresentationController.swift; path = Segues/GamesPresentationController.swift; sourceTree = "<group>"; };
BFFC461C1D59823500AF2CC6 /* GamesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesStoryboardSegue.swift; path = Segues/GamesStoryboardSegue.swift; sourceTree = "<group>"; };
BFFC461D1D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InitialGamesStoryboardSegue.swift; path = Segues/InitialGamesStoryboardSegue.swift; sourceTree = "<group>"; };
BFFC46221D5984A000AF2CC6 /* LaunchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LaunchViewController.swift; path = Launch/LaunchViewController.swift; sourceTree = "<group>"; };
BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesPresentationController.swift; sourceTree = "<group>"; };
BFFC461C1D59823500AF2CC6 /* GamesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesStoryboardSegue.swift; sourceTree = "<group>"; };
BFFC461D1D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialGamesStoryboardSegue.swift; sourceTree = "<group>"; };
BFFC46221D5984A000AF2CC6 /* LaunchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
BFFC46451D59861000AF2CC6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
BFFC464B1D5998D600AF2CC6 /* CheatTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatTableViewCell.swift; path = "Pause Menu/Cheats/CheatTableViewCell.swift"; sourceTree = "<group>"; };
BFFC464B1D5998D600AF2CC6 /* CheatTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatTableViewCell.swift; sourceTree = "<group>"; };
C786AF1D2DDB6223BE2063CC /* Pods-Delta.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -224,6 +267,7 @@
BF99C6941D0A9AA600BA92BC /* SNESDeltaCore.framework in Frameworks */,
BF70798C1B6B464B0019077C /* ZipZap.framework in Frameworks */,
BF0418141D01E93400E85BCF /* GBADeltaCore.framework in Frameworks */,
BFF0742C1E9DC17500ACDF4A /* GBCDeltaCore.framework in Frameworks */,
4FE8465FD28810191C3E5212 /* Pods_Delta.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -238,14 +282,15 @@
BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */,
BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */,
BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */,
BF11734C1DA32A5200047DF8 /* GameType+Localization.swift */,
BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */,
BF59428F1E09BD1A0051894B /* NSFetchedResultsController+Conveniences.h */,
BF5942901E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m */,
BF5942911E09BD1A0051894B /* NSManagedObject+Conveniences.swift */,
BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */,
BFC314761E0C8CFC0056E3A8 /* GameType+Delta.swift */,
BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */,
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */,
BFC6F7B71F435BC500221B96 /* Input+Display.swift */,
BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -254,18 +299,20 @@
isa = PBXGroup;
children = (
BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */,
BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */,
BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */,
);
name = Controllers;
path = Controllers;
sourceTree = "<group>";
};
BF1DAD5B1D9F574900E752A7 /* Controller Skins */ = {
isa = PBXGroup;
children = (
BF1DAD5C1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift */,
BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */,
BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */,
BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */,
);
name = "Controller Skins";
path = "Controller Skins";
sourceTree = "<group>";
};
BF353FFB1C5DA2F600C1184C /* Presentation Controller */ = {
@ -274,7 +321,7 @@
BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */,
BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */,
);
name = "Presentation Controller";
path = "Presentation Controller";
sourceTree = "<group>";
};
BF3540031C5DA6D800C1184C /* Save States */ = {
@ -283,7 +330,7 @@
BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */,
BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */,
);
name = "Save States";
path = "Save States";
sourceTree = "<group>";
};
BF46894D1AAC469800A2586D /* Game Selection */ = {
@ -299,12 +346,12 @@
BF5942571E09BB5D0051894B /* Components */ = {
isa = PBXGroup;
children = (
BF5942581E09BB810051894B /* Action.swift */,
BF59428C1E09BCE50051894B /* Importing */,
BF4828871F90290F00028B97 /* Action.swift */,
BFE0229C1F5B56840052D888 /* Popover Menu */,
BF5942671E09BBB70051894B /* Collection View */,
BF5942601E09BBA80051894B /* Loading */,
);
name = Components;
path = Components;
sourceTree = "<group>";
};
BF5942601E09BBA80051894B /* Loading */ = {
@ -313,7 +360,7 @@
BF5942631E09BBB10051894B /* LoadImageURLOperation.swift */,
BF5942611E09BBB10051894B /* LoadControllerSkinImageOperation.swift */,
);
name = Loading;
path = Loading;
sourceTree = "<group>";
};
BF5942671E09BBB70051894B /* Collection View */ = {
@ -322,28 +369,31 @@
BF5942681E09BBD00051894B /* GridCollectionViewCell.swift */,
BF5942691E09BBD00051894B /* GridCollectionViewLayout.swift */,
);
name = "Collection View";
path = "Collection View";
sourceTree = "<group>";
};
BF59426C1E09BC450051894B /* Database */ = {
isa = PBXGroup;
children = (
BF59426D1E09BC5D0051894B /* DatabaseManager.swift */,
BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */,
BF5942711E09BC690051894B /* Model */,
BF95E2751E49763D0030E7AD /* OpenVGDB */,
);
name = Database;
path = Database;
sourceTree = "<group>";
};
BF5942711E09BC690051894B /* Model */ = {
isa = PBXGroup;
children = (
BF5942721E09BC700051894B /* Model.xcdatamodel */,
BF4828811F9027B600028B97 /* Delta.xcdatamodeld */,
BF5942741E09BC740051894B /* Human */,
BF5942751E09BC780051894B /* Machine */,
BF6B82A31F7CC29A00042BFB /* Transformers */,
BFEF24F01F7DD4B600454C62 /* Migrations */,
BF5942761E09BC7C0051894B /* Misc */,
);
name = Model;
path = Model;
sourceTree = "<group>";
};
BF5942741E09BC740051894B /* Human */ = {
@ -353,9 +403,10 @@
BF5942781E09BC830051894B /* ControllerSkin.swift */,
BF5942791E09BC830051894B /* Game.swift */,
BF59427A1E09BC830051894B /* GameCollection.swift */,
BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */,
BF59427B1E09BC830051894B /* SaveState.swift */,
);
name = Human;
path = Human;
sourceTree = "<group>";
};
BF5942751E09BC780051894B /* Machine */ = {
@ -365,9 +416,10 @@
BF5942821E09BC8B0051894B /* _ControllerSkin.swift */,
BF5942831E09BC8B0051894B /* _Game.swift */,
BF5942841E09BC8B0051894B /* _GameCollection.swift */,
BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */,
BF5942851E09BC8B0051894B /* _SaveState.swift */,
);
name = Machine;
path = Machine;
sourceTree = "<group>";
};
BF5942761E09BC7C0051894B /* Misc */ = {
@ -375,16 +427,17 @@
children = (
BF59428B1E09BC930051894B /* ControllerSkinConfigurations.h */,
);
name = Misc;
path = Misc;
sourceTree = "<group>";
};
BF59428C1E09BCE50051894B /* Importing */ = {
isa = PBXGroup;
children = (
BF59428D1E09BCFB0051894B /* ImportController.swift */,
BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */,
BF6BF3121EB7E47F008E83CD /* ImportOption.swift */,
BF6BF3161EB820F4008E83CD /* Import Options */,
);
name = Importing;
path = Importing;
sourceTree = "<group>";
};
BF696B7E1D9B2AE6009639E0 /* Theming */ = {
@ -392,7 +445,26 @@
children = (
BF696B7F1D9B2B02009639E0 /* Theme.swift */,
);
name = Theming;
path = Theming;
sourceTree = "<group>";
};
BF6B82A31F7CC29A00042BFB /* Transformers */ = {
isa = PBXGroup;
children = (
BF6B82A41F7CC2A300042BFB /* GameControllerInputMappingTransformer.swift */,
);
path = Transformers;
sourceTree = "<group>";
};
BF6BF3161EB820F4008E83CD /* Import Options */ = {
isa = PBXGroup;
children = (
BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */,
BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */,
BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */,
BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */,
);
path = "Import Options";
sourceTree = "<group>";
};
BF7AE7FA1C2E851F00B1B5BC /* Pause Menu */ = {
@ -400,14 +472,14 @@
children = (
BF353FF41C5D837600C1184C /* PauseMenu.storyboard */,
BF353FF11C5D7FB000C1184C /* PauseViewController.swift */,
BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */,
BF353FF81C5D870B00C1184C /* PauseItem.swift */,
BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */,
BF353FF81C5D870B00C1184C /* MenuItem.swift */,
BF3540031C5DA6D800C1184C /* Save States */,
BFC9B7371CEFCD08008629BB /* Cheats */,
BF353FFB1C5DA2F600C1184C /* Presentation Controller */,
BF912E481C5CB5D50041527C /* Segues */,
);
name = "Pause Menu";
path = "Pause Menu";
sourceTree = "<group>";
};
BF912E481C5CB5D50041527C /* Segues */ = {
@ -416,23 +488,33 @@
BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */,
BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */,
);
name = Segues;
path = Segues;
sourceTree = "<group>";
};
BF930FFB1EB6D6EC00E8DBA0 /* Systems */ = {
isa = PBXGroup;
children = (
BF4828851F9028F500028B97 /* System.swift */,
);
path = Systems;
sourceTree = "<group>";
};
BF95E2751E49763D0030E7AD /* OpenVGDB */ = {
isa = PBXGroup;
children = (
BF6BF31F1EB82362008E83CD /* GamesDatabase.storyboard */,
BF59426E1E09BC5D0051894B /* GamesDatabase.swift */,
BF95E2761E4977BF0030E7AD /* GameMetadata.swift */,
BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */,
BFFA4C081E8A24D600D87934 /* GameMetadataTableViewCell.swift */,
);
name = OpenVGDB;
path = OpenVGDB;
sourceTree = "<group>";
};
BF9F4FCD1AAD7B25004C9500 /* Frameworks */ = {
isa = PBXGroup;
children = (
BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */,
BF0418131D01E93400E85BCF /* GBADeltaCore.framework */,
BF27CC941BCB7B7A00A20D89 /* GameController.framework */,
BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */,
@ -467,7 +549,17 @@
BF34FA101CF1899D006624C7 /* CheatTextView.swift */,
BF31878A1D489AAA00BD020D /* CheatValidator.swift */,
);
name = Cheats;
path = Cheats;
sourceTree = "<group>";
};
BFE0229C1F5B56840052D888 /* Popover Menu */ = {
isa = PBXGroup;
children = (
BF8CA9351F5F651900499FDD /* PopoverMenuController.swift */,
BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */,
BF6424821F5B8F3F00D6AB44 /* ListMenuViewController.swift */,
);
path = "Popover Menu";
sourceTree = "<group>";
};
BFEC732F1AAECCBD00650035 /* Resources */ = {
@ -479,12 +571,29 @@
path = Resources;
sourceTree = "<group>";
};
BFEF24F01F7DD4B600454C62 /* Migrations */ = {
isa = PBXGroup;
children = (
BFDC7B9E1F7DE0CF0052A7C5 /* Delta.xcmappingmodel */,
BFEF24F11F7DD4BE00454C62 /* Policies */,
);
path = Migrations;
sourceTree = "<group>";
};
BFEF24F11F7DD4BE00454C62 /* Policies */ = {
isa = PBXGroup;
children = (
BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */,
);
path = Policies;
sourceTree = "<group>";
};
BFFA71CE1AAC406100EE9DD1 = {
isa = PBXGroup;
children = (
BFFA71D91AAC406100EE9DD1 /* Delta */,
BFEC732F1AAECCBD00650035 /* Resources */,
BF9F4FCD1AAD7B25004C9500 /* Frameworks */,
BFEC732F1AAECCBD00650035 /* Resources */,
BFFA71D81AAC406100EE9DD1 /* Products */,
FD1E8AE87FA2DB8793F7B937 /* Pods */,
);
@ -509,6 +618,8 @@
BF7AE7FA1C2E851F00B1B5BC /* Pause Menu */,
BFAA1FEB1B8AA4E800495943 /* Settings */,
BF59426C1E09BC450051894B /* Database */,
BF59428C1E09BCE50051894B /* Importing */,
BF930FFB1EB6D6EC00E8DBA0 /* Systems */,
BF5942571E09BB5D0051894B /* Components */,
BF696B7E1D9B2AE6009639E0 /* Theming */,
BF090CEE1B490C1A00DCAB45 /* Extensions */,
@ -532,6 +643,7 @@
children = (
BF63BDE91D389EEB00FCB040 /* GameViewController.swift */,
BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */,
BF15AF831F54B43B009B6AAB /* ActionInput.swift */,
);
path = Emulation;
sourceTree = "<group>";
@ -544,7 +656,7 @@
BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */,
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */,
);
name = Segues;
path = Segues;
sourceTree = "<group>";
};
BFFC46211D59848000AF2CC6 /* Launch */ = {
@ -553,7 +665,7 @@
BFFC46441D59861000AF2CC6 /* LaunchScreen.storyboard */,
BFFC46221D5984A000AF2CC6 /* LaunchViewController.swift */,
);
name = Launch;
path = Launch;
sourceTree = "<group>";
};
FD1E8AE87FA2DB8793F7B937 /* Pods */ = {
@ -579,6 +691,7 @@
BF9F4FCC1AAD7AEE004C9500 /* Embed Frameworks */,
B444B2BB31CBCEE7D86E943D /* [CP] Embed Pods Frameworks */,
3521C8FA1BB6004A6E9FE324 /* [CP] Copy Pods Resources */,
BF6BF3281EB897F6008E83CD /* Fabric */,
);
buildRules = (
);
@ -596,7 +709,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0820;
LastUpgradeCheck = 0910;
ORGANIZATIONNAME = "Riley Testut";
TargetAttributes = {
BF14D8941DE7A512002CA1BE = {
@ -607,7 +720,8 @@
BFFA71D61AAC406100EE9DD1 = {
CreatedOnToolsVersion = 6.3;
DevelopmentTeam = 6XVY5G3U44;
LastSwiftMigration = 0800;
LastSwiftMigration = 0900;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.iCloud = {
enabled = 1;
@ -641,6 +755,7 @@
buildActionMask = 2147483647;
files = (
BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */,
BF6BF3211EB82362008E83CD /* GamesDatabase.storyboard in Resources */,
BF3540001C5DA3C500C1184C /* PausePresentationControllerContentView.xib in Resources */,
BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */,
BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */,
@ -697,6 +812,20 @@
shellPath = /bin/sh;
shellScript = "/usr/local/bin/mogenerator -m \"Delta/Database/Model/Model.xcdatamodel\" --human-dir \"Delta/Database/Model/Human\" --machine-dir \"Delta/Database/Model/Machine\" --swift --template-var scalarsWhenNonOptional=true --template-path \"Delta/Database/Model/mogenerator/templates\"";
};
BF6BF3281EB897F6008E83CD /* Fabric */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = Fabric;
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Fabric/run\" d542629b4f6625cfd5564d27318550321272076d 333118df9345dcec21e4ba0bb7fa8f6c67c4eb41734374e24f6c71a8dcd5c870";
};
DBD91E7D7EC2729786B4C5B1 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -709,7 +838,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@ -719,8 +848,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFC6F7B81F435BC500221B96 /* Input+Display.swift in Sources */,
BF59426A1E09BBD00051894B /* GridCollectionViewCell.swift in Sources */,
BF6BF3181EB82111008E83CD /* iTunesImportOption.swift in Sources */,
BFF6452E1F7CC5060056533E /* GameControllerInputMappingTransformer.swift in Sources */,
BF59427C1E09BC830051894B /* Cheat.swift in Sources */,
BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */,
BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */,
BF59427E1E09BC830051894B /* Game.swift in Sources */,
BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */,
@ -729,27 +862,34 @@
BF5942801E09BC830051894B /* SaveState.swift in Sources */,
BF59428E1E09BCFB0051894B /* ImportController.swift in Sources */,
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */,
BF6424831F5B8F3F00D6AB44 /* ListMenuViewController.swift in Sources */,
BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */,
BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */,
BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */,
BF5942731E09BC700051894B /* Model.xcdatamodel in Sources */,
BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */,
BF4828841F9027B600028B97 /* Delta.xcdatamodeld in Sources */,
BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */,
BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */,
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */,
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */,
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */,
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */,
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */,
BF59426B1E09BBD00051894B /* GridCollectionViewLayout.swift in Sources */,
BF6424851F5CBDC900D6AB44 /* UIView+ParentViewController.swift in Sources */,
BFDC7B9F1F7DE0CF0052A7C5 /* Delta.xcmappingmodel in Sources */,
BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */,
BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */,
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */,
BF1DAD5D1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift in Sources */,
BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */,
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */,
BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */,
BFFA4C091E8A24D600D87934 /* GameMetadataTableViewCell.swift in Sources */,
BFFC46231D5984A000AF2CC6 /* LaunchViewController.swift in Sources */,
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */,
BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */,
BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */,
BF5942881E09BC8B0051894B /* _Game.swift in Sources */,
BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */,
BF11734D1DA32A5200047DF8 /* GameType+Localization.swift in Sources */,
BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */,
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */,
BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */,
@ -758,14 +898,17 @@
BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */,
BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */,
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */,
BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */,
BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */,
BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */,
BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */,
BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */,
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */,
BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */,
BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */,
BFC314771E0C8CFC0056E3A8 /* GameType+Delta.swift in Sources */,
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */,
BF59426F1E09BC5D0051894B /* DatabaseManager.swift in Sources */,
BF4828861F9028F500028B97 /* System.swift in Sources */,
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */,
BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */,
BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */,
@ -775,12 +918,16 @@
BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */,
BF5942861E09BC8B0051894B /* _Cheat.swift in Sources */,
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */,
BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */,
BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */,
BF5942931E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m in Sources */,
BF4828881F90290F00028B97 /* Action.swift in Sources */,
BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */,
BF5942661E09BBB10051894B /* LoadImageURLOperation.swift in Sources */,
BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */,
BF59425C1E09BB810051894B /* Action.swift in Sources */,
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */,
BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */,
BF7AE8081C2E858400B1B5BC /* GridMenuViewController.swift in Sources */,
BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */,
BFF93AA01E0FB036005EC865 /* InputStreamOutputWriter.swift in Sources */,
BF59427F1E09BC830051894B /* GameCollection.swift in Sources */,
);
@ -795,7 +942,17 @@
BF353FF51C5D837600C1184C /* Base */,
);
name = PauseMenu.storyboard;
sourceTree = "<group>";
path = Delta;
sourceTree = SOURCE_ROOT;
};
BF6BF31F1EB82362008E83CD /* GamesDatabase.storyboard */ = {
isa = PBXVariantGroup;
children = (
BF6BF3201EB82362008E83CD /* Base */,
);
name = GamesDatabase.storyboard;
path = Delta;
sourceTree = SOURCE_ROOT;
};
BFFA71E01AAC406100EE9DD1 /* Main.storyboard */ = {
isa = PBXVariantGroup;
@ -803,8 +960,8 @@
BFFA71E11AAC406100EE9DD1 /* Base */,
);
name = Main.storyboard;
path = .;
sourceTree = "<group>";
path = Delta;
sourceTree = SOURCE_ROOT;
};
BFFC46441D59861000AF2CC6 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
@ -812,7 +969,8 @@
BFFC46451D59861000AF2CC6 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
path = Delta;
sourceTree = SOURCE_ROOT;
};
/* End PBXVariantGroup section */
@ -841,20 +999,26 @@
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
@ -891,14 +1055,20 @@
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
@ -934,12 +1104,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Delta/Supporting Files/Delta.entitlements";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEVELOPMENT_TEAM = 6XVY5G3U44;
INFOPLIST_FILE = "Delta/Supporting Files/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 4.0;
};
name = Debug;
};
@ -950,12 +1125,18 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Delta/Supporting Files/Delta.entitlements";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEVELOPMENT_TEAM = 6XVY5G3U44;
INFOPLIST_FILE = "Delta/Supporting Files/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DIMPACTOR";
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 3.0;
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 4.0;
};
name = Release;
};
@ -990,6 +1171,20 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCVersionGroup section */
BF4828811F9027B600028B97 /* Delta.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
BF4828821F9027B600028B97 /* Delta 2.xcdatamodel */,
BF4828831F9027B600028B97 /* Delta.xcdatamodel */,
);
currentVersion = BF4828821F9027B600028B97 /* Delta 2.xcdatamodel */;
path = Delta.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = BFFA71CF1AAC406100EE9DD1 /* Project object */;
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0820"
LastUpgradeVersion = "0910"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
@ -62,6 +62,20 @@
ReferencedContainer = "container:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF8F2AAD1E9C879300F89F15"
BuildableName = "GBCDeltaCore.framework"
BlueprintName = "GBCDeltaCore"
ReferencedContainer = "container:Cores/GBCDeltaCore/GBCDeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
@ -82,6 +96,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@ -101,6 +116,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@ -13,6 +13,9 @@
<FileRef
location = "group:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
</FileRef>
<FileRef
location = "group:Cores/GBCDeltaCore/GBCDeltaCore.xcodeproj">
</FileRef>
<FileRef
location = "group:External/Roxas/Roxas.xcodeproj">
</FileRef>

View File

@ -9,8 +9,9 @@
import UIKit
import DeltaCore
import SNESDeltaCore
import GBADeltaCore
import Fabric
import Crashlytics
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
@ -19,10 +20,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
Fabric.with([Crashlytics.self])
Settings.registerDefaults()
Delta.register(SNES.core)
Delta.register(GBA.core)
System.supportedSystems.forEach { Delta.register($0.deltaCore) }
self.configureAppearance()
@ -38,7 +40,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate
}
// Controllers
ExternalControllerManager.shared.startMonitoringExternalControllers()
ExternalGameControllerManager.shared.startMonitoring()
return true
}
@ -77,7 +79,7 @@ extension AppDelegate
{
self.window?.tintColor = UIColor.deltaPurple
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes[NSForegroundColorAttributeName] = UIColor.white
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes[NSAttributedStringKey.foregroundColor.rawValue] = UIColor.white
}
}
@ -88,12 +90,11 @@ extension AppDelegate
return self.openURL(url)
}
@discardableResult fileprivate func openURL(_ url: URL) -> Bool
@discardableResult private func openURL(_ url: URL) -> Bool
{
guard url.isFileURL else { return false }
let gameType = GameType.gameType(forFileExtension: url.pathExtension)
if gameType != .unknown || url.pathExtension.lowercased() == "zip"
if GameType(fileExtension: url.pathExtension) != nil || url.pathExtension.lowercased() == "zip"
{
return self.importGame(at: url)
}

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13528" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13526"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Games Database-->
<scene sceneID="S7I-gw-igt">
<objects>
<tableViewController id="SB6-jW-dhZ" customClass="GamesDatabaseBrowserViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="97" sectionHeaderHeight="28" sectionFooterHeight="28" id="bJf-Sa-ZOX">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="4cJ-4B-Kgt" customClass="GameMetadataTableViewCell" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="97"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="4cJ-4B-Kgt" id="7ze-s0-mpI">
<rect key="frame" x="0.0" y="0.0" width="375" height="96.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DSH-Hk-snb" userLabel="Selected Background View">
<rect key="frame" x="0.0" y="0.0" width="375" height="97.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BoxArt" translatesAutoresizingMaskIntoConstraints="NO" id="tNY-2F-llo">
<rect key="frame" x="15" y="8" width="80" height="80"/>
<constraints>
<constraint firstAttribute="width" secondItem="tNY-2F-llo" secondAttribute="height" multiplier="1:1" id="f4E-bV-L96"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Super Mario World" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DND-Fv-FyB">
<rect key="frame" x="110" y="38.5" width="250" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="DND-Fv-FyB" firstAttribute="leading" secondItem="tNY-2F-llo" secondAttribute="trailing" constant="15" id="71e-t3-7Av"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="DND-Fv-FyB" secondAttribute="bottom" constant="1" id="9No-RE-0xx"/>
<constraint firstItem="DND-Fv-FyB" firstAttribute="top" relation="greaterThanOrEqual" secondItem="7ze-s0-mpI" secondAttribute="top" constant="1" id="F9q-6H-sqC"/>
<constraint firstAttribute="trailing" secondItem="DND-Fv-FyB" secondAttribute="trailing" constant="15" id="KFv-7n-LrD"/>
<constraint firstItem="DND-Fv-FyB" firstAttribute="centerY" secondItem="7ze-s0-mpI" secondAttribute="centerY" id="YBX-t4-jkR"/>
<constraint firstItem="tNY-2F-llo" firstAttribute="top" secondItem="7ze-s0-mpI" secondAttribute="top" constant="8" id="bYX-gA-QvB"/>
<constraint firstAttribute="bottom" secondItem="tNY-2F-llo" secondAttribute="bottom" constant="8" id="fxr-wr-I6X"/>
<constraint firstItem="tNY-2F-llo" firstAttribute="leading" secondItem="7ze-s0-mpI" secondAttribute="leading" constant="15" id="hX2-Gr-Bnz"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="artworkImageView" destination="tNY-2F-llo" id="GqY-jv-rso"/>
<outlet property="artworkImageViewLeadingConstraint" destination="hX2-Gr-Bnz" id="be8-dr-c8K"/>
<outlet property="artworkImageViewTrailingConstraint" destination="71e-t3-7Av" id="y62-KO-y1r"/>
<outlet property="nameLabel" destination="DND-Fv-FyB" id="LhN-cA-8Hy"/>
<outlet property="selectedBackgroundView" destination="DSH-Hk-snb" id="hLY-4k-VxU"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="SB6-jW-dhZ" id="2aq-ZA-84E"/>
<outlet property="delegate" destination="SB6-jW-dhZ" id="WgY-cp-m7K"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff">
<connections>
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindFromGamesDatabaseBrowserWith:" id="zdg-Az-WwQ"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="f3a-hX-Qnu" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="mUU-ug-yNs" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="2652" y="1001.649175412294"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="REv-V5-eEz">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="6bq-zy-UZU" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="uzY-vR-coL">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="SB6-jW-dhZ" kind="relationship" relationship="rootViewController" id="b0w-Fq-hrk"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Hr9-N6-XXA" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1854" y="1002"/>
</scene>
</scenes>
<resources>
<image name="BoxArt" width="100" height="100"/>
</resources>
</document>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11760" systemVersion="16B2657" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="dkK-ii-Bx4">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13528" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="dkK-ii-Bx4">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11755"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13526"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -47,6 +47,6 @@
</scene>
</scenes>
<resources>
<image name="Delta" width="1280" height="1140"/>
<image name="Delta" width="1342" height="1196"/>
</resources>
</document>

View File

@ -1,12 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16C68" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13528" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13526"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -32,13 +31,13 @@
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="tmn-gd-5UN" secondAttribute="trailing" id="9Rq-HM-vqk"/>
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="tmn-gd-5UN" secondAttribute="bottom" id="DV5-hh-1VN"/>
<constraint firstItem="tmn-gd-5UN" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="f1f-sa-dBA"/>
<constraint firstAttribute="bottom" secondItem="tmn-gd-5UN" secondAttribute="bottom" id="ifM-Wa-u9y"/>
<constraint firstItem="tmn-gd-5UN" firstAttribute="top" secondItem="3Bk-k3-7J9" secondAttribute="top" id="nhS-aC-rUR"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Games" id="pFk-as-3k4">
<barButtonItem key="leftBarButtonItem" image="Settings_Button" id="2gg-lC-FhX">
<barButtonItem key="leftBarButtonItem" image="SettingsButton" id="2gg-lC-FhX">
<connections>
<segue destination="xMK-Cs-fAS" kind="presentation" id="uN5-PN-7FK"/>
</connections>
@ -49,91 +48,16 @@
</connections>
</barButtonItem>
</navigationItem>
<connections>
<segue destination="6bq-zy-UZU" kind="presentation" identifier="gamesDatabaseBrowser" id="7TT-mP-bjt"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1036" y="1002"/>
</scene>
<!--Games Database-->
<scene sceneID="S7I-gw-igt">
<objects>
<tableViewController id="SB6-jW-dhZ" customClass="GamesDatabaseBrowserViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="97" sectionHeaderHeight="28" sectionFooterHeight="28" id="bJf-Sa-ZOX">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="4cJ-4B-Kgt" customClass="GameMetadataTableViewCell" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="97"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="4cJ-4B-Kgt" id="7ze-s0-mpI">
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DSH-Hk-snb" userLabel="Selected Background View">
<rect key="frame" x="0.0" y="0.0" width="375" height="97"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BoxArt" translatesAutoresizingMaskIntoConstraints="NO" id="tNY-2F-llo">
<rect key="frame" x="15" y="8" width="80" height="80"/>
<constraints>
<constraint firstAttribute="width" secondItem="tNY-2F-llo" secondAttribute="height" multiplier="1:1" id="f4E-bV-L96"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Super Mario World" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DND-Fv-FyB">
<rect key="frame" x="110" y="38.5" width="250" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="DND-Fv-FyB" firstAttribute="leading" secondItem="tNY-2F-llo" secondAttribute="trailing" constant="15" id="71e-t3-7Av"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="DND-Fv-FyB" secondAttribute="bottom" constant="1" id="9No-RE-0xx"/>
<constraint firstItem="DND-Fv-FyB" firstAttribute="top" relation="greaterThanOrEqual" secondItem="7ze-s0-mpI" secondAttribute="top" constant="1" id="F9q-6H-sqC"/>
<constraint firstAttribute="trailing" secondItem="DND-Fv-FyB" secondAttribute="trailing" constant="15" id="KFv-7n-LrD"/>
<constraint firstItem="DND-Fv-FyB" firstAttribute="centerY" secondItem="7ze-s0-mpI" secondAttribute="centerY" id="YBX-t4-jkR"/>
<constraint firstItem="tNY-2F-llo" firstAttribute="top" secondItem="7ze-s0-mpI" secondAttribute="top" constant="8" id="bYX-gA-QvB"/>
<constraint firstAttribute="bottom" secondItem="tNY-2F-llo" secondAttribute="bottom" constant="8" id="fxr-wr-I6X"/>
<constraint firstItem="tNY-2F-llo" firstAttribute="leading" secondItem="7ze-s0-mpI" secondAttribute="leading" constant="15" id="hX2-Gr-Bnz"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="artworkImageView" destination="tNY-2F-llo" id="GqY-jv-rso"/>
<outlet property="artworkImageViewLeadingConstraint" destination="hX2-Gr-Bnz" id="be8-dr-c8K"/>
<outlet property="artworkImageViewTrailingConstraint" destination="71e-t3-7Av" id="y62-KO-y1r"/>
<outlet property="nameLabel" destination="DND-Fv-FyB" id="LhN-cA-8Hy"/>
<outlet property="selectedBackgroundView" destination="DSH-Hk-snb" id="hLY-4k-VxU"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="SB6-jW-dhZ" id="2aq-ZA-84E"/>
<outlet property="delegate" destination="SB6-jW-dhZ" id="WgY-cp-m7K"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff">
<connections>
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindFromGamesDatabaseBrowserWith:" id="zdg-Az-WwQ"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="f3a-hX-Qnu" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="mUU-ug-yNs" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="2652" y="1001.649175412294"/>
</scene>
<!--Game Collection View Controller-->
<scene sceneID="qNA-NP-TiF">
<objects>
<collectionViewController storyboardIdentifier="gameCollectionViewController" clearsSelectionOnViewWillAppear="NO" id="kqu-75-owz" customClass="GameCollectionViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="OIq-Z8-kxO">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" indicatorStyle="white" dataMode="prototypes" id="OIq-Z8-kxO">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
@ -161,7 +85,6 @@
<connections>
<segue destination="X2o-q6-XD5" kind="unwind" identifier="unwindFromGames" unwindAction="unwindFromGamesViewControllerWith:" id="k8C-Xn-maU"/>
<segue destination="MPk-bF-nkj" kind="presentation" identifier="saveStates" customClass="SaveStatesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="1Xp-2J-0cq"/>
<segue destination="6bq-zy-UZU" kind="presentation" identifier="gamesDatabaseBrowser" id="mzX-Bb-MaX"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bW1-t8-idm" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -260,12 +183,11 @@
<toolbarItems/>
<nil key="simulatedBottomBarMetrics"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="wj9-1e-eev">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="M4r-sO-G4H">
<rect key="frame" x="0.0" y="556" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</toolbar>
<connections>
@ -290,7 +212,7 @@
<navigationController storyboardIdentifier="saveStatesNavigationController" automaticallyAdjustsScrollViewInsets="NO" id="MPk-bF-nkj" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="twH-3X-6DV">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@ -314,31 +236,11 @@
</objects>
<point key="canvasLocation" x="3409" y="1716"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="REv-V5-eEz">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="6bq-zy-UZU" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" barStyle="black" id="uzY-vR-coL">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="SB6-jW-dhZ" kind="relationship" relationship="rootViewController" id="b0w-Fq-hrk"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Hr9-N6-XXA" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1854" y="1002"/>
</scene>
</scenes>
<resources>
<image name="BoxArt" width="100" height="100"/>
<image name="Settings_Button" width="22" height="22"/>
<image name="SettingsButton" width="16" height="16"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="mzX-Bb-MaX"/>
<segue reference="Tey-6Z-UHp"/>
</inferredMetricsTieBreakers>
</document>

View File

@ -1,11 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11185.3" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.19" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11151.4"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.16"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<customFonts key="customFonts">
<array key="Menlo.ttc">
<string>Menlo-Regular</string>
</array>
</customFonts>
<scenes>
<!--Pause View Controller-->
<scene sceneID="Wst-Dv-TjM">
@ -16,15 +20,17 @@
<viewControllerLayoutGuide type="bottom" id="gF0-0U-kR7"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="oOH-ea-jcb">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p2M-dE-BJs" userLabel="Blur View">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="eyD-0d-RHe" userLabel="Blur Content View">
<frame key="frameInset"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rqN-NB-jbb">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<connections>
<segue destination="sWv-Ky-VGs" kind="embed" identifier="embedNavigationController" id="1Ja-XW-uoT"/>
</connections>
@ -63,7 +69,7 @@
<objects>
<navigationController id="sWv-Ky-VGs" sceneMemberID="viewController">
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="Snh-Z0-9kC">
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" barStyle="black" id="Snh-Z0-9kC">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
@ -78,9 +84,9 @@
<!--Paused-->
<scene sceneID="1md-hu-g0J">
<objects>
<collectionViewController id="0jA-NY-mvB" customClass="PauseMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" delaysContentTouches="NO" dataMode="prototypes" id="scc-uc-vaJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<collectionViewController id="0jA-NY-mvB" customClass="GridMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" misplaced="YES" delaysContentTouches="NO" dataMode="prototypes" id="scc-uc-vaJ">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="10" id="yXv-zl-idO" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
@ -91,7 +97,7 @@
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="6XS-Ne-nGZ" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
<frame key="frameInset" minY="84" width="60" height="80"/>
<rect key="frame" x="0.0" y="20" width="60" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="60" height="80"/>
@ -128,7 +134,7 @@
<objects>
<collectionViewController storyboardIdentifier="saveStatesViewController" id="OOk-k7-INg" customClass="SaveStatesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="XgF-OF-CVf">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="tvW-q1-PD8" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
@ -139,7 +145,7 @@
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="c3N-1A-ryV" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
<frame key="frameInset" minX="20" minY="124" width="50" height="50"/>
<rect key="frame" x="20" y="60" width="50" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
@ -148,7 +154,7 @@
</collectionViewCell>
</cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Header" id="YeY-W9-CC6" customClass="SaveStatesCollectionHeaderView" customModule="Delta" customModuleProvider="target">
<frame key="frameInset" minY="64" width="375" height="50"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="50"/>
<autoresizingMask key="autoresizingMask"/>
</collectionReusableView>
<connections>
@ -174,22 +180,22 @@
<objects>
<tableViewController id="wb8-5o-1jE" customClass="CheatsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="f5S-hV-1yV">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="e8g-ZW-5lQ" customClass="CheatTableViewCell" customModule="Delta" customModuleProvider="target">
<frame key="frameInset" minY="92" width="375" height="44"/>
<rect key="frame" x="0.0" y="28" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="e8g-ZW-5lQ" id="AHE-Jk-ULE">
<frame key="frameInset" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="emc-gw-KkJ" userLabel="Selected Background View">
<frame key="frameInset" maxY="-0.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" id="9bA-Tg-Bko">
<frame key="frameInset"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
@ -198,8 +204,9 @@
</vibrancyEffect>
</visualEffectView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="R4A-9d-DGb">
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="EdX-fU-x54">
<frame key="frameInset"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<vibrancyEffect>
@ -245,20 +252,21 @@
<objects>
<tableViewController storyboardIdentifier="editCheatViewController" id="jTR-Oe-YUJ" customClass="EditCheatViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="BV9-ff-x83">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<sections>
<tableViewSection headerTitle="Name" id="QT6-DZ-g70">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ZeC-rg-QFa">
<rect key="frame" x="0.0" y="119.5" width="375" height="44"/>
<rect key="frame" x="0.0" y="55.5" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="ZeC-rg-QFa" id="UIF-fK-ApW">
<frame key="frameInset" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Cheat Name" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="DD1-X0-hg7">
<rect key="frame" x="20" y="0.0" width="560" height="43.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" returnKeyType="done"/>
<connections>
@ -280,13 +288,14 @@
<tableViewSection headerTitle="Type" footerTitle="Description" id="rvn-VK-2uH">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Tst-zn-e04">
<rect key="frame" x="0.0" y="227" width="375" height="44"/>
<rect key="frame" x="0.0" y="163" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Tst-zn-e04" id="gwV-zS-RWQ">
<frame key="frameInset" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xrD-ue-96Q">
<rect key="frame" x="20" y="8" width="560" height="29"/>
<segments>
<segment title="First"/>
<segment title="Second"/>
@ -308,13 +317,14 @@
<tableViewSection headerTitle="Code" footerTitle="Description" id="rHC-nA-ga0">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="210" id="xxc-cz-sb7">
<rect key="frame" x="0.0" y="346.5" width="375" height="210"/>
<rect key="frame" x="0.0" y="282.5" width="600" height="210"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="xxc-cz-sb7" id="agU-SE-fNa">
<frame key="frameInset" width="375" height="209.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="209.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="600" height="209.5"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/>
<textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/>

View File

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13528" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13526"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PausePresentationController" customModule="Delta" customModuleProvider="target">
@ -13,14 +18,14 @@
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="qFz-hB-77X">
<rect key="frame" x="15" y="236" width="570" height="127.5"/>
<rect key="frame" x="15" y="270.5" width="375" height="126.5"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="500" image="Pause" translatesAutoresizingMaskIntoConstraints="NO" id="cWa-Ht-i5m">
<rect key="frame" x="0.0" y="0.0" width="570" height="80"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" priority="900" constant="44" id="xki-9P-sHi">
<variation key="heightClass=compact" constant="25"/>
@ -28,9 +33,9 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="800" text="Super Mario World" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="ZcT-qE-aES">
<rect key="frame" x="0.0" y="95" width="570" height="32.5"/>
<rect key="frame" x="0.0" y="95" width="375" height="31.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11760" systemVersion="16B2657" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13528" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11755"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13526"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -21,15 +21,15 @@
<sections>
<tableViewSection headerTitle="Inputs" id="c6K-sJ-0vW">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="tls-Hv-Rx2" detailTextLabel="vJP-Ie-a9H" style="IBUITableViewCellStyleValue1" id="jvV-ZB-Rq1">
<rect key="frame" x="0.0" y="56" width="375" height="44"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="tls-Hv-Rx2" detailTextLabel="vJP-Ie-a9H" style="IBUITableViewCellStyleValue1" id="jvV-ZB-Rq1">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="jvV-ZB-Rq1" id="AVi-6C-eIS">
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tls-Hv-Rx2">
<rect key="frame" x="15" y="12" width="56" height="19.5"/>
<rect key="frame" x="16" y="12" width="56" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -45,15 +45,15 @@
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="e3u-x9-IEC" detailTextLabel="2OP-A1-VYo" style="IBUITableViewCellStyleValue1" id="1Fv-H5-0oH">
<rect key="frame" x="0.0" y="100" width="375" height="44"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="e3u-x9-IEC" detailTextLabel="2OP-A1-VYo" style="IBUITableViewCellStyleValue1" id="1Fv-H5-0oH">
<rect key="frame" x="0.0" y="99.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="1Fv-H5-0oH" id="kFJ-zK-MLZ">
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="e3u-x9-IEC">
<rect key="frame" x="15" y="12" width="58" height="19.5"/>
<rect key="frame" x="16" y="12" width="58" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -69,15 +69,15 @@
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="Cdn-11-xZe" detailTextLabel="wWc-NY-Bsd" style="IBUITableViewCellStyleValue1" id="EcC-Be-jV5">
<rect key="frame" x="0.0" y="144" width="375" height="44"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="Cdn-11-xZe" detailTextLabel="wWc-NY-Bsd" style="IBUITableViewCellStyleValue1" id="EcC-Be-jV5">
<rect key="frame" x="0.0" y="143.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="EcC-Be-jV5" id="9ZS-um-scR">
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 3" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Cdn-11-xZe">
<rect key="frame" x="15" y="12" width="58.5" height="19.5"/>
<rect key="frame" x="16" y="12" width="58.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -93,15 +93,15 @@
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="Hls-3b-EaS" detailTextLabel="hNf-uc-PLR" style="IBUITableViewCellStyleValue1" id="hO9-Ov-vsA">
<rect key="frame" x="0.0" y="188" width="375" height="44"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="Hls-3b-EaS" detailTextLabel="hNf-uc-PLR" style="IBUITableViewCellStyleValue1" id="hO9-Ov-vsA">
<rect key="frame" x="0.0" y="187.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hO9-Ov-vsA" id="MRi-re-XI7">
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 4" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Hls-3b-EaS">
<rect key="frame" x="15" y="12" width="59" height="19.5"/>
<rect key="frame" x="16" y="12" width="59" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -121,15 +121,15 @@
</tableViewSection>
<tableViewSection headerTitle="Controller Skins" id="Nch-k1-6pR">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="mBC-YU-BVK" style="IBUITableViewCellStyleDefault" id="ICf-ug-NwS">
<rect key="frame" x="0.0" y="289" width="375" height="44"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="mBC-YU-BVK" style="IBUITableViewCellStyleDefault" id="ICf-ug-NwS">
<rect key="frame" x="0.0" y="287.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="ICf-ug-NwS" id="7se-sE-x9e">
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Super Nintendo" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="mBC-YU-BVK">
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="mBC-YU-BVK">
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -138,15 +138,32 @@
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="MFD-lC-Vfk" style="IBUITableViewCellStyleDefault" id="4Sh-Mb-vqu">
<rect key="frame" x="0.0" y="333" width="375" height="44"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="Dxs-Me-IVU" style="IBUITableViewCellStyleDefault" id="Hqy-yc-Jef">
<rect key="frame" x="0.0" y="331.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="4Sh-Mb-vqu" id="cJG-Gr-n6q">
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Hqy-yc-Jef" id="wJL-kh-qW0">
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Game Boy Advance" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="MFD-lC-Vfk">
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Dxs-Me-IVU">
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="o9x-Kn-6bC" style="IBUITableViewCellStyleDefault" id="jFa-Qk-1cj">
<rect key="frame" x="0.0" y="375.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="jFa-Qk-1cj" id="rFR-qL-fNQ">
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="o9x-Kn-6bC">
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -160,14 +177,14 @@
<tableViewSection headerTitle="Controller Opacity" footerTitle="Determines how translucent the controller appears, if supported by the controller skin." id="SwK-m9-8gt">
<cells>
<tableViewCell contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="Xxk-vo-eu4">
<rect key="frame" x="0.0" y="441" width="375" height="44"/>
<rect key="frame" x="0.0" y="483" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Xxk-vo-eu4" id="vxt-Ex-b4b">
<rect key="frame" x="0.0" y="0.0" width="375" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="whi-If-wFf">
<rect key="frame" x="74" y="7" width="288" height="31"/>
<rect key="frame" x="75" y="7" width="286" height="31"/>
<connections>
<action selector="beginChangingControllerOpacityWith:" destination="eHi-aO-uGS" eventType="touchDown" id="NG9-FX-62d"/>
<action selector="changeControllerOpacityWith:" destination="eHi-aO-uGS" eventType="valueChanged" id="Zci-tN-4uU"/>
@ -177,7 +194,7 @@
</connections>
</slider>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="50%" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zaz-yD-CYG">
<rect key="frame" x="15" y="11" width="46" height="21"/>
<rect key="frame" x="16" y="11" width="46" height="21"/>
<constraints>
<constraint firstAttribute="height" constant="21" id="ACD-qY-k0J"/>
<constraint firstAttribute="width" constant="46" id="ZVd-ie-qRm"/>
@ -223,6 +240,78 @@
</objects>
<point key="canvasLocation" x="1555" y="471"/>
</scene>
<!--Controls-->
<scene sceneID="Gi9-m1-y9x">
<objects>
<viewController title="Controls" id="x1g-pH-DnF" customClass="ControllerInputsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="cH1-gu-g2u"/>
<viewControllerLayoutGuide type="bottom" id="Z6c-bc-h6l"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="cPg-qa-ERT">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6Wl-el-X30" userLabel="GameViewController">
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
<connections>
<segue destination="LIv-AL-s86" kind="embed" identifier="embedGameViewController" id="2Qg-Jw-0mM"/>
</connections>
</containerView>
<containerView opaque="NO" contentMode="scaleToFill" placeholderIntrinsicWidth="375" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="KkE-ji-6Y8" userLabel="GridMenuViewController">
<rect key="frame" x="0.0" y="233.5" width="375" height="200"/>
<constraints>
<constraint firstAttribute="height" constant="200" id="MWA-T4-ROi"/>
</constraints>
<connections>
<segue destination="Jpj-e9-6XW" kind="embed" identifier="embedActionsMenuViewController" id="kfu-fO-l6Z"/>
</connections>
</containerView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="KkE-ji-6Y8" firstAttribute="centerY" secondItem="cPg-qa-ERT" secondAttribute="centerY" placeholder="YES" id="4wi-cL-aCQ"/>
<constraint firstItem="Z6c-bc-h6l" firstAttribute="top" secondItem="6Wl-el-X30" secondAttribute="bottom" id="Bmp-yB-Yf1"/>
<constraint firstAttribute="trailing" secondItem="KkE-ji-6Y8" secondAttribute="trailing" id="Jeb-8K-VYw"/>
<constraint firstItem="6Wl-el-X30" firstAttribute="top" secondItem="cH1-gu-g2u" secondAttribute="bottom" id="TD2-bx-DJC"/>
<constraint firstAttribute="trailing" secondItem="6Wl-el-X30" secondAttribute="trailing" id="Xph-DL-tBk"/>
<constraint firstItem="6Wl-el-X30" firstAttribute="leading" secondItem="cPg-qa-ERT" secondAttribute="leading" id="gcd-77-5wR"/>
<constraint firstItem="KkE-ji-6Y8" firstAttribute="leading" secondItem="cPg-qa-ERT" secondAttribute="leading" id="z7N-Cn-hGs"/>
</constraints>
<connections>
<outletCollection property="gestureRecognizers" destination="4p8-OB-LsR" appends="YES" id="4k4-Oj-XtP"/>
</connections>
</view>
<navigationItem key="navigationItem" id="UeP-Yr-9jA">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="QfC-sf-WbP">
<connections>
<segue destination="8l5-7I-Z7e" kind="unwind" identifier="cancelControllerInputs" unwindAction="unwindFromControllerControlsViewController:" id="AkD-Lu-h5b"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="save" id="WHh-7W-jpl">
<connections>
<segue destination="8l5-7I-Z7e" kind="unwind" identifier="saveControllerInputs" unwindAction="unwindFromControllerControlsViewController:" id="4xr-OB-4dx"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<connections>
<outlet property="actionsMenuViewControllerHeightConstraint" destination="MWA-T4-ROi" id="itx-dZ-m76"/>
<outlet property="cancelTapGestureRecognizer" destination="4p8-OB-LsR" id="coO-FL-pbp"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="DqP-Jn-rth" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="8l5-7I-Z7e" userLabel="Exit" sceneMemberID="exit"/>
<tapGestureRecognizer delaysTouchesBegan="YES" id="4p8-OB-LsR">
<connections>
<action selector="handleTapGesture:" destination="x1g-pH-DnF" id="8KO-75-4Iy"/>
<outlet property="delegate" destination="x1g-pH-DnF" id="GDY-v6-naf"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="3809" y="471"/>
</scene>
<!--Controllers-->
<scene sceneID="swa-DT-VKS">
<objects>
@ -240,14 +329,14 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Controller Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="VBO-V1-Wfu">
<rect key="frame" x="15" y="12" width="118.5" height="19.5"/>
<rect key="frame" x="16" y="12" width="118.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tqn-1q-p53">
<rect key="frame" x="304" y="12" width="56" height="19.5"/>
<rect key="frame" x="303" y="12" width="56" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.55686274509803924" green="0.55686274509803924" blue="0.57647058823529407" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -255,9 +344,6 @@
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="HzL-SB-qry" kind="unwind" identifier="unwindControllersSegue" unwindAction="unwindFromControllersSettingsViewController:" id="WJe-ZI-clo"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
@ -267,16 +353,18 @@
</tableView>
<navigationItem key="navigationItem" title="Controllers" id="QK7-oi-2jJ"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<segue destination="0QR-U9-gtx" kind="presentation" identifier="controllerInputsSegue" id="E3Y-yV-zT5"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="owG-Kh-rfn" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="HzL-SB-qry" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="2221" y="471"/>
</scene>
<!--Game Boy Advance-->
<scene sceneID="pkL-Te-puh">
<objects>
<tableViewController id="56e-ul-z6v" customClass="GameTypeControllerSkinsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableViewController id="56e-ul-z6v" customClass="SystemControllerSkinsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="fdQ-n7-kUL">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -399,7 +487,7 @@
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="ssH-mM-uG6" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="Ckw-ES-lkE">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@ -411,5 +499,76 @@
</objects>
<point key="canvasLocation" x="889" y="471"/>
</scene>
<!--Grid Menu View Controller-->
<scene sceneID="Lgi-Ii-M1W">
<objects>
<collectionViewController id="Jpj-e9-6XW" customClass="GridMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="yGk-jU-wZQ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="tLr-UM-1BH" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="Hef-IR-nMO" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="Jpj-e9-6XW" id="iAK-8A-KXA"/>
<outlet property="delegate" destination="Jpj-e9-6XW" id="sbi-az-9kr"/>
</connections>
</collectionView>
<size key="freeformSize" width="375" height="667"/>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="pRg-BA-3KK" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4566" y="-216"/>
</scene>
<!--Game View Controller-->
<scene sceneID="qAz-yz-iOc">
<objects>
<viewController id="LIv-AL-s86" customClass="GameViewController" customModule="DeltaCore" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="9u3-RP-Qcj"/>
<viewControllerLayoutGuide type="bottom" id="XGZ-ro-kQv"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="57g-cn-rbZ">
<rect key="frame" x="0.0" y="0.0" width="375" height="603"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="uQK-ch-9AG" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4566" y="471"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="bwW-s2-fcE">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="0QR-U9-gtx" sceneMemberID="viewController">
<toolbarItems/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" id="Y5H-O6-CQ5">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="x1g-pH-DnF" kind="relationship" relationship="rootViewController" id="EOa-ao-vBI"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="D4f-Fb-zfa" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2977" y="471"/>
</scene>
</scenes>
</document>

View File

@ -45,15 +45,15 @@ class GridCollectionViewCell: UICollectionViewCell
}
}
fileprivate var vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
private var vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
fileprivate var imageViewWidthConstraint: NSLayoutConstraint!
fileprivate var imageViewHeightConstraint: NSLayoutConstraint!
private var imageViewWidthConstraint: NSLayoutConstraint!
private var imageViewHeightConstraint: NSLayoutConstraint!
fileprivate var textLabelBottomAnchorConstraint: NSLayoutConstraint!
private var textLabelBottomAnchorConstraint: NSLayoutConstraint!
fileprivate var textLabelVerticalSpacingConstraint: NSLayoutConstraint!
fileprivate var textLabelFocusedVerticalSpacingConstraint: NSLayoutConstraint?
private var textLabelVerticalSpacingConstraint: NSLayoutConstraint!
private var textLabelFocusedVerticalSpacingConstraint: NSLayoutConstraint?
override init(frame: CGRect)
{

View File

@ -1,184 +0,0 @@
//
// ImportController.swift
// Delta
//
// Created by Riley Testut on 10/10/15.
// Copyright © 2015 Riley Testut. All rights reserved.
//
import UIKit
import ObjectiveC
import DeltaCore
import MobileCoreServices
protocol ImportControllerDelegate
{
func importController(_ importController: ImportController, didImport games: Set<Game>, with errors: Set<DatabaseManager.ImportError>)
func importController(_ importController: ImportController, didImport controllerSkins: Set<ControllerSkin>, with errors: Set<DatabaseManager.ImportError>)
/** Optional **/
func importControllerDidCancel(_ importController: ImportController)
}
extension ImportControllerDelegate
{
func importControllerDidCancel(_ importController: ImportController)
{
// Empty Implementation
}
}
class ImportController: NSObject
{
var delegate: ImportControllerDelegate?
fileprivate weak var presentingViewController: UIViewController?
fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completion: ((Void) -> Void)?)
{
self.presentingViewController = presentingViewController
var documentTypes = GameType.supportedTypes.map { $0.rawValue }
documentTypes.append(kUTTypeDeltaControllerSkin as String)
documentTypes.append(kUTTypeZipArchive as String)
// Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations)
documentTypes.append("com.rileytestut.gba")
documentTypes.append("com.rileytestut.gbc")
documentTypes.append("com.rileytestut.gb")
#if os(iOS)
let documentMenuController = UIDocumentMenuViewController(documentTypes: documentTypes, in: .import)
documentMenuController.delegate = self
documentMenuController.addOption(withTitle: NSLocalizedString("iTunes", comment: ""), image: nil, order: .first) { self.importFromiTunes(nil) }
self.presentingViewController?.present(documentMenuController, animated: true, completion: nil)
#else
self.importFromiTunes(completion)
#endif
}
private func importFromiTunes(_ completion: ((Void) -> Void)?)
{
let alertController = UIAlertController(title: NSLocalizedString("Import from iTunes?", comment: ""), message: NSLocalizedString("Delta will import the games and controller skins copied over via iTunes.", comment: ""), preferredStyle: .alert)
let importAction = UIAlertAction(title: NSLocalizedString("Import", comment: ""), style: .default) { action in
let documentsDirectoryURL = DatabaseManager.defaultDirectoryURL().deletingLastPathComponent()
do
{
let contents = try FileManager.default.contentsOfDirectory(at: documentsDirectoryURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
DatabaseManager.shared.performBackgroundTask { (context) in
let controllerSkinURLs = contents.filter { $0.pathExtension.lowercased() == "deltaskin" }
self.importControllerSkins(at: Set(controllerSkinURLs))
let gameURLs = contents.filter { GameType.gameType(forFileExtension: $0.pathExtension) != .unknown || $0.pathExtension.lowercased() == "zip" }
self.importGames(at: Set(gameURLs))
}
}
catch let error as NSError
{
print(error)
}
self.presentingViewController?.importController = nil
}
alertController.addAction(importAction)
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { action in
self.delegate?.importControllerDidCancel(self)
self.presentingViewController?.importController = nil
}
alertController.addAction(cancelAction)
self.presentingViewController?.present(alertController, animated: true, completion: completion)
}
fileprivate func importGames(at urls: Set<URL>)
{
DatabaseManager.shared.importGames(at: urls) { (games, errors) in
self.delegate?.importController(self, didImport: games, with: errors)
}
}
fileprivate func importControllerSkins(at urls: Set<URL>)
{
DatabaseManager.shared.importControllerSkins(at: urls) { (controllerSkins, errors) in
self.delegate?.importController(self, didImport: controllerSkins, with: errors)
}
}
}
#if os(iOS)
extension ImportController: UIDocumentMenuDelegate
{
func documentMenu(_ documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController)
{
documentPicker.delegate = self
self.presentingViewController?.present(documentPicker, animated: true, completion: nil)
}
func documentMenuWasCancelled(_ documentMenu: UIDocumentMenuViewController)
{
self.delegate?.importControllerDidCancel(self)
self.presentingViewController?.importController = nil
}
}
extension ImportController: UIDocumentPickerDelegate
{
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL)
{
if url.pathExtension.lowercased() == "deltaskin"
{
self.importControllerSkins(at: [url])
}
else
{
self.importGames(at: [url])
}
self.presentingViewController?.importController = nil
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController)
{
self.delegate?.importControllerDidCancel(self)
self.presentingViewController?.importController = nil
}
}
#endif
private var ImportControllerKey: UInt8 = 0
extension UIViewController
{
fileprivate(set) var importController: ImportController?
{
set
{
objc_setAssociatedObject(self, &ImportControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get
{
return objc_getAssociatedObject(self, &ImportControllerKey) as? ImportController
}
}
func present(_ importController: ImportController, animated: Bool, completion: ((Void) -> Void)?)
{
self.importController = importController
importController.presentImportController(from: self, animated: animated, completion: completion)
}
}

View File

@ -45,6 +45,11 @@ class LoadImageURLOperation: RSTLoadOperation<UIImage, NSURL>
super.cancel()
self.downloadOperation?.cancel()
if self.isAsynchronous
{
self.finish()
}
}
override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void)
@ -74,14 +79,12 @@ class LoadImageURLOperation: RSTLoadOperation<UIImage, NSURL>
private func loadLocalImage(completion: @escaping (UIImage?, Error?) -> Void)
{
let options: NSDictionary = [kCGImageSourceShouldCache as NSString: true]
guard let imageSource = CGImageSourceCreateWithURL(self.url as CFURL, options) else {
guard let imageSource = CGImageSourceCreateWithURL(self.url as CFURL, nil) else {
completion(nil, .doesNotExist)
return
}
guard let quartzImage = CGImageSourceCreateImageAtIndex(imageSource, 0, options) else {
guard let quartzImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else {
completion(nil, .invalid)
return
}

View File

@ -0,0 +1,64 @@
//
// PopoverMenuViewController.swift
// Delta
//
// Created by Riley Testut on 9/2/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class ListMenuViewController: UITableViewController
{
var items: [MenuItem] {
get { return self.dataSource.items }
set { self.dataSource.items = newValue }
}
private let dataSource = RSTArrayTableViewDataSource<MenuItem>(items: [])
override var preferredContentSize: CGSize {
get {
let navigationBarHeight = self.navigationController?.navigationBar.bounds.height ?? 0.0
return CGSize(width: 0, height: (self.tableView.rowHeight * CGFloat(self.items.count)) + navigationBarHeight)
}
set {}
}
init()
{
super.init(style: .plain)
self.dataSource.cellConfigurationHandler = { (cell, item, indexPath) in
cell.textLabel?.text = item.text
cell.accessoryType = item.isSelected ? .checkmark : .none
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.tableView.rowHeight = 44
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
}
}
extension ListMenuViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let item = self.dataSource.item(at: indexPath)
item.isSelected = !item.isSelected
item.action(item)
self.tableView.reloadData()
}
}

View File

@ -0,0 +1,121 @@
//
// PopoverMenuButton.swift
// Delta
//
// Created by Riley Testut on 9/2/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
extension UINavigationBar
{
fileprivate var defaultTitleTextAttributes: [NSAttributedStringKey: Any]? {
if let textAttributes = self._defaultTitleTextAttributes
{
return textAttributes
}
// Make "copy" of self.
let navigationBar = UINavigationBar(frame: .zero)
navigationBar.barStyle = self.barStyle
// Set item with title so we can retrieve default text attributes.
let navigationItem = UINavigationItem(title: "Testut")
navigationBar.items = [navigationItem]
navigationBar.isHidden = true
// Must be added to window hierarchy for it to create title UILabel.
self.addSubview(navigationBar)
defer { navigationBar.removeFromSuperview() }
navigationBar.layoutIfNeeded()
let textAttributes = navigationBar._defaultTitleTextAttributes
return textAttributes
}
private var _defaultTitleTextAttributes: [NSAttributedStringKey: Any]? {
guard self.titleTextAttributes == nil else { return self.titleTextAttributes }
guard
let contentView = self.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("ContentView") || NSStringFromClass(type(of: $0)).contains("ItemView") }),
let titleLabel = contentView.subviews.first(where: { $0 is UILabel }) as? UILabel
else { return nil }
let textAttributes = titleLabel.attributedText?.attributes(at: 0, effectiveRange: nil)
return textAttributes
}
}
class PopoverMenuButton: UIControl
{
var title: String {
get { return self.textLabel.text ?? "" }
set {
self.textLabel.text = newValue
self.updateTextAttributes()
self.invalidateIntrinsicContentSize()
}
}
private let textLabel: UILabel
private let arrowLabel: UILabel
private let stackView: UIStackView
private var parentNavigationBar: UINavigationBar? {
guard let navigationController = self.parentViewController as? UINavigationController ?? self.parentViewController?.navigationController else { return nil }
guard self.isDescendant(of: navigationController.navigationBar) else { return nil }
return navigationController.navigationBar
}
override var intrinsicContentSize: CGSize {
return self.stackView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
}
init()
{
self.textLabel = UILabel()
self.textLabel.textColor = .black
self.arrowLabel = UILabel()
self.arrowLabel.text = ""
self.arrowLabel.textColor = .black
self.stackView = UIStackView(arrangedSubviews: [self.textLabel, self.arrowLabel])
self.stackView.axis = .horizontal
self.stackView.distribution = .fillProportionally
self.stackView.alignment = .center
self.stackView.spacing = 4.0
self.stackView.isUserInteractionEnabled = false
let intrinsicContentSize = self.stackView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
super.init(frame: CGRect(origin: .zero, size: intrinsicContentSize))
self.addSubview(self.stackView, pinningEdgesWith: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToSuperview()
{
self.updateTextAttributes()
}
}
private extension PopoverMenuButton
{
func updateTextAttributes()
{
guard let parentNavigationBar = self.parentNavigationBar else { return }
guard let textAttributes = parentNavigationBar.defaultTitleTextAttributes else { return }
for label in [self.textLabel, self.arrowLabel]
{
label.attributedText = NSAttributedString(string: label.text ?? "", attributes: textAttributes)
}
}
}

View File

@ -0,0 +1,102 @@
//
// PopoverMenuController.swift
// Delta
//
// Created by Riley Testut on 9/5/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
private var popoverMenuControllerKey: UInt8 = 0
extension UINavigationItem
{
var popoverMenuController: PopoverMenuController? {
get { return objc_getAssociatedObject(self, &popoverMenuControllerKey) as? PopoverMenuController }
set {
self.titleView = newValue?.popoverMenuButton
objc_setAssociatedObject(self, &popoverMenuControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
class PopoverMenuController: NSObject
{
let popoverViewController: UIViewController
let popoverMenuButton: PopoverMenuButton
var isActive: Bool = false {
willSet {
guard newValue != self.isActive else { return }
if newValue
{
self.presentPopoverViewController()
}
else
{
self.dismissPopoverViewController()
}
}
}
init(popoverViewController: UIViewController)
{
self.popoverViewController = popoverViewController
self.popoverMenuButton = PopoverMenuButton()
super.init()
self.popoverMenuButton.addTarget(self, action: #selector(PopoverMenuController.pressedPopoverMenuButton(_:)), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension PopoverMenuController
{
@objc func pressedPopoverMenuButton(_ button: PopoverMenuButton)
{
self.isActive = !self.isActive
}
func presentPopoverViewController()
{
guard !self.isActive else { return }
guard let presentingViewController = self.popoverMenuButton.parentViewController else { return }
self.popoverViewController.modalPresentationStyle = .popover
self.popoverViewController.popoverPresentationController?.delegate = self
self.popoverViewController.popoverPresentationController?.sourceView = self.popoverMenuButton.superview
self.popoverViewController.popoverPresentationController?.sourceRect = self.popoverMenuButton.frame
presentingViewController.present(self.popoverViewController, animated: true, completion: nil)
}
func dismissPopoverViewController()
{
guard self.isActive else { return }
self.popoverViewController.dismiss(animated: true, completion: nil)
}
}
extension PopoverMenuController: UIPopoverPresentationControllerDelegate
{
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
{
// Force popover presentation, regardless of trait collection.
return .none
}
func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController)
{
self.isActive = false
}
}

View File

@ -0,0 +1,68 @@
//
// PopoverMenuViewController.swift
// Delta
//
// Created by Riley Testut on 9/2/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class ListMenuViewController: UITableViewController
{
var items: [MenuItem] {
get { return self.dataSource.items }
set { self.dataSource.items = newValue; }
}
fileprivate let dataSource = RSTArrayTableViewDataSource<MenuItem>(items: [])
override var preferredContentSize: CGSize {
get {
let navigationBarHeight = self.navigationController?.navigationBar.bounds.height ?? 0.0
return CGSize(width: 0, height: (self.tableView.rowHeight * CGFloat(self.items.count)) + navigationBarHeight)
}
set {}
}
init()
{
super.init(style: .plain)
self.dataSource.cellConfigurationHandler = { (cell, item, indexPath) in
cell.textLabel?.text = item.text
cell.accessoryType = item.isSelected ? .checkmark : .none
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.tableView.rowHeight = 44
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
}
}
extension ListMenuViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
self.items.forEach { $0.isSelected = false }
let item = self.dataSource.item(at: indexPath)
item.isSelected = true
item.action(item)
self.tableView.reloadData()
self.dismiss(animated: true, completion: nil)
}
}

View File

@ -22,6 +22,7 @@ extension DatabaseManager
{
case doesNotExist(URL)
case invalid(URL)
case unsupported(URL)
case unknown(URL, NSError)
case saveFailed(Set<URL>, NSError)
@ -30,8 +31,9 @@ extension DatabaseManager
{
case .doesNotExist: return 0
case .invalid: return 1
case .unknown: return 2
case .saveFailed: return 3
case .unsupported: return 2
case .unknown: return 3
case .saveFailed: return 4
}
}
@ -41,15 +43,16 @@ extension DatabaseManager
{
case (let .doesNotExist(url1), let .doesNotExist(url2)) where url1 == url2: return true
case (let .invalid(url1), let .invalid(url2)) where url1 == url2: return true
case (let .unsupported(url1), let .unsupported(url2)) where url1 == url2: return true
case (let .unknown(url1, error1), let .unknown(url2, error2)) where url1 == url2 && error1 == error2: return true
case (let .saveFailed(urls1, error1), let .saveFailed(urls2, error2)) where urls1 == urls2 && error1 == error2: return true
case (.doesNotExist, _): return false
case (.invalid, _): return false
case (.unsupported, _): return false
case (.unknown, _): return false
case (.saveFailed, _): return false
}
}
}
}
@ -57,16 +60,15 @@ final class DatabaseManager: NSPersistentContainer
{
static let shared = DatabaseManager()
fileprivate var gamesDatabase: GamesDatabase? = nil
private var gamesDatabase: GamesDatabase? = nil
private init()
{
guard
let modelURL = Bundle(for: DatabaseManager.self).url(forResource: "Model", withExtension: "mom"),
let modelURL = Bundle(for: DatabaseManager.self).url(forResource: "Delta", withExtension: "momd"),
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)
else { fatalError("Core Data model cannot be found. Aborting.") }
super.init(name: "Delta", managedObjectModel: managedObjectModel)
self.viewContext.automaticallyMergesChangesFromParent = true
@ -95,13 +97,13 @@ extension DatabaseManager
//MARK: - Preparation -
private extension DatabaseManager
{
func prepareDatabase(completion: @escaping (Void) -> Void)
func prepareDatabase(completion: @escaping () -> Void)
{
self.performBackgroundTask { (context) in
for gameType in GameType.supportedTypes
for system in System.supportedSystems
{
guard let deltaControllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: gameType) else { continue }
guard let deltaControllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: system.gameType) else { continue }
let controllerSkin = ControllerSkin(context: context)
controllerSkin.isStandard = true
@ -173,21 +175,27 @@ extension DatabaseManager
continue
}
let identifier = FileHash.sha1HashOfFile(atPath: url.path) as String
guard let gameType = GameType(fileExtension: url.pathExtension), let system = System(gameType: gameType) else {
errors.insert(.unsupported(url))
continue
}
let identifier = FileHash.sha1HashOfFile(atPath: url.path) as String
let filename = identifier + "." + url.pathExtension
let game = Game.insertIntoManagedObjectContext(context)
let game = Game(context: context)
game.identifier = identifier
game.type = gameType
game.filename = filename
let databaseMetadata = self.gamesDatabase?.metadata(for: game)
game.name = databaseMetadata?.name ?? url.deletingPathExtension().lastPathComponent
game.artworkURL = databaseMetadata?.artworkURL
let gameCollection = GameCollection.gameSystemCollectionForPathExtension(url.pathExtension, inManagedObjectContext: context)
game.type = GameType(rawValue: gameCollection.identifier)
game.gameCollections.insert(gameCollection)
let gameCollection = GameCollection(context: context)
gameCollection.identifier = gameType.rawValue
gameCollection.index = Int16(system.year)
gameCollection.games.insert(game)
do
{
@ -212,7 +220,6 @@ extension DatabaseManager
errors.insert(.unknown(url, error))
}
}
do
@ -328,9 +335,8 @@ extension DatabaseManager
guard !entry.fileName.contains("/") else { continue }
let fileExtension = (entry.fileName as NSString).pathExtension
let gameType = GameType.gameType(forFileExtension: fileExtension)
guard gameType != .unknown else { continue }
guard GameType(fileExtension: fileExtension) != nil else { continue }
// At least one entry is a valid game file, so we set archiveContainsValidGameFile to true
// This will result in this archive being considered valid, and thus we will not return an ImportError.invalid error for the archive
@ -486,6 +492,14 @@ extension DatabaseManager
return gameTypeDirectoryURL
}
class func artworkURL(for game: Game) -> URL
{
let gameURL = game.fileURL
let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("jpg")
return artworkURL
}
}
//MARK: - Private -

View File

@ -15,9 +15,9 @@ class InputStreamOutputWriter: NSObject
let inputStream: InputStream
let outputStream: OutputStream
fileprivate var completion: ((Error?) -> Void)?
private var completion: ((Error?) -> Void)?
fileprivate var dataBuffer = Data(capacity: MaximumBufferLength * 2)
private var dataBuffer = Data(capacity: MaximumBufferLength * 2)
init(inputStream: InputStream, outputStream: OutputStream)
{

View File

@ -0,0 +1,8 @@
<?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>_XCCurrentVersionName</key>
<string>Delta 2.xcdatamodel</string>
</dict>
</plist>

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13532" systemVersion="16G29" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
<attribute name="code" attributeType="String" syncable="YES"/>
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="CheatType"/>
</userInfo>
</attribute>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
<attribute name="filename" attributeType="String" syncable="YES"/>
<attribute name="gameType" attributeType="Transformable" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
<userInfo>
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
</userInfo>
</attribute>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="gameType"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Game" representedClassName="Game" syncable="YES">
<attribute name="artworkURL" optional="YES" attributeType="Transformable" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="URL"/>
</userInfo>
</attribute>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
<relationship name="gameCollections" toMany="YES" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollections" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="Any"/>
</userInfo>
</attribute>
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES">
<userInfo>
<entry key="attributeValueScalarType" value="SaveStateType"/>
</userInfo>
</attribute>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="135"/>
<element name="Game" positionX="-378" positionY="-54" width="128" height="180"/>
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="105"/>
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
</elements>
</model>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="11759" systemVersion="16C68" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13240" systemVersion="16G29" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
<attribute name="code" attributeType="String" syncable="YES"/>
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
@ -79,6 +79,31 @@
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="Any"/>
</userInfo>
</attribute>
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SaveState" representedClassName="SaveState" syncable="YES">
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="filename" attributeType="String" syncable="YES">
@ -107,6 +132,7 @@
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="135"/>
<element name="Game" positionX="-378" positionY="-54" width="128" height="180"/>
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="105"/>
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
</elements>
</model>

View File

@ -61,7 +61,7 @@ public class ControllerSkin: _ControllerSkin
return self.controllerSkin?.isDebugModeEnabled ?? false
}
fileprivate lazy var controllerSkin: DeltaCore.ControllerSkin? = {
private lazy var controllerSkin: DeltaCore.ControllerSkin? = {
let controllerSkin = self.isStandard ? DeltaCore.ControllerSkin.standardControllerSkin(for: self.gameType) : DeltaCore.ControllerSkin(fileURL: self.fileURL)
return controllerSkin
}()
@ -88,9 +88,9 @@ extension ControllerSkin: ControllerSkinProtocol
return self.controllerSkin?.image(for: traits, preferredSize: preferredSize)
}
public func inputs(for traits: DeltaCore.ControllerSkin.Traits, point: CGPoint) -> [Input]?
public func inputs(for traits: DeltaCore.ControllerSkin.Traits, at point: CGPoint) -> [Input]?
{
return self.controllerSkin?.inputs(for: traits, point: point)
return self.controllerSkin?.inputs(for: traits, at: point)
}
public func items(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Item]?

View File

@ -22,6 +22,37 @@ public class Game: _Game, GameProtocol
return fileURL
}
public override var artworkURL: URL? {
get {
self.willAccessValue(forKey: #keyPath(Game.artworkURL))
var artworkURL = self.primitiveValue(forKey: #keyPath(Game.artworkURL)) as? URL
self.didAccessValue(forKey: #keyPath(Game.artworkURL))
if let unwrappedArtworkURL = artworkURL, unwrappedArtworkURL.isFileURL
{
// Recreate the stored URL relative to current sandbox location.
artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL)
}
return artworkURL
}
set {
self.willChangeValue(forKey: #keyPath(Game.artworkURL))
var artworkURL = newValue
if let newValue = newValue, newValue.isFileURL
{
// Store a relative URL, since the sandbox location changes.
artworkURL = URL(fileURLWithPath: newValue.lastPathComponent, relativeTo: DatabaseManager.gamesDirectoryURL)
}
self.setPrimitiveValue(artworkURL, forKey: #keyPath(Game.artworkURL))
self.didChangeValue(forKey: #keyPath(Game.artworkURL))
}
}
}
extension Game

View File

@ -9,48 +9,22 @@
import CoreData
import DeltaCore
import SNESDeltaCore
import GBADeltaCore
@objc(GameCollection)
public class GameCollection: _GameCollection
public class GameCollection: _GameCollection
{
var name: String
{
let gameType = GameType(rawValue: self.identifier)
return gameType.localizedName
var name: String {
return self.system?.localizedName ?? NSLocalizedString("Unknown", comment: "")
}
var shortName: String
{
let gameType = GameType(rawValue: self.identifier)
return gameType.localizedShortName
var shortName: String {
return self.system?.localizedShortName ?? NSLocalizedString("Unknown", comment: "")
}
class func gameSystemCollectionForPathExtension(_ pathExtension: String?, inManagedObjectContext managedObjectContext: NSManagedObjectContext) -> GameCollection
{
let gameType = GameType.gameType(forFileExtension: pathExtension ?? "")
let identifier = gameType.rawValue
var system: System? {
let gameType = GameType(rawValue: self.identifier)
let index: Int16
switch gameType
{
case GameType.snes: index = 1990
case GameType.gba: index = 2001
default: index = Int16(INT16_MAX)
}
let predicate = NSPredicate(format: "%K == %@", #keyPath(GameCollection.identifier), identifier)
var gameCollection = GameCollection.instancesWithPredicate(predicate, inManagedObjectContext: managedObjectContext, type: GameCollection.self).first
if gameCollection == nil
{
gameCollection = GameCollection.insertIntoManagedObjectContext(managedObjectContext)
gameCollection?.identifier = identifier
gameCollection?.index = index
}
return gameCollection!
let system = System(gameType: gameType)
return system
}
}

View File

@ -0,0 +1,77 @@
//
// GameControllerInputMapping.swift
// Delta
//
// Created by Riley Testut on 8/30/16.
// Copyright (c) 2016 Riley Testut. All rights reserved.
//
import Foundation
import DeltaCore
@objc(GameControllerInputMapping)
public class GameControllerInputMapping: _GameControllerInputMapping
{
private var inputMapping: DeltaCore.GameControllerInputMapping {
get { return self.deltaCoreInputMapping as! DeltaCore.GameControllerInputMapping }
set { self.deltaCoreInputMapping = newValue }
}
public convenience init(inputMapping: DeltaCore.GameControllerInputMapping, context: NSManagedObjectContext)
{
self.init(entity: GameControllerInputMapping.entity(), insertInto: context)
self.inputMapping = inputMapping
}
}
extension GameControllerInputMapping
{
class func inputMapping(for gameController: GameController, gameType: GameType, in managedObjectContext: NSManagedObjectContext) -> GameControllerInputMapping?
{
guard let playerIndex = gameController.playerIndex else {
return nil
}
let fetchRequest: NSFetchRequest<GameControllerInputMapping> = GameControllerInputMapping.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@ AND %K == %d", #keyPath(GameControllerInputMapping.gameControllerInputType), gameController.inputType.rawValue, #keyPath(GameControllerInputMapping.gameType), gameType.rawValue, #keyPath(GameControllerInputMapping.playerIndex), playerIndex)
do
{
let inputMappings = try managedObjectContext.fetch(fetchRequest)
let inputMapping = inputMappings.first
return inputMapping
}
catch
{
print(error)
return nil
}
}
}
extension GameControllerInputMapping: GameControllerInputMappingProtocol
{
var name: String? {
get { return self.inputMapping.name }
set { self.inputMapping.name = newValue }
}
var supportedControllerInputs: [Input] {
return self.inputMapping.supportedControllerInputs
}
public func input(forControllerInput controllerInput: Input) -> Input?
{
return self.inputMapping.input(forControllerInput: controllerInput)
}
func set(_ input: Input?, forControllerInput controllerInput: Input)
{
self.inputMapping.set(input, forControllerInput: controllerInput)
}
}

View File

@ -13,6 +13,7 @@ import DeltaCore
@objc public enum SaveStateType: Int16
{
case auto
case quick
case general
case locked
}
@ -78,4 +79,14 @@ public class SaveState: _SaveState, SaveStateProtocol
print(error)
}
}
class func fetchRequest(for game: Game, type: SaveStateType) -> NSFetchRequest<SaveState>
{
let predicate = NSPredicate(format: "%K == %@ AND %K == %d", #keyPath(SaveState.game), game, #keyPath(SaveState.type), type.rawValue)
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
fetchRequest.predicate = predicate
return fetchRequest
}
}

View File

@ -0,0 +1,28 @@
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
// Make changes to GameControllerInputMapping.swift instead.
import Foundation
import CoreData
import DeltaCore
public class _GameControllerInputMapping: NSManagedObject
{
@nonobjc public class func fetchRequest() -> NSFetchRequest<GameControllerInputMapping> {
return NSFetchRequest<GameControllerInputMapping>(entityName: "GameControllerInputMapping")
}
// MARK: - Properties
@NSManaged public var deltaCoreInputMapping: Any
@NSManaged public var gameControllerInputType: GameControllerInputType
@NSManaged public var gameType: GameType
@NSManaged public var playerIndex: Int16
// MARK: - Relationships
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
//
// SaveStateMigrationPolicy.swift
// Delta
//
// Created by Riley Testut on 9/28/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
@objc(SaveStateToSaveStateMigrationPolicy)
class SaveStateToSaveStateMigrationPolicy: NSEntityMigrationPolicy
{
@objc(migrateSaveStateType:)
func migrateSaveStateType(_ rawValue: NSNumber) -> NSNumber
{
switch rawValue.intValue
{
case 0: return NSNumber(value: SaveStateType.auto.rawValue)
case 1: return NSNumber(value: SaveStateType.general.rawValue)
case 2: return NSNumber(value: SaveStateType.locked.rawValue)
default: return rawValue
}
}
}

View File

@ -0,0 +1,61 @@
//
// GameControllerInputMappingTransformer.swift
// Delta
//
// Created by Riley Testut on 9/27/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import Foundation
import DeltaCore
@objc(GameControllerInputMappingTransformer)
class GameControllerInputMappingTransformer: ValueTransformer
{
override class func transformedValueClass() -> AnyClass {
return NSData.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func transformedValue(_ value: Any?) -> Any?
{
guard let inputMapping = value as? DeltaCore.GameControllerInputMapping else { return nil }
let plistEncoder = PropertyListEncoder()
do
{
let data = try plistEncoder.encode(inputMapping)
return data
}
catch
{
print(error)
return nil
}
}
override func reverseTransformedValue(_ value: Any?) -> Any?
{
guard let inputMappingData = value as? Data else { return nil }
let plistDecoder = PropertyListDecoder()
do
{
let inputMapping = try plistDecoder.decode(DeltaCore.GameControllerInputMapping.self, from: inputMappingData)
return inputMapping
}
catch
{
print(error)
return nil
}
}
}

View File

@ -8,9 +8,32 @@
import Foundation
// Must be a class (not struct) so it can be used with Objective-C generics
class GameMetadata
// Must be an NSObject subclass so it can be used with RSTCellContentDataSource.
class GameMetadata: NSObject
{
var name: String?
var artworkURL: URL?
let identifier: Int
let name: String?
let artworkURL: URL?
init(identifier: Int, name: String?, artworkURL: URL?)
{
self.name = name
self.identifier = identifier
self.artworkURL = artworkURL
}
}
extension GameMetadata
{
override var hash: Int {
return self.identifier.hashValue
}
override func isEqual(_ object: Any?) -> Bool
{
guard let metadata = object as? GameMetadata else { return false }
return self.identifier == metadata.identifier
}
}

View File

@ -9,6 +9,11 @@
import Foundation
import SQLite
private extension UserDefaults
{
@NSManaged var previousGamesDatabaseVersion: Int
}
extension ExpressionType
{
static var name: SQLite.Expression<String?> {
@ -19,13 +24,17 @@ extension ExpressionType
return SQLite.Expression<String?>("releaseCoverFront")
}
static var hash: SQLite.Expression<String> {
static var sha1Hash: SQLite.Expression<String> {
return SQLite.Expression<String>("romHashSHA1")
}
static var romID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("romID")
}
static var releaseID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("releaseID")
}
}
extension Table
@ -57,7 +66,9 @@ extension GamesDatabase
class GamesDatabase
{
fileprivate let connection: Connection
static let version = -1
private let connection: Connection
init() throws
{
@ -71,28 +82,36 @@ class GamesDatabase
{
throw Error.connection(error)
}
self.invalidateVirtualTableIfNeeded()
}
func metadataResults(forGameName gameName: String) -> [GameMetadata]
{
let releaseID = Expression<Any>.releaseID
let name = Expression<Any>.name
let artworkAddress = Expression<Any>.artworkAddress
let query = VirtualTable.search.select(name, artworkAddress).filter(name.match(gameName + "*"))
let query = VirtualTable.search.select(releaseID, name, artworkAddress).filter(name.match(gameName + "*"))
do
{
let rows = try self.connection.prepare(query)
let results = rows.map { row -> GameMetadata in
let metadata = GameMetadata()
metadata.name = row[name]
let results = rows.map { (row) -> GameMetadata in
let artworkURL: URL?
if let address = row[artworkAddress]
{
metadata.artworkURL = URL(string: address)
artworkURL = URL(string: address)
}
else
{
artworkURL = nil
}
let metadata = GameMetadata(identifier: row[releaseID], name: row[name], artworkURL: artworkURL)
return metadata
}
@ -118,26 +137,31 @@ class GamesDatabase
func metadata(for game: Game) -> GameMetadata?
{
let releaseID = Expression<Any>.releaseID
let name = Expression<Any>.name
let artworkAddress = Expression<Any>.artworkAddress
let hash = Expression<Any>.hash
let sha1Hash = Expression<Any>.sha1Hash
let romID = Expression<Any>.romID
let gameHash = game.identifier.uppercased()
let query = Table.roms.select(name, artworkAddress).filter(hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID])
let query = Table.roms.select(releaseID, name, artworkAddress).filter(sha1Hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID])
do
{
if let row = try self.connection.pluck(query)
{
let metadata = GameMetadata()
metadata.name = row[name]
let artworkURL: URL?
if let address = row[artworkAddress]
{
metadata.artworkURL = URL(string: address)
artworkURL = URL(string: address)
}
else
{
artworkURL = nil
}
let metadata = GameMetadata(identifier: row[releaseID], name: row[name], artworkURL: artworkURL)
return metadata
}
}
@ -152,16 +176,33 @@ class GamesDatabase
private extension GamesDatabase
{
func invalidateVirtualTableIfNeeded()
{
guard UserDefaults.standard.previousGamesDatabaseVersion != GamesDatabase.version else { return }
do
{
try self.connection.run(VirtualTable.search.drop(ifExists: true))
UserDefaults.standard.previousGamesDatabaseVersion = GamesDatabase.version
}
catch
{
print(error)
}
}
func prepareFTS() -> Bool
{
let name = Expression<Any>.name
let artworkAddress = Expression<Any>.artworkAddress
let releaseID = Expression<Any>.releaseID
do
{
try self.connection.run(VirtualTable.search.create(.FTS4([name, artworkAddress], tokenize: .Unicode61())))
try self.connection.run(VirtualTable.search.create(.FTS4([releaseID, name, artworkAddress], tokenize: .Unicode61())))
let update = VirtualTable.search.insert(Table.releases.select(name, artworkAddress))
let update = VirtualTable.search.insert(Table.releases.select(releaseID, name, artworkAddress))
_ = try self.connection.run(update)
}
catch

View File

@ -15,11 +15,9 @@ class GamesDatabaseBrowserViewController: UITableViewController
{
var selectionHandler: ((GameMetadata) -> Void)?
fileprivate let database: GamesDatabase?
fileprivate let dataSource: RSTArrayTableViewDataSource<GameMetadata>
private let database: GamesDatabase?
fileprivate let operationQueue = RSTOperationQueue()
fileprivate let imageCache = NSCache<NSURL, UIImage>()
private let dataSource: RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>
override init(style: UITableViewStyle) {
fatalError()
@ -37,39 +35,13 @@ class GamesDatabaseBrowserViewController: UITableViewController
print(error)
}
self.dataSource = RSTArrayTableViewDataSource<GameMetadata>(items: [])
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.textColor = UIColor.lightText
placeholderView.detailTextLabel.textColor = UIColor.lightText
self.dataSource.placeholderView = placeholderView
self.dataSource = RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>(items: [])
super.init(coder: aDecoder)
self.dataSource.cellConfigurationHandler = { (cell, metadata, indexPath) in
self.configure(cell: cell as! GameMetadataTableViewCell, with: metadata, for: indexPath)
}
if let database = self.database
{
self.dataSource.searchController.searchHandler = { [unowned database, unowned dataSource] (searchValue, previousSearchValue) in
return RSTBlockOperation(executionBlock: { [unowned database, unowned dataSource] (operation) in
let results = database.metadataResults(forGameName: searchValue.text)
guard !operation.isCancelled else { return }
dataSource.items = results
rst_dispatch_sync_on_main_thread {
self.updatePlaceholderView()
}
})
}
}
self.definesPresentationContext = true
self.prepareDataSource()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
@ -83,6 +55,8 @@ class GamesDatabaseBrowserViewController: UITableViewController
self.view.backgroundColor = UIColor.deltaDarkGray
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
self.tableView.indicatorStyle = .white
self.tableView.separatorColor = UIColor.gray
@ -99,7 +73,73 @@ class GamesDatabaseBrowserViewController: UITableViewController
}
}
extension GamesDatabaseBrowserViewController
private extension GamesDatabaseBrowserViewController
{
func prepareDataSource()
{
/* Placeholder View */
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.textColor = UIColor.lightText
placeholderView.detailTextLabel.textColor = UIColor.lightText
self.dataSource.placeholderView = placeholderView
/* Cell Configuration */
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, metadata, indexPath) in
self.configure(cell: cell as! GameMetadataTableViewCell, with: metadata, for: indexPath)
}
/* Prefetching */
self.dataSource.prefetchHandler = { (metadata, indexPath, completionHandler) in
guard let artworkURL = metadata.artworkURL else { return nil }
let operation = LoadImageURLOperation(url: artworkURL)
operation.resultHandler = { (image, error) in
completionHandler(image, error)
}
return operation
}
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard let image = image else { return }
let cell = cell as! GameMetadataTableViewCell
let artworkDisplaySize = AVMakeRect(aspectRatio: image.size, insideRect: cell.artworkImageView.bounds)
let offset = (cell.artworkImageView.bounds.width - artworkDisplaySize.width) / 2
// Offset artworkImageViewLeadingConstraint and artworkImageViewTrailingConstraint to right-align artworkImageView
cell.artworkImageViewLeadingConstraint.constant += offset
cell.artworkImageViewTrailingConstraint.constant -= offset
cell.artworkImageView.image = image
cell.artworkImageView.superview?.layoutIfNeeded()
}
/* Searching */
if let database = self.database
{
self.dataSource.searchController.searchHandler = { [unowned self, unowned database] (searchValue, previousSearchValue) in
return RSTBlockOperation() { [unowned self, unowned database] (operation) in
let results = database.metadataResults(forGameName: searchValue.text)
guard !operation.isCancelled else { return }
self.dataSource.items = results
rst_dispatch_sync_on_main_thread {
self.resetTableViewContentOffset()
self.updatePlaceholderView()
}
}
}
}
}
}
private extension GamesDatabaseBrowserViewController
{
func configure(cell: GameMetadataTableViewCell, with metadata: GameMetadata, for indexPath: IndexPath)
{
@ -112,30 +152,6 @@ extension GamesDatabaseBrowserViewController
cell.artworkImageViewTrailingConstraint.constant = 15
cell.separatorInset.left = cell.nameLabel.frame.minX
if let artworkURL = metadata.artworkURL
{
let operation = LoadImageURLOperation(url: artworkURL)
operation.resultsCache = self.imageCache
operation.resultHandler = { (image, error) in
if let image = image
{
let artworkDisplaySize = AVMakeRect(aspectRatio: image.size, insideRect: cell.artworkImageView.bounds)
let offset = (cell.artworkImageView.bounds.width - artworkDisplaySize.width) / 2
DispatchQueue.main.async {
// Offset artworkImageViewLeadingConstraint and artworkImageViewTrailingConstraint to right-align artworkImageView
cell.artworkImageViewLeadingConstraint.constant += offset
cell.artworkImageViewTrailingConstraint.constant -= offset
cell.artworkImageView.image = image
cell.artworkImageView.superview?.layoutIfNeeded()
}
}
}
self.operationQueue.addOperation(operation, forKey: indexPath as NSIndexPath)
}
}
func updatePlaceholderView()
@ -153,6 +169,12 @@ extension GamesDatabaseBrowserViewController
placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure the name is correct, or try searching for another game.", comment: "")
}
}
func resetTableViewContentOffset()
{
self.tableView.setContentOffset(CGPoint.zero, animated: false)
self.tableView.setContentOffset(CGPoint(x: 0, y: -self.topLayoutGuide.length), animated: false)
}
}
extension GamesDatabaseBrowserViewController
@ -167,12 +189,6 @@ extension GamesDatabaseBrowserViewController
let metadata = self.dataSource.item(at: indexPath)
self.selectionHandler?(metadata)
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
let operation = self.operationQueue[indexPath as NSIndexPath]
operation?.cancel()
}
}
extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
@ -193,7 +209,6 @@ extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
func didDismissSearchController(_ searchController: UISearchController)
{
// Fix potentially incorrect offset if user dismisses searchController while scrolling
self.tableView.setContentOffset(CGPoint.zero, animated: false)
self.tableView.setContentOffset(CGPoint(x: 0, y: -self.topLayoutGuide.length), animated: false)
self.resetTableViewContentOffset()
}
}

View File

@ -0,0 +1,28 @@
//
// ActionInput.swift
// Delta
//
// Created by Riley Testut on 8/28/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import DeltaCore
public extension GameControllerInputType
{
static let action = GameControllerInputType("com.rileytestut.Delta.input.action")
}
enum ActionInput: String
{
case quickSave
case quickLoad
case fastForward
}
extension ActionInput: Input
{
var type: InputType {
return .controller(.action)
}
}

View File

@ -29,6 +29,25 @@ private extension GameViewController
self.gameType = gameType
}
}
struct SustainInputsMapping: GameControllerInputMappingProtocol
{
let gameController: GameController
var gameControllerInputType: GameControllerInputType {
return self.gameController.inputType
}
func input(forControllerInput controllerInput: Input) -> Input?
{
if let mappedInput = self.gameController.defaultInputMapping?.input(forControllerInput: controllerInput), mappedInput == StandardGameControllerInput.menu
{
return mappedInput
}
return controllerInput
}
}
}
class GameViewController: DeltaCore.GameViewController
@ -53,11 +72,11 @@ class GameViewController: DeltaCore.GameViewController
}
//MARK: - Private Properties -
fileprivate var pauseViewController: PauseViewController?
fileprivate var pausingGameController: GameController?
private var pauseViewController: PauseViewController?
private var pausingGameController: GameController?
// Prevents the same save state from being saved multiple times
fileprivate var pausedSaveState: PausedSaveState? {
private var pausedSaveState: PausedSaveState? {
didSet
{
if let saveState = oldValue, self.pausedSaveState == nil
@ -74,25 +93,20 @@ class GameViewController: DeltaCore.GameViewController
}
}
fileprivate var _isLoadingSaveState = false
private var _isLoadingSaveState = false
fileprivate var context = CIContext(options: [kCIContextWorkingColorSpace: NSNull()])
private var context = CIContext(options: [kCIContextWorkingColorSpace: NSNull()])
// Sustain Buttons
fileprivate var updateSemaphores = Set<DispatchSemaphore>()
fileprivate var sustainedInputs = [ObjectIdentifier: [Input]]()
fileprivate var reactivateSustainedInputsQueue: OperationQueue
fileprivate var selectingSustainedButtons = false
private var isSelectingSustainedButtons = false
private var sustainInputsMapping: SustainInputsMapping?
fileprivate var sustainButtonsContentView: UIView!
fileprivate var sustainButtonsBlurView: UIVisualEffectView!
fileprivate var sustainButtonsBackgroundView: RSTPlaceholderView!
private var sustainButtonsContentView: UIView!
private var sustainButtonsBlurView: UIVisualEffectView!
private var sustainButtonsBackgroundView: RSTPlaceholderView!
required init()
{
self.reactivateSustainedInputsQueue = OperationQueue()
self.reactivateSustainedInputsQueue.maxConcurrentOperationCount = 1
super.init()
self.initialize()
@ -100,9 +114,6 @@ class GameViewController: DeltaCore.GameViewController
required init?(coder aDecoder: NSCoder)
{
self.reactivateSustainedInputsQueue = OperationQueue()
self.reactivateSustainedInputsQueue.maxConcurrentOperationCount = 1
super.init(coder: aDecoder)
self.initialize()
@ -112,8 +123,8 @@ class GameViewController: DeltaCore.GameViewController
{
self.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalGameControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalGameControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didEnterBackground(with:)), name: .UIApplicationDidEnterBackground, object: UIApplication.shared)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.settingsDidChange(with:)), name: .settingsDidChange, object: nil)
}
@ -128,19 +139,42 @@ class GameViewController: DeltaCore.GameViewController
{
super.gameController(gameController, didActivate: input)
guard (input as? ControllerInput) != .menu else { return }
if self.selectingSustainedButtons
if self.isSelectingSustainedButtons
{
self.addSustainedInput(input, for: gameController)
}
else if let sustainedInputs = self.sustainedInputs[ObjectIdentifier(gameController)], sustainedInputs.contains(where: { $0.isEqual(input) })
{
// Perform on next run loop
DispatchQueue.main.async {
self.reactivateSustainedInput(input, for: gameController)
guard let pausingGameController = self.pausingGameController, gameController == pausingGameController else { return }
if input != StandardGameControllerInput.menu
{
gameController.sustain(input)
}
}
else if self.emulatorCore?.state == .running
{
guard let actionInput = ActionInput(input: input) else { return }
switch actionInput
{
case .quickSave: self.performQuickSaveAction()
case .quickLoad: self.performQuickLoadAction()
case .fastForward: self.performFastForwardAction(activate: true)
}
}
}
override func gameController(_ gameController: GameController, didDeactivate input: Input)
{
super.gameController(gameController, didDeactivate: input)
guard !self.isSelectingSustainedButtons else { return }
guard let actionInput = ActionInput(input: input) else { return }
switch actionInput
{
case .quickSave: break
case .quickLoad: break
case .fastForward: self.performFastForwardAction(activate: false)
}
}
}
@ -153,10 +187,15 @@ extension GameViewController
{
super.viewDidLoad()
// Lays out self.gameView, so we can pin self.sustainButtonsContentView to it without resulting in a temporary "cannot satisfy constraints".
self.view.layoutIfNeeded()
let gameViewContainerView = self.gameView.superview!
self.sustainButtonsContentView = UIView(frame: CGRect(x: 0, y: 0, width: self.gameView.bounds.width, height: self.gameView.bounds.height))
self.sustainButtonsContentView.translatesAutoresizingMaskIntoConstraints = false
self.sustainButtonsContentView.isHidden = true
self.view.insertSubview(self.sustainButtonsContentView, aboveSubview: self.gameView)
self.view.insertSubview(self.sustainButtonsContentView, aboveSubview: gameViewContainerView)
let blurEffect = UIBlurEffect(style: .dark)
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
@ -182,10 +221,10 @@ extension GameViewController
vibrancyView.contentView.addSubview(self.sustainButtonsBackgroundView)
// Auto Layout
self.sustainButtonsContentView.leadingAnchor.constraint(equalTo: self.gameView.leadingAnchor).isActive = true
self.sustainButtonsContentView.trailingAnchor.constraint(equalTo: self.gameView.trailingAnchor).isActive = true
self.sustainButtonsContentView.topAnchor.constraint(equalTo: self.gameView.topAnchor).isActive = true
self.sustainButtonsContentView.bottomAnchor.constraint(equalTo: self.gameView.bottomAnchor).isActive = true
self.sustainButtonsContentView.leadingAnchor.constraint(equalTo: gameViewContainerView.leadingAnchor).isActive = true
self.sustainButtonsContentView.trailingAnchor.constraint(equalTo: gameViewContainerView.trailingAnchor).isActive = true
self.sustainButtonsContentView.topAnchor.constraint(equalTo: gameViewContainerView.topAnchor).isActive = true
self.sustainButtonsContentView.bottomAnchor.constraint(equalTo: gameViewContainerView.bottomAnchor).isActive = true
self.updateControllerSkin()
self.updateControllers()
@ -220,7 +259,7 @@ extension GameViewController
if let game = self.game
{
let fileURL = FileManager.uniqueTemporaryURL()
let fileURL = FileManager.default.uniqueTemporaryURL()
self.pausedSaveState = PausedSaveState(fileURL: fileURL, gameType: game.type)
self.emulatorCore?.saveSaveState(to: fileURL)
@ -238,22 +277,27 @@ extension GameViewController
pauseViewController.saveStatesViewControllerDelegate = self
pauseViewController.cheatsViewControllerDelegate = self
pauseViewController.fastForwardItem?.selected = (self.emulatorCore?.rate != self.emulatorCore?.configuration.supportedRates.lowerBound)
pauseViewController.fastForwardItem?.isSelected = (self.emulatorCore?.rate != self.emulatorCore?.deltaCore.supportedRates.lowerBound)
pauseViewController.fastForwardItem?.action = { [unowned self] item in
guard let emulatorCore = self.emulatorCore else { return }
emulatorCore.rate = item.selected ? emulatorCore.configuration.supportedRates.upperBound : emulatorCore.configuration.supportedRates.lowerBound
self.performFastForwardAction(activate: item.isSelected)
}
pauseViewController.sustainButtonsItem?.selected = (self.sustainedInputs[ObjectIdentifier(gameController)]?.count ?? 0) > 0
pauseViewController.sustainButtonsItem?.isSelected = gameController.sustainedInputs.count > 0
pauseViewController.sustainButtonsItem?.action = { [unowned self, unowned pauseViewController] item in
self.resetSustainedInputs(for: gameController)
for input in gameController.sustainedInputs
{
gameController.unsustain(input)
}
if item.selected
if item.isSelected
{
self.showSustainButtonView()
pauseViewController.dismiss()
}
// Re-set gameController as pausingGameController.
self.pausingGameController = gameController
}
self.pauseViewController = pauseViewController
@ -341,13 +385,14 @@ private extension GameViewController
{
@objc func updateControllers()
{
var controllers = [GameController]()
controllers.append(self.controllerView)
let isExternalGameControllerConnected = ExternalGameControllerManager.shared.connectedControllers.contains(where: { $0.playerIndex != nil })
if !isExternalGameControllerConnected && Settings.localControllerPlayerIndex == nil
{
Settings.localControllerPlayerIndex = 0
}
// We need to map each item as a GameControllerProtocol due to a Swift bug
controllers.append(contentsOf: ExternalControllerManager.shared.connectedControllers.map { $0 as GameController })
if let index = Settings.localControllerPlayerIndex
// If Settings.localControllerPlayerIndex is non-nil, and there isn't a connected controller with same playerIndex, show controller view.
if let index = Settings.localControllerPlayerIndex, !ExternalGameControllerManager.shared.connectedControllers.contains { $0.playerIndex == index }
{
self.controllerView.playerIndex = index
self.controllerView.isHidden = false
@ -356,44 +401,48 @@ private extension GameViewController
{
self.controllerView.playerIndex = nil
self.controllerView.isHidden = true
}
// Removing all game controllers from EmulatorCore will reset each controller's playerIndex to nil
// We temporarily cache their playerIndexes, and then we reset them after removing all controllers
var controllerIndexes = [ObjectIdentifier: Int?]()
controllers.forEach { controllerIndexes[ObjectIdentifier($0)] = $0.playerIndex }
self.emulatorCore?.removeAllGameControllers()
// Reset each controller's playerIndex to what it was before removing all controllers from EmulatorCore
controllers.forEach { $0.playerIndex = controllerIndexes[ObjectIdentifier($0)] ?? nil }
for controller in controllers
{
if let index = controller.playerIndex
{
// We need to place the underscore here to silence erroneous unused result warning despite annotating function with @discardableResult
// Hopefully this bug won't be around for too long...
_ = self.emulatorCore?.setGameController(controller, at: index)
controller.addReceiver(self)
}
else
{
controller.removeReceiver(self)
}
Settings.localControllerPlayerIndex = nil
}
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
if let emulatorCore = self.emulatorCore, let game = self.game
{
let controllers = [self.controllerView as GameController] + ExternalGameControllerManager.shared.connectedControllers
for gameController in controllers
{
if gameController.playerIndex != nil
{
if let inputMapping = GameControllerInputMapping.inputMapping(for: gameController, gameType: game.type, in: DatabaseManager.shared.viewContext)
{
gameController.addReceiver(self, inputMapping: inputMapping)
gameController.addReceiver(emulatorCore, inputMapping: inputMapping)
}
else
{
gameController.addReceiver(self)
gameController.addReceiver(emulatorCore)
}
}
else
{
gameController.removeReceiver(self)
gameController.removeReceiver(emulatorCore)
}
}
}
}
func updateControllerSkin()
{
guard let game = self.game else { return }
guard let game = self.game, let system = System(gameType: game.type) else { return }
let traits = DeltaCore.ControllerSkin.Traits.defaults(for: self.view)
let controllerSkin = Settings.preferredControllerSkin(for: game.type, traits: traits)
let controllerSkin = Settings.preferredControllerSkin(for: system, traits: traits)
self.controllerView.controllerSkin = controllerSkin
if controllerSkin?.isTranslucent(for: traits) ?? false
@ -411,7 +460,7 @@ private extension GameViewController
/// Save States
extension GameViewController: SaveStatesViewControllerDelegate
{
fileprivate func updateAutoSaveState()
private func updateAutoSaveState()
{
// Ensures game is non-nil and also a Game subclass
guard let game = self.game as? Game else { return }
@ -429,47 +478,42 @@ extension GameViewController: SaveStatesViewControllerDelegate
let game = backgroundContext.object(with: game.objectID) as! Game
let predicate = NSPredicate(format: "%K == %d AND %K == %@", #keyPath(SaveState.type), SaveStateType.auto.rawValue, #keyPath(SaveState.game), game)
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
fetchRequest.predicate = predicate
let fetchRequest = SaveState.fetchRequest(for: game, type: .auto)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(SaveState.creationDate), ascending: true)]
var saveStates: [SaveState]? = nil
do
{
saveStates = try fetchRequest.execute()
let saveStates = try fetchRequest.execute()
if let saveState = saveStates.first, saveStates.count >= 2
{
// If there are two or more auto save states, update the oldest one
self.update(saveState, with: self.pausedSaveState)
// Tiny hack: SaveStatesViewController sorts save states by creation date, so we update the creation date too
// Simpler than deleting old save states ¯\_()_/¯
saveState.creationDate = saveState.modifiedDate
}
else
{
// Otherwise, create a new one
let saveState = SaveState.insertIntoManagedObjectContext(backgroundContext)
saveState.type = .auto
saveState.game = game
self.update(saveState, with: self.pausedSaveState)
}
}
catch
{
print(error)
}
if let saveStates = saveStates, let saveState = saveStates.first, saveStates.count >= 2
{
// If there are two or more auto save states, update the oldest one
self.update(saveState, with: self.pausedSaveState)
// Tiny hack; SaveStatesViewController sorts save states by creation date, so we update the creation date too
// Simpler than deleting old save states ¯\_()_/¯
saveState.creationDate = saveState.modifiedDate
}
else
{
// Otherwise, create a new one
let saveState = SaveState.insertIntoManagedObjectContext(backgroundContext)
saveState.type = .auto
saveState.game = game
self.update(saveState, with: self.pausedSaveState)
}
backgroundContext.saveWithErrorLogging()
}
}
fileprivate func update(_ saveState: SaveState, with replacementSaveState: SaveStateProtocol? = nil)
private func update(_ saveState: SaveState, with replacementSaveState: SaveStateProtocol? = nil)
{
let isRunning = (self.emulatorCore?.state == .running)
@ -523,25 +567,14 @@ extension GameViewController: SaveStatesViewControllerDelegate
}
}
//MARK: - SaveStatesViewControllerDelegate
func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, updateSaveState saveState: SaveState)
private func load(_ saveState: SaveStateProtocol)
{
let updatingExistingSaveState = FileManager.default.fileExists(atPath: saveState.fileURL.path)
let isRunning = (self.emulatorCore?.state == .running)
self.update(saveState)
// Dismiss if updating an existing save state.
// If creating a new one, don't dismiss.
if updatingExistingSaveState
if isRunning
{
self.pauseViewController?.dismiss()
self.pauseEmulation()
}
}
func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, loadSaveState saveState: SaveStateProtocol)
{
self._isLoadingSaveState = true
// If we're loading the auto save state, we need to create a temporary copy of saveState.
// Then, we update the auto save state, but load our copy so everything works out.
@ -549,7 +582,7 @@ extension GameViewController: SaveStatesViewControllerDelegate
if let autoSaveState = saveState as? SaveState, autoSaveState.type == .auto
{
let temporaryURL = FileManager.uniqueTemporaryURL()
let temporaryURL = FileManager.default.uniqueTemporaryURL()
do
{
@ -585,16 +618,33 @@ extension GameViewController: SaveStatesViewControllerDelegate
print(error)
}
// Reactivate sustained inputs
for gameController in self.emulatorCore?.gameControllers ?? []
if isRunning
{
guard let sustainedInputs = self.sustainedInputs[ObjectIdentifier(gameController)] else { continue }
for input in sustainedInputs
{
self.reactivateSustainedInput(input, for: gameController)
}
self.resumeEmulation()
}
}
//MARK: - SaveStatesViewControllerDelegate
func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, updateSaveState saveState: SaveState)
{
let updatingExistingSaveState = FileManager.default.fileExists(atPath: saveState.fileURL.path)
self.update(saveState)
// Dismiss if updating an existing save state.
// If creating a new one, don't dismiss.
if updatingExistingSaveState
{
self.pauseViewController?.dismiss()
}
}
func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, loadSaveState saveState: SaveStateProtocol)
{
self._isLoadingSaveState = true
self.load(saveState)
self.pauseViewController?.dismiss()
}
@ -620,7 +670,12 @@ private extension GameViewController
{
func showSustainButtonView()
{
self.selectingSustainedButtons = true
guard let gameController = self.pausingGameController else { return }
self.isSelectingSustainedButtons = true
let sustainInputsMapping = SustainInputsMapping(gameController: gameController)
gameController.addReceiver(self, inputMapping: sustainInputsMapping)
let blurEffect = self.sustainButtonsBlurView.effect
self.sustainButtonsBlurView.effect = nil
@ -635,7 +690,18 @@ private extension GameViewController
func hideSustainButtonView()
{
self.selectingSustainedButtons = false
guard let gameController = self.pausingGameController else { return }
self.isSelectingSustainedButtons = false
self.updateControllers()
self.sustainInputsMapping = nil
// Reactivate all sustained inputs, since they will now be mapped to game inputs.
for input in gameController.sustainedInputs
{
gameController.activate(input)
}
let blurEffect = self.sustainButtonsBlurView.effect
@ -647,94 +713,76 @@ private extension GameViewController
self.sustainButtonsBlurView.effect = blurEffect
}
}
func resetSustainedInputs(for gameController: GameController)
}
//MARK: - Action Inputs -
/// Action Inputs
extension GameViewController
{
func performQuickSaveAction()
{
if let previousInputs = self.sustainedInputs[ObjectIdentifier(gameController)]
{
let receivers = gameController.receivers
receivers.forEach { gameController.removeReceiver($0) }
// Activate previousInputs without notifying anyone so we can then deactivate them
// We do this because deactivating an already deactivated input has no effect
previousInputs.forEach { gameController.activate($0) }
receivers.forEach { gameController.addReceiver($0) }
// Deactivate previously sustained inputs
previousInputs.forEach { gameController.deactivate($0) }
}
guard let game = self.game as? Game else { return }
self.sustainedInputs[ObjectIdentifier(gameController)] = []
}
func addSustainedInput(_ input: Input, for gameController: GameController)
{
var inputs = self.sustainedInputs[ObjectIdentifier(gameController)] ?? []
guard !inputs.contains(where: { $0.isEqual(input) }) else { return }
inputs.append(input)
self.sustainedInputs[ObjectIdentifier(gameController)] = inputs
let receivers = gameController.receivers
receivers.forEach { gameController.removeReceiver($0) }
// Causes input to be considered deactivated, so gameController won't send a subsequent message to observers when user actually deactivates
// However, at this point the core still thinks it is activated, and is temporarily not a receiver, thus sustaining it
gameController.deactivate(input)
receivers.forEach { gameController.addReceiver($0) }
}
func reactivateSustainedInput(_ input: Input, for gameController: GameController)
{
// These MUST be performed serially, or else Bad Things Happen if multiple inputs are reactivated at once
self.reactivateSustainedInputsQueue.addOperation {
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.performAndWait {
// The manual activations/deactivations here are hidden implementation details, so we won't notify ourselves about them
gameController.removeReceiver(self)
let game = backgroundContext.object(with: game.objectID) as! Game
let fetchRequest = SaveState.fetchRequest(for: game, type: .quick)
// Must deactivate first so core recognizes a secondary activation
gameController.deactivate(input)
let dispatchQueue = DispatchQueue(label: "com.rileytestut.Delta.sustainButtonsQueue")
dispatchQueue.async {
let semaphore = DispatchSemaphore(value: 0)
self.updateSemaphores.insert(semaphore)
// To ensure the emulator core recognizes us activating the input again, we need to wait at least two frames
// Unfortunately we cannot init DispatchSemaphore with value less than 0
// To compensate, we simply wait twice; once the first wait returns, we wait again
semaphore.wait()
semaphore.wait()
// These MUST be performed serially, or else Bad Things Happen if multiple inputs are reactivated at once
self.reactivateSustainedInputsQueue.addOperation {
self.updateSemaphores.remove(semaphore)
// Ensure we still are not a receiver (to prevent rare race conditions)
gameController.removeReceiver(self)
gameController.activate(input)
let receivers = gameController.receivers
receivers.forEach { gameController.removeReceiver($0) }
// Causes input to be considered deactivated, so gameController won't send a subsequent message to observers when user actually deactivates
// However, at this point the core still thinks it is activated, and is temporarily not a receiver, thus sustaining it
gameController.deactivate(input)
receivers.forEach { gameController.addReceiver($0) }
do
{
if let quickSaveState = try fetchRequest.execute().first
{
self.update(quickSaveState)
}
else
{
let saveState = SaveState(context: backgroundContext)
saveState.type = .quick
saveState.game = game
self.update(saveState)
}
// More Bad Things Happen if we add self as observer before ALL reactivations have occurred (notable, infinite loops)
self.reactivateSustainedInputsQueue.waitUntilAllOperationsAreFinished()
gameController.addReceiver(self)
}
catch
{
print(error)
}
backgroundContext.saveWithErrorLogging()
}
}
func performQuickLoadAction()
{
guard let game = self.game as? Game else { return }
let fetchRequest = SaveState.fetchRequest(for: game, type: .quick)
do
{
if let quickSaveState = try DatabaseManager.shared.viewContext.fetch(fetchRequest).first
{
self.load(quickSaveState)
}
}
catch
{
print(error)
}
}
func performFastForwardAction(activate: Bool)
{
guard let emulatorCore = self.emulatorCore else { return }
if activate
{
emulatorCore.rate = emulatorCore.deltaCore.supportedRates.upperBound
}
else
{
emulatorCore.rate = emulatorCore.deltaCore.supportedRates.lowerBound
}
}
}
@ -745,12 +793,17 @@ extension GameViewController: GameViewControllerDelegate
{
func gameViewController(_ gameViewController: DeltaCore.GameViewController, handleMenuInputFrom gameController: GameController)
{
if self.selectingSustainedButtons
if let pausingGameController = self.pausingGameController
{
guard pausingGameController == gameController else { return }
}
if self.isSelectingSustainedButtons
{
self.hideSustainButtonView()
}
if let pauseViewController = self.pauseViewController, !self.selectingSustainedButtons
if let pauseViewController = self.pauseViewController, !self.isSelectingSustainedButtons
{
pauseViewController.dismiss()
}
@ -763,15 +816,7 @@ extension GameViewController: GameViewControllerDelegate
func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool
{
return (self.presentedViewController == nil || self.presentedViewController?.isDisappearing == true) && !self.selectingSustainedButtons && self.view.window != nil
}
func gameViewControllerDidUpdate(_ gameViewController: DeltaCore.GameViewController)
{
for semaphore in self.updateSemaphores
{
semaphore.signal()
}
return (self.presentedViewController == nil || self.presentedViewController?.isDisappearing == true) && !self.isSelectingSustainedButtons && self.view.window != nil
}
}
@ -805,12 +850,12 @@ private extension GameViewController
case .preferredControllerSkin:
guard
let gameType = notification.userInfo?[Settings.NotificationUserInfoKey.gameType] as? GameType,
let system = notification.userInfo?[Settings.NotificationUserInfoKey.system] as? System,
let traits = notification.userInfo?[Settings.NotificationUserInfoKey.traits] as? DeltaCore.ControllerSkin.Traits
else { return }
let currentTraits = DeltaCore.ControllerSkin.Traits.defaults(for: self.view)
if gameType == self.game?.type && traits == currentTraits
if system.gameType == self.game?.type && traits == currentTraits
{
self.updateControllerSkin()
}

View File

@ -27,7 +27,7 @@ class PreviewGameViewController: DeltaCore.GameViewController
}
}
fileprivate var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
private var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
override var game: GameProtocol? {
willSet {

View File

@ -0,0 +1,23 @@
//
// DeltaCoreProtocol+Delta.swift
// Delta
//
// Created by Riley Testut on 4/30/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import DeltaCore
extension DeltaCoreProtocol
{
var supportedRates: ClosedRange<Double> {
guard let system = System(gameType: self.gameType) else { return 1...1 }
switch system
{
case .snes: return 1...4
case .gba: return 1...3
case .gbc: return 1...4
}
}
}

View File

@ -1,31 +0,0 @@
//
// GameType+Delta.swift
// Delta
//
// Created by Riley Testut on 12/22/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import DeltaCore
extension GameType
{
static var supportedTypes: Set<GameType>
{
return [GameType.snes, GameType.gba]
}
static func gameType(forFileExtension fileExtension: String) -> GameType
{
let gameType: GameType
switch fileExtension.lowercased()
{
case "smc", "sfc", "fig": gameType = GameType.snes
case "gba": gameType = GameType.gba
default: gameType = GameType.unknown
}
return gameType
}
}

View File

@ -1,34 +0,0 @@
//
// GameType+Localization.swift
// Delta
//
// Created by Riley Testut on 10/3/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import DeltaCore
extension GameType
{
var localizedName: String
{
switch self
{
case GameType.snes: return NSLocalizedString("Super Nintendo Entertainment System", comment: "")
case GameType.gba: return NSLocalizedString("Game Boy Advance", comment: "")
case GameType.unknown: return NSLocalizedString("Unsupported System", comment: "")
default: return NSLocalizedString("Unknown", comment: "")
}
}
var localizedShortName: String
{
switch self
{
case GameType.snes: return NSLocalizedString("SNES", comment: "")
case GameType.gba: return NSLocalizedString("GBA", comment: "")
case GameType.unknown: return NSLocalizedString("Unsupported", comment: "")
default: return NSLocalizedString("Unknown", comment: "")
}
}
}

View File

@ -0,0 +1,112 @@
//
// Input+Display.swift
// Delta
//
// Created by Riley Testut on 8/15/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import DeltaCore
extension Input
{
// With the default GameControllerInputMapping files, multiple controller inputs may map to the same game input.
// This is because each controller input maps to a unique standard input, but then multiple standard inputs may map to same game input.
// To ensure we only show the most "important" controller input for a game input, we define general "display priorities" for each input.
//
// For example, MFiGameController.down and MFiGameController.leftThumbstickDown both map to a "down" game input.
// However, .down has a higher priority than .leftThumbstickDown, so we show .down instead of .leftThumbstickDown.
var displayPriority: Int {
switch self.type
{
case .game: break
case .controller(.standard): break
case .controller(.mfi):
let input = MFiGameController.Input(input: self)!
switch input
{
case .leftThumbstickUp: return 750
case .leftThumbstickDown: return 750
case .leftThumbstickLeft: return 750
case .leftThumbstickRight: return 750
case .leftShoulder: return 750
case .leftTrigger: return 500
case .rightShoulder: return 750
case .rightTrigger: return 500
default: break
}
default: break
}
return 1000
}
var localizedName: String {
switch self.type
{
case .game: break
case .controller(.standard):
let input = StandardGameControllerInput(input: self)!
switch input
{
case .menu: return NSLocalizedString("Menu", comment: "")
case .up: return NSLocalizedString("Up", comment: "")
case .down: return NSLocalizedString("Down", comment: "")
case .left: return NSLocalizedString("Left", comment: "")
case .right: return NSLocalizedString("Right", comment: "")
case .leftThumbstickUp: return NSLocalizedString("L🕹↑", comment: "")
case .leftThumbstickDown: return NSLocalizedString("L🕹↓", comment: "")
case .leftThumbstickLeft: return NSLocalizedString("L🕹←", comment: "")
case .leftThumbstickRight: return NSLocalizedString("L🕹→", comment: "")
case .rightThumbstickUp: return NSLocalizedString("R🕹↑", comment: "")
case .rightThumbstickDown: return NSLocalizedString("R🕹↓", comment: "")
case .rightThumbstickLeft: return NSLocalizedString("R🕹←", comment: "")
case .rightThumbstickRight: return NSLocalizedString("R🕹→", comment: "")
case .a: return NSLocalizedString("A", comment: "")
case .b: return NSLocalizedString("B", comment: "")
case .x: return NSLocalizedString("X", comment: "")
case .y: return NSLocalizedString("Y", comment: "")
case .start: return NSLocalizedString("Start", comment: "Start button")
case .select: return NSLocalizedString("Select", comment: "Select button")
case .l1: return NSLocalizedString("L1", comment: "")
case .l2: return NSLocalizedString("L2", comment: "")
case .l3: return NSLocalizedString("L3", comment: "")
case .r1: return NSLocalizedString("R1", comment: "")
case .r2: return NSLocalizedString("R2", comment: "")
case .r3: return NSLocalizedString("R3", comment: "")
}
case .controller(.mfi):
let input = MFiGameController.Input(input: self)!
switch input
{
case .menu: return NSLocalizedString("Menu", comment: "")
case .up: return NSLocalizedString("Up", comment: "")
case .down: return NSLocalizedString("Down", comment: "")
case .left: return NSLocalizedString("Left", comment: "")
case .right: return NSLocalizedString("Right", comment: "")
case .leftThumbstickUp: return NSLocalizedString("L🕹↑", comment: "")
case .leftThumbstickDown: return NSLocalizedString("L🕹↓", comment: "")
case .leftThumbstickLeft: return NSLocalizedString("L🕹←", comment: "")
case .leftThumbstickRight: return NSLocalizedString("L🕹→", comment: "")
case .rightThumbstickUp: return NSLocalizedString("R🕹↑", comment: "")
case .rightThumbstickDown: return NSLocalizedString("R🕹↓", comment: "")
case .rightThumbstickLeft: return NSLocalizedString("R🕹←", comment: "")
case .rightThumbstickRight: return NSLocalizedString("R🕹→", comment: "")
case .a: return NSLocalizedString("A", comment: "")
case .b: return NSLocalizedString("B", comment: "")
case .x: return NSLocalizedString("X", comment: "")
case .y: return NSLocalizedString("Y", comment: "")
case .leftShoulder: return NSLocalizedString("L1", comment: "")
case .leftTrigger: return NSLocalizedString("L2", comment: "")
case .rightShoulder: return NSLocalizedString("R1", comment: "")
case .rightTrigger: return NSLocalizedString("R2", comment: "")
}
default: break
}
return ""
}
}

View File

@ -38,6 +38,7 @@ extension UIAlertController
{
case .doesNotExist(let url): urls.insert(url)
case .invalid(let url): urls.insert(url)
case .unsupported(let url): urls.insert(url)
case .unknown(let url, _): urls.insert(url)
case .saveFailed(let errorURLs, _): urls.formUnion(errorURLs)
}

View File

@ -12,7 +12,7 @@ extension UIColor
{
class var deltaPurple: UIColor
{
return UIColor(red: 140.0/255.0, green: 26.0/255.0, blue: 233.0/255.0, alpha: 1.0)
return UIColor(red: 139.0/255.0, green: 40.0/255.0, blue: 247.0/255.0, alpha: 1.0)
}
class var deltaDarkGray: UIColor

View File

@ -18,7 +18,7 @@ internal extension UILabel
context.minimumScaleFactor = self.minimumScaleFactor
// Using self.attributedString returns incorrect calculations, so we create our own attributed string
let attributedString = NSAttributedString(string: text, attributes: [NSFontAttributeName: self.font])
let attributedString = NSAttributedString(string: text, attributes: [.font: self.font])
attributedString.boundingRect(with: self.bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: context)
let scaleFactor = context.actualScaleFactor

View File

@ -0,0 +1,28 @@
//
// UIView+ParentViewController.swift
// Delta
//
// Created by Riley Testut on 9/3/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
extension UIView
{
var parentViewController: UIViewController? {
var nextResponder = self.next
while nextResponder != nil
{
if let parentViewController = nextResponder as? UIViewController
{
return parentViewController
}
nextResponder = nextResponder?.next
}
return nil
}
}

View File

@ -21,11 +21,11 @@ extension UIViewControllerContextTransitioning
/// UIViews
var sourceView: UIView {
return self.sourceViewController.view
return self.view(forKey: .from) ?? self.sourceViewController.view
}
var destinationView: UIView {
return self.destinationViewController.view
return self.view(forKey: .to) ?? self.destinationViewController.view
}

View File

@ -7,6 +7,7 @@
//
import UIKit
import MobileCoreServices
import DeltaCore
@ -16,9 +17,9 @@ import SDWebImage
class GameCollectionViewController: UICollectionViewController
{
var gameCollection: GameCollection! {
var gameCollection: GameCollection? {
didSet {
self.title = self.gameCollection.shortName
self.title = self.gameCollection?.shortName
self.updateDataSource()
}
}
@ -40,17 +41,28 @@ class GameCollectionViewController: UICollectionViewController
}
}
internal let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource<Game, UIImage>
weak var activeEmulatorCore: EmulatorCore?
fileprivate var activeSaveState: SaveStateProtocol?
private var activeSaveState: SaveStateProtocol?
fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource<Game>(fetchedResultsController: NSFetchedResultsController())
fileprivate let prototypeCell = GridCollectionViewCell()
private let prototypeCell = GridCollectionViewCell()
fileprivate var _performing3DTouchTransition = false
fileprivate weak var _destination3DTouchTransitionViewController: UIViewController?
private var _performing3DTouchTransition = false
private weak var _destination3DTouchTransitionViewController: UIViewController?
fileprivate var _renameAction: UIAlertAction?
private var _renameAction: UIAlertAction?
private var _changingArtworkGame: Game?
required init?(coder aDecoder: NSCoder)
{
self.dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<Game, UIImage>(fetchedResultsController: NSFetchedResultsController())
super.init(coder: aDecoder)
self.prepareDataSource()
}
}
//MARK: - UIViewController -
@ -61,11 +73,8 @@ extension GameCollectionViewController
{
super.viewDidLoad()
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell as! GridCollectionViewCell, for: indexPath)
}
self.collectionView?.dataSource = self.dataSource
self.collectionView?.prefetchDataSource = self.dataSource
self.collectionView?.delegate = self
let layout = self.collectionViewLayout as! GridCollectionViewLayout
@ -122,25 +131,7 @@ extension GameCollectionViewController
saveStatesViewController.game = game
saveStatesViewController.mode = .loading
saveStatesViewController.theme = self.theme
case "gamesDatabaseBrowser":
let game = sender as! Game
let gamesDatabaseBrowserViewController = (segue.destination as! UINavigationController).topViewController as! GamesDatabaseBrowserViewController
gamesDatabaseBrowserViewController.selectionHandler = { (metadata) in
DatabaseManager.shared.performBackgroundTask({ (context) in
let temporaryGame = context.object(with: game.objectID) as! Game
temporaryGame.artworkURL = metadata.artworkURL
context.saveWithErrorLogging()
DispatchQueue.main.async {
gamesDatabaseBrowserViewController.dismiss(animated: true, completion: nil)
}
})
}
case "unwindFromGames":
let destinationViewController = segue.destination as! GameViewController
let cell = sender as! UICollectionViewCell
@ -195,11 +186,42 @@ extension GameCollectionViewController
//MARK: - Private Methods -
private extension GameCollectionViewController
{
//MARK: - Update
//MARK: - Data Source
func prepareDataSource()
{
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell as! GridCollectionViewCell, for: indexPath)
}
self.dataSource.prefetchHandler = { (game, indexPath, completionHandler) in
guard let artworkURL = game.artworkURL else { return nil }
let imageOperation = LoadImageURLOperation(url: artworkURL)
imageOperation.resultHandler = { (image, error) in
completionHandler(image, error)
}
return imageOperation
}
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard let image = image else { return }
let cell = cell as! GridCollectionViewCell
cell.imageView.image = image
cell.isImageViewVibrancyEnabled = false
}
}
func updateDataSource()
{
let fetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "ANY %K == %@", #keyPath(Game.gameCollections), self.gameCollection)
if let gameCollection = self.gameCollection
{
fetchRequest.predicate = NSPredicate(format: "ANY %K == %@", #keyPath(Game.gameCollections), gameCollection)
}
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Game.name), ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
@ -207,7 +229,7 @@ private extension GameCollectionViewController
}
//MARK: - Configure Cells
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath, ignoreImageOperations: Bool = false)
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath)
{
let game = self.dataSource.item(at: indexPath)
@ -222,29 +244,11 @@ private extension GameCollectionViewController
cell.isImageViewVibrancyEnabled = true
}
cell.imageView.image = #imageLiteral(resourceName: "BoxArt")
cell.maximumImageSize = CGSize(width: 90, height: 90)
cell.textLabel.text = game.name
cell.textLabel.textColor = UIColor.gray
if let artworkURL = game.artworkURL, !ignoreImageOperations
{
cell.imageView.sd_setImage(with: artworkURL, placeholderImage: #imageLiteral(resourceName: "BoxArt"), options: .continueInBackground) { (image, error, type, url) in
if let error = error
{
print(error)
}
if image != nil
{
cell.isImageViewVibrancyEnabled = false
}
}
}
else
{
cell.imageView.image = #imageLiteral(resourceName: "BoxArt")
}
}
//MARK: - Emulation
@ -342,7 +346,7 @@ private extension GameCollectionViewController
func rename(_ game: Game, with name: String)
{
guard name.characters.count > 0 else { return }
guard name.count > 0 else { return }
DatabaseManager.shared.performBackgroundTask { (context) in
let game = context.object(with: game.objectID) as! Game
@ -356,7 +360,16 @@ private extension GameCollectionViewController
func changeArtwork(for game: Game)
{
self.performSegue(withIdentifier: "gamesDatabaseBrowser", sender: game)
self._changingArtworkGame = game
let clipboardImportOption = ClipboardImportOption()
let photoLibraryImportOption = PhotoLibraryImportOption(presentingViewController: self)
let gamesDatabaseImportOption = GamesDatabaseImportOption(presentingViewController: self)
let importController = ImportController(documentTypes: [kUTTypeImage as String])
importController.delegate = self
importController.importOptions = [clipboardImportOption, photoLibraryImportOption, gamesDatabaseImportOption]
self.present(importController, animated: true, completion: nil)
}
func share(_ game: Game)
@ -394,7 +407,7 @@ private extension GameCollectionViewController
@objc func textFieldTextDidChange(_ textField: UITextField)
{
let text = textField.text ?? ""
self._renameAction?.isEnabled = text.characters.count > 0
self._renameAction?.isEnabled = text.count > 0
}
@objc func handleLongPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer)
@ -417,7 +430,7 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
{
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
guard self.gameCollection.identifier != GameType.unknown.rawValue else { return nil }
guard self.gameCollection?.identifier != GameType.unknown.rawValue else { return nil }
guard
let collectionView = self.collectionView,
@ -452,7 +465,7 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: game)!
let cell = self.collectionView?.cellForItem(at: indexPath)
let fileURL = FileManager.uniqueTemporaryURL()
let fileURL = FileManager.default.uniqueTemporaryURL()
self.activeSaveState = gameViewController.emulatorCore?.saveSaveState(to: fileURL)
gameViewController.emulatorCore?.stop()
@ -493,13 +506,105 @@ extension GameCollectionViewController: SaveStatesViewControllerDelegate
}
}
//MARK: - ImportControllerDelegate -
/// ImportControllerDelegate
extension GameCollectionViewController: ImportControllerDelegate
{
func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error])
{
guard let game = self._changingArtworkGame else { return }
var errors = errors
var imageURL: URL?
if let url = urls.first
{
if url.isFileURL
{
do
{
let imageData = try Data(contentsOf: url)
if let image = UIImage(data: imageData)
{
let resizedImage = image.resizing(toFit: CGSize(width: 300, height: 300))
if let resizedData = UIImageJPEGRepresentation(resizedImage, 0.85)
{
let destinationURL = DatabaseManager.artworkURL(for: game)
try resizedData.write(to: destinationURL, options: .atomic)
imageURL = destinationURL
}
}
}
catch
{
errors.append(error)
}
}
else
{
imageURL = url
}
}
for error in errors
{
print(error)
}
if let imageURL = imageURL
{
// Remove previous artwork from cache.
self.dataSource.prefetchItemCache.removeObject(forKey: game)
DatabaseManager.shared.performBackgroundTask { (context) in
let temporaryGame = context.object(with: game.objectID) as! Game
temporaryGame.artworkURL = imageURL
context.saveWithErrorLogging()
DispatchQueue.main.async {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
}
else
{
func presentAlertController()
{
let alertController = UIAlertController(title: NSLocalizedString("Unable to Change Artwork", comment: ""), message: NSLocalizedString("The image might be corrupted or in an unsupported format.", comment: ""), preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
if let presentedViewController = self.presentedViewController
{
presentedViewController.dismiss(animated: true) {
presentAlertController()
}
}
else
{
presentAlertController()
}
}
}
func importControllerDidCancel(_ importController: ImportController)
{
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
//MARK: - UICollectionViewDelegate -
/// UICollectionViewDelegate
extension GameCollectionViewController
{
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
guard self.gameCollection.identifier != GameType.unknown.rawValue else { return }
guard self.gameCollection?.identifier != GameType.unknown.rawValue else { return }
let cell = collectionView.cellForItem(at: indexPath)
let game = self.dataSource.item(at: indexPath)
@ -542,12 +647,6 @@ extension GameCollectionViewController
self.launchGame(withSender: cell, clearScreen: true)
}
}
override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
{
let cell = cell as! GridCollectionViewCell
cell.imageView.sd_cancelCurrentImageLoad()
}
}
//MARK: - UICollectionViewDelegateFlowLayout -
@ -562,7 +661,7 @@ extension GameCollectionViewController: UICollectionViewDelegateFlowLayout
widthConstraint.isActive = true
defer { widthConstraint.isActive = false }
self.configure(self.prototypeCell, for: indexPath, ignoreImageOperations: true)
self.configure(self.prototypeCell, for: indexPath)
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
return size

View File

@ -8,6 +8,7 @@
import UIKit
import CoreData
import MobileCoreServices
import DeltaCore
@ -34,11 +35,17 @@ class GamesViewController: UIViewController
}
}
fileprivate var pageViewController: UIPageViewController!
fileprivate var placeholderView: RSTPlaceholderView!
fileprivate var pageControl: UIPageControl!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
fileprivate let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>
private var pageViewController: UIPageViewController!
private var placeholderView: RSTPlaceholderView!
private var pageControl: UIPageControl!
private let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>
private var searchController: RSTSearchController?
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
fatalError("initWithNibName: not implemented")
@ -87,6 +94,11 @@ extension GamesViewController
self.navigationController?.navigationBar.barStyle = .blackTranslucent
self.navigationController?.toolbar.barStyle = .blackTranslucent
if #available(iOS 11.0, *)
{
self.prepareSearchController()
}
self.updateTheme()
}
@ -108,11 +120,15 @@ extension GamesViewController
{
super.viewDidLayoutSubviews()
if let viewControllers = self.pageViewController.viewControllers as? [GameCollectionViewController]
if #available(iOS 11.0, *) {}
else
{
for viewController in viewControllers
if let viewControllers = self.pageViewController.viewControllers as? [GameCollectionViewController]
{
viewController.collectionView?.contentInset.top = self.topLayoutGuide.length
for viewController in viewControllers
{
viewController.collectionView?.contentInset.top = self.topLayoutGuide.length
}
}
}
}
@ -148,6 +164,48 @@ extension GamesViewController
/// UI
private extension GamesViewController
{
@available(iOS 11.0, *)
func prepareSearchController()
{
let searchResultsController = self.storyboard?.instantiateViewController(withIdentifier: "gameCollectionViewController") as! GameCollectionViewController
searchResultsController.gameCollection = nil
searchResultsController.theme = self.theme
searchResultsController.activeEmulatorCore = self.activeEmulatorCore
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.text = NSLocalizedString("No Games Found", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure the name is correct, or try searching for another game.", comment: "")
switch self.theme
{
case .opaque: searchResultsController.dataSource.placeholderView = placeholderView
case .translucent:
let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
vibrancyView.contentView.addSubview(placeholderView, pinningEdgesWith: .zero)
searchResultsController.dataSource.placeholderView = vibrancyView
}
self.searchController = RSTSearchController(searchResultsController: searchResultsController)
self.searchController?.searchableKeyPaths = [#keyPath(Game.name)]
self.searchController?.searchHandler = { [weak searchController, weak searchResultsController] (searchValue, _) in
if searchController?.searchBar.text?.isEmpty == false
{
self.pageViewController.view.isHidden = true
}
else
{
self.pageViewController.view.isHidden = false
}
searchResultsController?.dataSource.predicate = searchValue.predicate
return nil
}
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.definesPresentationContext = true
}
func updateTheme()
{
switch self.theme
@ -185,12 +243,16 @@ private extension GamesViewController
let indexPath = IndexPath(row: safeIndex, section: 0)
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "gameCollectionViewController") as! GameCollectionViewController
viewController.gameCollection = self.fetchedResultsController.object(at: indexPath) as! GameCollection
viewController.gameCollection = self.fetchedResultsController.object(at: indexPath) as? GameCollection
viewController.theme = self.theme
viewController.activeEmulatorCore = self.activeEmulatorCore
// Need to set content inset here AND willTransitionTo callback to ensure its correct for all edge cases
viewController.collectionView?.contentInset.top = self.topLayoutGuide.length
if #available(iOS 11.0, *) {}
else
{
// Need to set content inset here AND willTransitionTo callback to ensure its correct for all edge cases
viewController.collectionView?.contentInset.top = self.topLayoutGuide.length
}
return viewController
}
@ -264,39 +326,57 @@ private extension GamesViewController
/// Importing
extension GamesViewController: ImportControllerDelegate
{
@IBAction fileprivate func importFiles()
@IBAction private func importFiles()
{
let importController = ImportController()
var documentTypes = Set(System.supportedSystems.map { $0.gameType.rawValue })
documentTypes.insert(kUTTypeZipArchive as String)
// Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations)
documentTypes.insert("com.rileytestut.gba")
documentTypes.insert("com.rileytestut.gbc")
documentTypes.insert("com.rileytestut.gb")
let itunesImportOption = iTunesImportOption(presentingViewController: self)
let importController = ImportController(documentTypes: documentTypes)
importController.delegate = self
importController.importOptions = [itunesImportOption]
self.present(importController, animated: true, completion: nil)
}
//MARK: - ImportControllerDelegate
@nonobjc func importController(_ importController: ImportController, didImport games: Set<Game>, with errors: Set<DatabaseManager.ImportError>)
func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error])
{
if errors.count > 0
for error in errors
{
let alertController = UIAlertController.alertController(for: .games, with: errors)
self.present(alertController, animated: true, completion: nil)
print(error)
}
if games.count > 0
{
print("Imported Games:", games.map { $0.name })
}
}
@nonobjc func importController(_ importController: ImportController, didImport controllerSkins: Set<ControllerSkin>, with errors: Set<DatabaseManager.ImportError>)
{
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors)
self.present(alertController, animated: true, completion: nil)
let gameURLs = urls.filter { $0.pathExtension.lowercased() != "deltaskin" }
DatabaseManager.shared.importGames(at: Set(gameURLs)) { (games, errors) in
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .games, with: errors)
self.present(alertController, animated: true, completion: nil)
}
if games.count > 0
{
print("Imported Games:", games.map { $0.name })
}
}
if controllerSkins.count > 0
{
print("Imported Controller Skins:", controllerSkins.map { $0.name })
let controllerSkinURLs = urls.filter { $0.pathExtension.lowercased() == "deltaskin" }
DatabaseManager.shared.importControllerSkins(at: Set(controllerSkinURLs)) { (controllerSkins, errors) in
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors)
self.present(alertController, animated: true, completion: nil)
}
if controllerSkins.count > 0
{
print("Imported Controller Skins:", controllerSkins.map { $0.name })
}
}
}
}
@ -347,9 +427,13 @@ extension GamesViewController: UIPageViewControllerDataSource, UIPageViewControl
{
guard let viewControllers = pendingViewControllers as? [GameCollectionViewController] else { return }
for viewController in viewControllers
if #available(iOS 11.0, *) {}
else
{
viewController.collectionView?.contentInset.top = self.topLayoutGuide.length
for viewController in viewControllers
{
viewController.collectionView?.contentInset.top = self.topLayoutGuide.length
}
}
}
@ -371,6 +455,21 @@ extension GamesViewController: UIPageViewControllerDataSource, UIPageViewControl
}
}
extension GamesViewController: UISearchResultsUpdating
{
func updateSearchResults(for searchController: UISearchController)
{
if searchController.searchBar.text?.isEmpty == false
{
self.pageViewController.view.isHidden = true
}
else
{
self.pageViewController.view.isHidden = false
}
}
}
//MARK: - NSFetchedResultsControllerDelegate -
/// NSFetchedResultsControllerDelegate
extension GamesViewController: NSFetchedResultsControllerDelegate

View File

@ -12,8 +12,12 @@ class GamesPresentationController: UIPresentationController
{
private let blurView: UIVisualEffectView
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?)
private let animator: UIViewPropertyAnimator
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, animator: UIViewPropertyAnimator)
{
self.animator = animator
self.blurView = UIVisualEffectView(effect: nil)
self.blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
@ -26,17 +30,17 @@ class GamesPresentationController: UIPresentationController
self.blurView.frame = CGRect(x: 0, y: 0, width: containerView.bounds.width, height: containerView.bounds.height)
containerView.addSubview(self.blurView)
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.animator.addAnimations {
self.blurView.effect = UIBlurEffect(style: .dark)
})
}
}
override func dismissalTransitionWillBegin()
{
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.animator.addAnimations {
self.blurView.effect = nil
})
}
}
override func dismissalTransitionDidEnd(_ completed: Bool)

View File

@ -10,9 +10,9 @@ import UIKit
class GamesStoryboardSegue: UIStoryboardSegue
{
fileprivate let animator: UIViewPropertyAnimator
private let animator: UIViewPropertyAnimator
fileprivate var isPresenting: Bool = true
private var isPresenting: Bool = true
override init(identifier: String?, source: UIViewController, destination: UIViewController)
{
@ -48,7 +48,7 @@ extension GamesStoryboardSegue: UIViewControllerTransitioningDelegate
func presentationController(forPresented presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, source: UIViewController) -> UIPresentationController?
{
let presentationController = GamesPresentationController(presentedViewController: presentedViewController, presenting: presentingViewController)
let presentationController = GamesPresentationController(presentedViewController: presentedViewController, presenting: presentingViewController, animator: self.animator)
return presentationController
}
}
@ -85,48 +85,51 @@ extension GamesStoryboardSegue: UIViewControllerAnimatedTransitioning
snapshotView.alpha = 1.0
transitionContext.containerView.addSubview(snapshotView)
// Ensures navigation controller toolbar (if visible) has been added to view heirachy, allowing us to add constraints
transitionContext.containerView.layoutIfNeeded()
// We add extra padding around the existing navigation bar and toolbar so they never appear to be detached from the edges of the screen during the overshooting of the spring animation
var topPaddingToolbar: UIToolbar? = nil
var bottomPaddingToolbar: UIToolbar? = nil
if let navigationController = transitionContext.destinationViewController as? UINavigationController
{
let padding: CGFloat = 44
// Must be wrapped in no-animation block to prevent iOS 11 search bar from not appearing.
UIView.performWithoutAnimation {
// Ensures navigation controller toolbar (if visible) has been added to view heirachy, allowing us to add constraints
transitionContext.containerView.layoutIfNeeded()
if !navigationController.isNavigationBarHidden
if let navigationController = transitionContext.destinationViewController as? UINavigationController
{
let topToolbar = UIToolbar(frame: CGRect.zero)
topToolbar.translatesAutoresizingMaskIntoConstraints = false
topToolbar.barStyle = navigationController.toolbar.barStyle
transitionContext.destinationView.insertSubview(topToolbar, belowSubview: navigationController.navigationBar)
let padding: CGFloat = 44
topToolbar.bottomAnchor.constraint(equalTo: navigationController.navigationBar.bottomAnchor).isActive = true
topToolbar.centerXAnchor.constraint(equalTo: navigationController.navigationBar.centerXAnchor).isActive = true
topToolbar.widthAnchor.constraint(equalTo: navigationController.navigationBar.widthAnchor, constant: padding * 2).isActive = true
topToolbar.heightAnchor.constraint(equalTo: navigationController.navigationBar.heightAnchor, constant: padding).isActive = true
if !navigationController.isNavigationBarHidden
{
let topToolbar = UIToolbar(frame: CGRect.zero)
topToolbar.translatesAutoresizingMaskIntoConstraints = false
topToolbar.barStyle = navigationController.toolbar.barStyle
transitionContext.destinationView.insertSubview(topToolbar, at: 1)
topToolbar.topAnchor.constraint(equalTo: navigationController.navigationBar.topAnchor, constant: -padding).isActive = true
topToolbar.bottomAnchor.constraint(equalTo: navigationController.topViewController!.topLayoutGuide.bottomAnchor).isActive = true
topToolbar.leftAnchor.constraint(equalTo: navigationController.navigationBar.leftAnchor, constant: -padding).isActive = true
topToolbar.rightAnchor.constraint(equalTo: navigationController.navigationBar.rightAnchor, constant: padding).isActive = true
topPaddingToolbar = topToolbar
}
topPaddingToolbar = topToolbar
}
if !navigationController.isToolbarHidden
{
let bottomToolbar = UIToolbar(frame: CGRect.zero)
bottomToolbar.translatesAutoresizingMaskIntoConstraints = false
bottomToolbar.barStyle = navigationController.toolbar.barStyle
transitionContext.destinationView.insertSubview(bottomToolbar, belowSubview: navigationController.navigationBar)
bottomToolbar.topAnchor.constraint(equalTo: navigationController.toolbar.topAnchor).isActive = true
bottomToolbar.centerXAnchor.constraint(equalTo: navigationController.toolbar.centerXAnchor).isActive = true
bottomToolbar.widthAnchor.constraint(equalTo: navigationController.toolbar.widthAnchor, constant: padding * 2).isActive = true
bottomToolbar.heightAnchor.constraint(equalTo: navigationController.toolbar.heightAnchor, constant: padding).isActive = true
bottomPaddingToolbar = bottomToolbar
if !navigationController.isToolbarHidden
{
let bottomToolbar = UIToolbar(frame: CGRect.zero)
bottomToolbar.translatesAutoresizingMaskIntoConstraints = false
bottomToolbar.barStyle = navigationController.toolbar.barStyle
transitionContext.destinationView.insertSubview(bottomToolbar, belowSubview: navigationController.navigationBar)
bottomToolbar.topAnchor.constraint(equalTo: navigationController.toolbar.topAnchor).isActive = true
bottomToolbar.bottomAnchor.constraint(equalTo: navigationController.toolbar.bottomAnchor, constant: padding).isActive = true
bottomToolbar.leftAnchor.constraint(equalTo: navigationController.toolbar.leftAnchor, constant: -padding).isActive = true
bottomToolbar.rightAnchor.constraint(equalTo: navigationController.toolbar.rightAnchor, constant: padding).isActive = true
bottomPaddingToolbar = bottomToolbar
}
}
}
self.animator.addAnimations {
snapshotView.alpha = 0.0
transitionContext.destinationView.transform = CGAffineTransform.identity
@ -163,3 +166,4 @@ extension GamesStoryboardSegue: UIViewControllerAnimatedTransitioning
self.animator.startAnimation()
}
}

View File

@ -10,9 +10,9 @@ import UIKit
class InitialGamesStoryboardSegue: UIStoryboardSegue
{
fileprivate let animator: UIViewPropertyAnimator
private let animator: UIViewPropertyAnimator
fileprivate var isPresenting: Bool = true
private var isPresenting: Bool = true
override init(identifier: String?, source: UIViewController, destination: UIViewController)
{

View File

@ -0,0 +1,39 @@
//
// ClipboardImportOption.swift
// Delta
//
// Created by Riley Testut on 5/1/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import MobileCoreServices
import Roxas
struct ClipboardImportOption: ImportOption
{
let title = NSLocalizedString("Clipboard", comment: "")
let image: UIImage? = nil
func `import`(withCompletionHandler completionHandler: @escaping (Set<URL>?) -> Void)
{
guard UIPasteboard.general.hasImages else { return completionHandler([]) }
guard let data = UIPasteboard.general.data(forPasteboardType: kUTTypeImage as String) else { return completionHandler([]) }
do
{
let temporaryURL = FileManager.default.uniqueTemporaryURL()
try data.write(to: temporaryURL, options: .atomic)
completionHandler([temporaryURL])
}
catch
{
print(error)
completionHandler([])
}
}
}

View File

@ -0,0 +1,42 @@
//
// GamesDatabaseImportOption.swift
// Delta
//
// Created by Riley Testut on 5/1/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
struct GamesDatabaseImportOption: ImportOption
{
let title = NSLocalizedString("Games Database", comment: "")
let image: UIImage? = nil
private let presentingViewController: UIViewController
init(presentingViewController: UIViewController)
{
self.presentingViewController = presentingViewController
}
func `import`(withCompletionHandler completionHandler: @escaping (Set<URL>?) -> Void)
{
let storyboard = UIStoryboard(name: "GamesDatabase", bundle: nil)
let navigationController = (storyboard.instantiateInitialViewController() as! UINavigationController)
let gamesDatabaseBrowserViewController = navigationController.topViewController as! GamesDatabaseBrowserViewController
gamesDatabaseBrowserViewController.selectionHandler = { (metadata) in
if let artworkURL = metadata.artworkURL
{
completionHandler([artworkURL])
}
else
{
completionHandler(nil)
}
}
self.presentingViewController.present(navigationController, animated: true, completion: nil)
}
}

View File

@ -0,0 +1,61 @@
//
// PhotoLibraryImportOption.swift
// Delta
//
// Created by Riley Testut on 5/2/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import MobileCoreServices
class PhotoLibraryImportOption: NSObject, ImportOption
{
let title = NSLocalizedString("Photo Library", comment: "")
let image: UIImage? = nil
private let presentingViewController: UIViewController
private var completionHandler: ((Set<URL>?) -> Void)?
init(presentingViewController: UIViewController)
{
self.presentingViewController = presentingViewController
super.init()
}
func `import`(withCompletionHandler completionHandler: @escaping (Set<URL>?) -> Void)
{
self.completionHandler = completionHandler
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = self
imagePickerController.sourceType = .photoLibrary
imagePickerController.mediaTypes = [kUTTypeImage as String]
imagePickerController.view.backgroundColor = .white
self.presentingViewController.present(imagePickerController, animated: true, completion: nil)
}
}
extension PhotoLibraryImportOption: UIImagePickerControllerDelegate, UINavigationControllerDelegate
{
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])
{
guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage, let data = UIImageJPEGRepresentation(image, 0.85) else {
self.completionHandler?([])
return
}
do
{
let temporaryURL = FileManager.default.uniqueTemporaryURL()
try data.write(to: temporaryURL, options: .atomic)
self.completionHandler?([temporaryURL])
}
catch
{
self.completionHandler?([])
}
}
}

View File

@ -0,0 +1,73 @@
//
// iTunesImportOption.swift
// Delta
//
// Created by Riley Testut on 5/1/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import DeltaCore
struct iTunesImportOption: ImportOption
{
let title = NSLocalizedString("iTunes", comment: "")
let image: UIImage? = nil
private let presentingViewController: UIViewController
init(presentingViewController: UIViewController)
{
self.presentingViewController = presentingViewController
}
func `import`(withCompletionHandler completionHandler: @escaping (Set<URL>?) -> Void)
{
let alertController = UIAlertController(title: NSLocalizedString("Import from iTunes?", comment: ""), message: NSLocalizedString("Delta will import the games and controller skins copied over via iTunes.", comment: ""), preferredStyle: .alert)
let importAction = UIAlertAction(title: NSLocalizedString("Import", comment: ""), style: .default) { action in
var importedURLs = Set<URL>()
let documentsDirectoryURL = DatabaseManager.defaultDirectoryURL().deletingLastPathComponent()
do
{
let contents = try FileManager.default.contentsOfDirectory(at: documentsDirectoryURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
let itemURLs = contents.filter { GameType(fileExtension: $0.pathExtension) != nil || $0.pathExtension.lowercased() == "zip" || $0.pathExtension.lowercased() == "deltaskin" }
for url in itemURLs
{
let destinationURL = FileManager.default.uniqueTemporaryURL().appendingPathExtension(url.pathExtension)
do
{
try FileManager.default.moveItem(at: url, to: destinationURL)
importedURLs.insert(destinationURL)
}
catch
{
print("Error importing file at URL", url, error)
}
}
}
catch
{
print(error)
}
completionHandler(importedURLs)
}
alertController.addAction(importAction)
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { action in
completionHandler(nil)
}
alertController.addAction(cancelAction)
self.presentingViewController.present(alertController, animated: true, completion: nil)
}
}

View File

@ -0,0 +1,250 @@
//
// ImportController.swift
// Delta
//
// Created by Riley Testut on 10/10/15.
// Copyright © 2015 Riley Testut. All rights reserved.
//
import UIKit
import MobileCoreServices
import ObjectiveC
import DeltaCore
import Roxas
protocol ImportControllerDelegate
{
func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error])
/** Optional **/
func importControllerDidCancel(_ importController: ImportController)
}
extension ImportControllerDelegate
{
func importControllerDidCancel(_ importController: ImportController)
{
// Empty Implementation
}
}
class ImportController: NSObject
{
let documentTypes: Set<String>
var delegate: ImportControllerDelegate?
var importOptions: [ImportOption]?
private weak var presentingViewController: UIViewController?
// Store presentedViewController separately, since when we dismiss we don't know if it has already been dismissed.
// Calling dismiss on presentingViewController in that case would dismiss presentingViewController, which is bad.
private weak var presentedViewController: UIViewController?
private let importQueue: OperationQueue
private let fileCoordinator: NSFileCoordinator
init(documentTypes: Set<String>)
{
self.documentTypes = documentTypes
let dispatchQueue = DispatchQueue(label: "com.rileytestut.Delta.ImportController.dispatchQueue", qos: .userInitiated, attributes: .concurrent)
self.importQueue = OperationQueue()
self.importQueue.name = "com.rileytestut.Delta.ImportController.importQueue"
self.importQueue.underlyingQueue = dispatchQueue
self.fileCoordinator = NSFileCoordinator(filePresenter: nil)
super.init()
}
fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?)
{
self.presentingViewController = presentingViewController
#if IMPACTOR
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction.cancel)
if let importOptions = self.importOptions
{
for importOption in importOptions
{
alertController.add(importOption) { [unowned self] (urls) in
self.finish(with: urls, errors: [])
}
}
}
self.presentedViewController = alertController
self.presentingViewController?.present(alertController, animated: true, completion: nil)
#else
let documentMenuController = UIDocumentMenuViewController(documentTypes: Array(self.documentTypes), in: .import)
documentMenuController.delegate = self
if let reversedImportOptions = self.importOptions?.reversed()
{
for importOption in reversedImportOptions
{
documentMenuController.add(importOption, order: .first) { [unowned self] (urls) in
self.finish(with: urls, errors: [])
}
}
}
self.presentedViewController = documentMenuController
self.presentingViewController?.present(documentMenuController, animated: true, completion: nil)
#endif
}
@objc private func cancel()
{
self.finish(with: nil, errors: [])
}
private func finish(with urls: Set<URL>?, errors: [Error])
{
if let urls = urls
{
self.delegate?.importController(self, didImportItemsAt: urls, errors: errors)
}
else
{
self.delegate?.importControllerDidCancel(self)
}
self.presentedViewController?.dismiss(animated: true)
self.presentingViewController?.importController = nil
}
}
extension ImportController: UIDocumentMenuDelegate
{
func documentMenu(_ documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController)
{
if #available(iOS 11.0, *)
{
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(ImportController.cancel))
let documentBrowserViewController = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes: Array(self.documentTypes))
documentBrowserViewController.delegate = self
documentBrowserViewController.browserUserInterfaceStyle = .dark
documentBrowserViewController.allowsPickingMultipleItems = true
documentBrowserViewController.allowsDocumentCreation = false
documentBrowserViewController.additionalTrailingNavigationBarButtonItems = [cancelButton]
self.presentedViewController = documentBrowserViewController
self.presentingViewController?.present(documentBrowserViewController, animated: true, completion: nil)
}
else
{
documentPicker.delegate = self
self.presentedViewController = documentPicker
self.presentingViewController?.present(documentPicker, animated: true, completion: nil)
}
}
func documentMenuWasCancelled(_ documentMenu: UIDocumentMenuViewController)
{
self.finish(with: nil, errors: [])
}
}
extension ImportController: UIDocumentPickerDelegate
{
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL)
{
self.finish(with: [url], errors: [])
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL])
{
self.finish(with: Set(urls), errors: [])
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController)
{
self.finish(with: nil, errors: [])
}
}
@available(iOS 11.0, *)
extension ImportController: UIDocumentBrowserViewControllerDelegate
{
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL])
{
var coordinatedURLs = Set<URL>()
var errors = [Error]()
let dispatchGroup = DispatchGroup()
for url in documentURLs
{
dispatchGroup.enter()
let intent = NSFileAccessIntent.readingIntent(with: url)
self.fileCoordinator.coordinate(with: [intent], queue: self.importQueue) { (error) in
if let error = error
{
errors.append(error)
}
else
{
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
do
{
// Always access intent.url, as the system may have updated it when requesting access.
try FileManager.default.copyItem(at: intent.url, to: temporaryURL)
coordinatedURLs.insert(temporaryURL)
}
catch
{
errors.append(error)
}
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: self.importQueue.underlyingQueue!) {
self.finish(with: coordinatedURLs, errors: errors)
}
}
}
private var ImportControllerKey: UInt8 = 0
extension UIViewController
{
fileprivate(set) var importController: ImportController?
{
set
{
objc_setAssociatedObject(self, &ImportControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get
{
return objc_getAssociatedObject(self, &ImportControllerKey) as? ImportController
}
}
func present(_ importController: ImportController, animated: Bool, completion: (() -> Void)?)
{
self.importController = importController
importController.presentImportController(from: self, animated: animated, completionHandler: completion)
}
}

View File

@ -0,0 +1,40 @@
//
// ImportOption.swift
// Delta
//
// Created by Riley Testut on 5/1/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import DeltaCore
extension UIDocumentMenuViewController
{
func add(_ importOption: ImportOption, order: UIDocumentMenuOrder, completionHandler: @escaping (Set<URL>?) -> Void)
{
self.addOption(withTitle: importOption.title, image: importOption.image, order: order) {
importOption.import(withCompletionHandler: completionHandler)
}
}
}
extension UIAlertController
{
func add(_ importOption: ImportOption, completionHandler: @escaping (Set<URL>?) -> Void)
{
let action = UIAlertAction(title: importOption.title, style: .default, handler: { action in
importOption.import(withCompletionHandler: completionHandler)
})
self.addAction(action)
}
}
protocol ImportOption
{
var title: String { get }
var image: UIImage? { get }
func `import`(withCompletionHandler completionHandler: @escaping (Set<URL>?) -> Void)
}

View File

@ -10,10 +10,10 @@ import UIKit
class LaunchViewController: UIViewController
{
@IBOutlet fileprivate var gameViewContainerView: UIView!
fileprivate var gameViewController: GameViewController!
@IBOutlet private var gameViewContainerView: UIView!
private var gameViewController: GameViewController!
fileprivate var presentedGameViewController: Bool = false
private var presentedGameViewController: Bool = false
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.gameViewController?.preferredStatusBarStyle ?? .lightContent

View File

@ -13,7 +13,10 @@ import DeltaCore
import Roxas
private let CheatPrefixAttribute = "prefix"
private extension NSAttributedStringKey
{
static let cheatPrefix = NSAttributedStringKey("CheatPrefix")
}
class CheatTextView: UITextView
{
@ -23,7 +26,7 @@ class CheatTextView: UITextView
}
}
@NSCopying fileprivate var attributedFormat: NSAttributedString?
@NSCopying private var attributedFormat: NSAttributedString?
required init?(coder aDecoder: NSCoder)
{
@ -44,9 +47,9 @@ extension CheatTextView
if let format = self.cheatFormat, let font = self.font
{
let characterWidth = ("A" as NSString).size(attributes: [NSFontAttributeName: font]).width
let characterWidth = ("A" as NSString).size(withAttributes: [.font: font]).width
let width = characterWidth * CGFloat(format.format.characters.count)
let width = characterWidth * CGFloat(format.format.count)
self.textContainer.size = CGSize(width: width, height: 0)
}
}
@ -79,7 +82,7 @@ private extension CheatTextView
if let prefixString = prefixString, prefixString.length > 0
{
attributedString.addAttribute(CheatPrefixAttribute, value: prefixString, range: NSRange(location: 0, length: 1))
attributedString.addAttribute(.cheatPrefix, value: prefixString, range: NSRange(location: 0, length: 1))
}
attributedFormat.append(attributedString)
@ -105,7 +108,7 @@ private extension CheatTextView
extension CheatTextView: NSLayoutManagerDelegate
{
func layoutManager(_ layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSGlyphProperty>, characterIndexes charIndexes: UnsafePointer<Int>, font aFont: UIFont, forGlyphRange glyphRange: NSRange) -> Int
func layoutManager(_ layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes charIndexes: UnsafePointer<Int>, font aFont: UIFont, forGlyphRange glyphRange: NSRange) -> Int
{
// Returning 0 = let the layoutManager do the normal logic
guard let attributedFormat = self.attributedFormat else { return 0 }
@ -118,7 +121,7 @@ extension CheatTextView: NSLayoutManagerDelegate
// Allocate our replacement buffers
let glyphBuffer = UnsafeMutablePointer<CGGlyph>.allocate(capacity: bufferSize)
let propertyBuffer = UnsafeMutablePointer<NSGlyphProperty>.allocate(capacity: bufferSize)
let propertyBuffer = UnsafeMutablePointer<NSLayoutManager.GlyphProperty>.allocate(capacity: bufferSize)
let characterBuffer = UnsafeMutablePointer<Int>.allocate(capacity: bufferSize)
var offset = 0
@ -128,10 +131,10 @@ extension CheatTextView: NSLayoutManagerDelegate
// The index the actual character maps to in the cheat format
let characterIndex = charIndexes[i] % attributedFormat.length
if let prefix = attributedFormat.attributes(at: characterIndex, effectiveRange: nil)[CheatPrefixAttribute] as? String
if let prefix = attributedFormat.attributes(at: characterIndex, effectiveRange: nil)[.cheatPrefix] as? String
{
// If there is a prefix string, we insert the glyphs (and associated properties/character indexes) first
let prefixCount = prefix.characters.count
let prefixCount = prefix.count
for j in 0 ..< prefixCount
{

View File

@ -51,7 +51,7 @@ struct CheatValidator
// Remove newline characters (code should already be formatted)
let sanitizedCode = (cheat.code as NSString).replacingOccurrences(of: "\n", with: "")
if sanitizedCode.characters.count % self.format.format.characters.count != 0
if sanitizedCode.count % self.format.format.count != 0
{
throw Error.invalidCode
}

View File

@ -29,7 +29,7 @@ class CheatsViewController: UITableViewController
weak var delegate: CheatsViewControllerDelegate?
fileprivate let dataSource = RSTFetchedResultsTableViewDataSource<Cheat>(fetchedResultsController: NSFetchedResultsController())
private let dataSource = RSTFetchedResultsTableViewDataSource<Cheat>(fetchedResultsController: NSFetchedResultsController())
}
extension CheatsViewController

View File

@ -33,7 +33,7 @@ class EditCheatViewController: UITableViewController
var game: Game! {
didSet {
let deltaCore = Delta.core(for: self.game.type)!
self.supportedCheatFormats = deltaCore.emulatorConfiguration.supportedCheatFormats
self.supportedCheatFormats = deltaCore.supportedCheatFormats.sorted() { $0.name < $1.name }
}
}
@ -43,19 +43,19 @@ class EditCheatViewController: UITableViewController
var isPreviewing = false
fileprivate var supportedCheatFormats: [CheatFormat]!
private var supportedCheatFormats: [CheatFormat]!
fileprivate var selectedCheatFormat: CheatFormat {
private var selectedCheatFormat: CheatFormat {
let cheatFormat = self.supportedCheatFormats[self.typeSegmentedControl.selectedSegmentIndex]
return cheatFormat
}
fileprivate var mutableCheat: Cheat!
fileprivate var managedObjectContext = DatabaseManager.shared.newBackgroundContext()
private var mutableCheat: Cheat!
private var managedObjectContext = DatabaseManager.shared.newBackgroundContext()
@IBOutlet fileprivate var nameTextField: UITextField!
@IBOutlet fileprivate var typeSegmentedControl: UISegmentedControl!
@IBOutlet fileprivate var codeTextView: CheatTextView!
@IBOutlet private var nameTextField: UITextField!
@IBOutlet private var typeSegmentedControl: UISegmentedControl!
@IBOutlet private var codeTextView: CheatTextView!
override var previewActionItems: [UIPreviewActionItem]
{
@ -132,7 +132,7 @@ extension EditCheatViewController
// Update UI
if name.characters.count == 0
if name.count == 0
{
self.title = NSLocalizedString("Cheat", comment: "")
}
@ -224,7 +224,7 @@ private extension EditCheatViewController
@IBAction func updateCheatName(_ sender: UITextField)
{
var title = sender.text ?? ""
if title.characters.count == 0
if title.count == 0
{
title = NSLocalizedString("Cheat", comment: "")
}
@ -325,7 +325,7 @@ private extension EditCheatViewController
sender.resignFirstResponder()
}
func presentErrorAlert(title: String, message: String, handler: ((Void) -> Void)?)
func presentErrorAlert(title: String, message: String, handler: (() -> Void)?)
{
DispatchQueue.main.async {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
@ -382,7 +382,9 @@ extension EditCheatViewController: UITextViewDelegate
// We need to manually add back the attributes when manually modifying the underlying text storage
// Otherwise, pasting text into an empty text view will result in the wrong font being used
let attributedString = NSAttributedString(string: sanitizedText, attributes: textView.typingAttributes)
let attributes = Dictionary(uniqueKeysWithValues: textView.typingAttributes.map { (key, value) in (NSAttributedStringKey(key), value) })
let attributedString = NSAttributedString(string: sanitizedText, attributes: attributes)
textView.textStorage.replaceCharacters(in: range, with: attributedString)
// We must add attributedString.length, not range.length, in case the attributed string's length differs

View File

@ -0,0 +1,176 @@
//
// GridMenuViewController.swift
// Delta
//
// Created by Riley Testut on 12/21/15.
// Copyright © 2015 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class GridMenuViewController: UICollectionViewController
{
var items: [MenuItem] {
get { return self.dataSource.items }
set { self.dataSource.items = newValue; self.updateItems() }
}
var isVibrancyEnabled = true
override var preferredContentSize: CGSize {
set { }
get { return self.collectionView?.contentSize ?? CGSize.zero }
}
private let dataSource = RSTArrayCollectionViewDataSource<MenuItem>(items: [])
private var prototypeCell = GridCollectionViewCell()
private var previousIndexPath: IndexPath? = nil
private var registeredKVOObservers = Set<NSKeyValueObservation>()
init()
{
let collectionViewLayout = GridCollectionViewLayout()
collectionViewLayout.itemSize = CGSize(width: 60, height: 80)
collectionViewLayout.minimumLineSpacing = 20
collectionViewLayout.minimumInteritemSpacing = 10
super.init(collectionViewLayout: collectionViewLayout)
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
deinit
{
// Crashes on iOS 10 if not explicitly invalidated.
self.registeredKVOObservers.forEach { $0.invalidate() }
}
}
extension GridMenuViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView?.register(GridCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell as! GridCollectionViewCell, for: indexPath)
}
self.collectionView?.dataSource = self.dataSource
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
collectionViewLayout.itemWidth = 90
collectionViewLayout.usesEqualHorizontalSpacingDistributionForSingleRow = true
// Manually update prototype cell properties
self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth).isActive = true
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
if let indexPath = self.previousIndexPath
{
UIView.animate(withDuration: 0.2) {
let item = self.items[indexPath.item]
item.isSelected = !item.isSelected
}
}
}
}
private extension GridMenuViewController
{
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath)
{
let pauseItem = self.items[indexPath.item]
cell.maximumImageSize = CGSize(width: 60, height: 60)
cell.imageView.image = pauseItem.image
cell.imageView.contentMode = .center
cell.imageView.layer.borderWidth = 2
cell.imageView.layer.borderColor = self.view.tintColor.cgColor
cell.imageView.layer.cornerRadius = 10
cell.textLabel.text = pauseItem.text
cell.textLabel.textColor = self.view.tintColor
if pauseItem.isSelected
{
cell.imageView.tintColor = UIColor.black
cell.imageView.backgroundColor = self.view.tintColor
}
else
{
cell.imageView.tintColor = self.view.tintColor
cell.imageView.backgroundColor = UIColor.clear
}
cell.isImageViewVibrancyEnabled = self.isVibrancyEnabled
cell.isTextLabelVibrancyEnabled = self.isVibrancyEnabled
}
func updateItems()
{
self.registeredKVOObservers.removeAll()
for (index, item) in self.items.enumerated()
{
let observer = item.observe(\.isSelected, changeHandler: { [unowned self] (item, change) in
let indexPath = IndexPath(item: index, section: 0)
if let cell = self.collectionView?.cellForItem(at: indexPath) as? GridCollectionViewCell
{
self.configure(cell, for: indexPath)
}
})
self.registeredKVOObservers.insert(observer)
}
}
}
extension GridMenuViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
self.configure(self.prototypeCell, for: indexPath)
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
return size
}
}
extension GridMenuViewController
{
override func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath)
{
let item = self.items[indexPath.item]
item.isSelected = !item.isSelected
}
override func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath)
{
let item = self.items[indexPath.item]
item.isSelected = !item.isSelected
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
self.previousIndexPath = indexPath
let item = self.items[indexPath.item]
item.isSelected = !item.isSelected
item.action(item)
}
}

View File

@ -0,0 +1,35 @@
//
// MenuItem.swift
// Delta
//
// Created by Riley Testut on 1/30/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import UIKit
// Must be class for use with Objective-C generics :(
class MenuItem: NSObject
{
var text: String
var image: UIImage?
var action: ((MenuItem) -> Void)
@objc dynamic var isSelected = false
init(text: String, image: UIImage?, action: @escaping ((MenuItem) -> Void))
{
self.image = image
self.text = text
self.action = action
}
}
extension MenuItem
{
override func isEqual(_ object: Any?) -> Bool
{
guard let item = object as? MenuItem else { return false }
return item.image == self.image && item.text == self.text
}
}

View File

@ -1,31 +0,0 @@
//
// PauseItem.swift
// Delta
//
// Created by Riley Testut on 1/30/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import UIKit
// Must be class for use with Objective-C generics :(
class PauseItem: Equatable
{
var image: UIImage
var text: String
var action: ((PauseItem) -> Void)
var selected = false
init(image: UIImage, text: String, action: @escaping ((PauseItem) -> Void))
{
self.image = image
self.text = text
self.action = action
}
}
func ==(lhs: PauseItem, rhs: PauseItem) -> Bool
{
return (lhs.image == rhs.image) && (lhs.text == rhs.text)
}

View File

@ -1,146 +0,0 @@
//
// PauseMenuViewController.swift
// Delta
//
// Created by Riley Testut on 12/21/15.
// Copyright © 2015 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class PauseMenuViewController: UICollectionViewController
{
var items = [PauseItem]() {
didSet
{
guard oldValue != self.items else { return }
if self.items.count > 8
{
fatalError("PauseViewController only supports up to 8 items (for my sanity when laying out on a landscape iPhone 4s")
}
self.dataSource.items = self.items
}
}
override var preferredContentSize: CGSize {
set { }
get { return self.collectionView?.contentSize ?? CGSize.zero }
}
fileprivate let dataSource = RSTArrayCollectionViewDataSource<PauseItem>(items: [])
fileprivate var prototypeCell = GridCollectionViewCell()
fileprivate var previousIndexPath: IndexPath? = nil
}
extension PauseMenuViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell as! GridCollectionViewCell, for: indexPath)
}
self.collectionView?.dataSource = self.dataSource
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
collectionViewLayout.itemWidth = 90
collectionViewLayout.usesEqualHorizontalSpacingDistributionForSingleRow = true
// Manually update prototype cell properties
self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth).isActive = true
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
if let indexPath = self.previousIndexPath
{
UIView.animate(withDuration: 0.2) {
self.toggleSelectedStateForPauseItemAtIndexPath(indexPath)
}
}
}
}
private extension PauseMenuViewController
{
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath)
{
let pauseItem = self.items[(indexPath as NSIndexPath).item]
cell.maximumImageSize = CGSize(width: 60, height: 60)
cell.imageView.image = pauseItem.image
cell.imageView.contentMode = .center
cell.imageView.layer.borderWidth = 2
cell.imageView.layer.borderColor = UIColor.white.cgColor
cell.imageView.layer.cornerRadius = 10
cell.textLabel.text = pauseItem.text
cell.textLabel.textColor = UIColor.white
if pauseItem.selected
{
cell.imageView.tintColor = UIColor.black
cell.imageView.backgroundColor = UIColor.white
}
else
{
cell.imageView.tintColor = UIColor.white
cell.imageView.backgroundColor = UIColor.clear
}
cell.isImageViewVibrancyEnabled = true
cell.isTextLabelVibrancyEnabled = true
}
func toggleSelectedStateForPauseItemAtIndexPath(_ indexPath: IndexPath)
{
let pauseItem = self.items[indexPath.item]
pauseItem.selected = !pauseItem.selected
let cell = self.collectionView!.cellForItem(at: indexPath) as! GridCollectionViewCell
self.configure(cell, for: indexPath)
}
}
extension PauseMenuViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
self.configure(self.prototypeCell, for: indexPath)
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
return size
}
}
extension PauseMenuViewController
{
override func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath)
{
self.toggleSelectedStateForPauseItemAtIndexPath(indexPath)
}
override func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath)
{
self.toggleSelectedStateForPauseItemAtIndexPath(indexPath)
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
self.previousIndexPath = indexPath
self.toggleSelectedStateForPauseItemAtIndexPath(indexPath)
let pauseItem = self.items[indexPath.item]
pauseItem.action(pauseItem)
}
}

View File

@ -18,16 +18,16 @@ class PauseViewController: UIViewController, PauseInfoProviding
}
}
var pauseItems: [PauseItem] {
var pauseItems: [MenuItem] {
return [self.saveStateItem, self.loadStateItem, self.cheatCodesItem, self.fastForwardItem, self.sustainButtonsItem].flatMap { $0 }
}
/// Pause Items
var saveStateItem: PauseItem?
var loadStateItem: PauseItem?
var cheatCodesItem: PauseItem?
var fastForwardItem: PauseItem?
var sustainButtonsItem: PauseItem?
var saveStateItem: MenuItem?
var loadStateItem: MenuItem?
var cheatCodesItem: MenuItem?
var fastForwardItem: MenuItem?
var sustainButtonsItem: MenuItem?
/// PauseInfoProviding
var pauseText: String?
@ -38,9 +38,9 @@ class PauseViewController: UIViewController, PauseInfoProviding
/// Save States
weak var saveStatesViewControllerDelegate: SaveStatesViewControllerDelegate?
fileprivate var saveStatesViewControllerMode = SaveStatesViewController.Mode.loading
private var saveStatesViewControllerMode = SaveStatesViewController.Mode.loading
fileprivate var pauseNavigationController: UINavigationController!
private var pauseNavigationController: UINavigationController!
/// UIViewController
override var preferredContentSize: CGSize {
@ -85,8 +85,8 @@ extension PauseViewController
self.pauseNavigationController.navigationBar.tintColor = UIColor.deltaPurple
self.pauseNavigationController.view.backgroundColor = UIColor.clear
let pauseMenuViewController = self.pauseNavigationController.topViewController as! PauseMenuViewController
pauseMenuViewController.items = self.pauseItems
let gridMenuViewController = self.pauseNavigationController.topViewController as! GridMenuViewController
gridMenuViewController.items = self.pauseItems
case "saveStates":
let saveStatesViewController = segue.destination as! SaveStatesViewController
@ -135,21 +135,21 @@ private extension PauseViewController
guard self.emulatorCore != nil else { return }
self.saveStateItem = PauseItem(image: #imageLiteral(resourceName: "SaveSaveState"), text: NSLocalizedString("Save State", comment: ""), action: { [unowned self] _ in
self.saveStateItem = MenuItem(text: NSLocalizedString("Save State", comment: ""), image: #imageLiteral(resourceName: "SaveSaveState"), action: { [unowned self] _ in
self.saveStatesViewControllerMode = .saving
self.performSegue(withIdentifier: "saveStates", sender: self)
})
self.loadStateItem = PauseItem(image: #imageLiteral(resourceName: "LoadSaveState"), text: NSLocalizedString("Load State", comment: ""), action: { [unowned self] _ in
self.loadStateItem = MenuItem(text: NSLocalizedString("Load State", comment: ""), image: #imageLiteral(resourceName: "LoadSaveState"), action: { [unowned self] _ in
self.saveStatesViewControllerMode = .loading
self.performSegue(withIdentifier: "saveStates", sender: self)
})
self.cheatCodesItem = PauseItem(image: #imageLiteral(resourceName: "CheatCodes"), text: NSLocalizedString("Cheat Codes", comment: ""), action: { [unowned self] _ in
self.cheatCodesItem = MenuItem(text: NSLocalizedString("Cheat Codes", comment: ""), image: #imageLiteral(resourceName: "CheatCodes"), action: { [unowned self] _ in
self.performSegue(withIdentifier: "cheats", sender: self)
})
self.fastForwardItem = PauseItem(image: #imageLiteral(resourceName: "FastForward"), text: NSLocalizedString("Fast Forward", comment: ""), action: { _ in })
self.sustainButtonsItem = PauseItem(image: #imageLiteral(resourceName: "SustainButtons"), text: NSLocalizedString("Sustain Buttons", comment: ""), action: { _ in })
self.fastForwardItem = MenuItem(text: NSLocalizedString("Fast Forward", comment: ""), image: #imageLiteral(resourceName: "FastForward"), action: { _ in })
self.sustainButtonsItem = MenuItem(text: NSLocalizedString("Sustain Buttons", comment: ""), image: #imageLiteral(resourceName: "SustainButtons"), action: { _ in })
}
}

View File

@ -17,13 +17,17 @@ protocol PauseInfoProviding
class PausePresentationController: UIPresentationController
{
let presentationAnimator: UIViewPropertyAnimator
private let blurringView: UIVisualEffectView
private let vibrancyView: UIVisualEffectView
private var contentView: UIView!
@IBOutlet private weak var pauseLabel: UILabel!
@IBOutlet private weak var pauseIconImageView: UIImageView!
@IBOutlet private weak var stackView: UIStackView!
// Must not be weak, or else may result in crash when deallocating.
@IBOutlet private var pauseLabel: UILabel!
@IBOutlet private var pauseIconImageView: UIImageView!
@IBOutlet private var stackView: UIStackView!
override var frameOfPresentedViewInContainerView: CGRect
{
@ -45,8 +49,10 @@ class PausePresentationController: UIPresentationController
return frame
}
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?)
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, presentationAnimator: UIViewPropertyAnimator)
{
self.presentationAnimator = presentationAnimator
self.blurringView = UIVisualEffectView(effect: nil)
self.vibrancyView = UIVisualEffectView(effect: nil)
@ -83,16 +89,18 @@ class PausePresentationController: UIPresentationController
self.contentView.alpha = 0.0
self.vibrancyView.contentView.addSubview(self.contentView)
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { context in
self.presentationAnimator.addAnimations {
let blurEffect = UIBlurEffect(style: .dark)
self.blurringView.effect = blurEffect
self.vibrancyView.effect = UIVibrancyEffect(blurEffect: blurEffect)
self.contentView.alpha = 1.0
}, completion: nil)
}
// I have absolutely no clue why animating with transition coordinator results in no animation on iOS 11.
// Spent far too long trying to fix it, so just use the presentation animator.
// self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { context in }, completion: nil)
}
override func dismissalTransitionWillBegin()

View File

@ -12,7 +12,7 @@ class SaveStatesCollectionHeaderView: UICollectionReusableView
{
let textLabel = UILabel()
fileprivate let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
private let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
var isTextLabelVibrancyEnabled = true {
didSet {

View File

@ -29,6 +29,7 @@ extension SaveStatesViewController
enum Section: Int
{
case auto
case quick
case general
case locked
}
@ -58,29 +59,30 @@ class SaveStatesViewController: UICollectionViewController
}
}
fileprivate var vibrancyView: UIVisualEffectView!
fileprivate var placeholderView: RSTPlaceholderView!
private var vibrancyView: UIVisualEffectView!
private var placeholderView: RSTPlaceholderView!
fileprivate var prototypeCell = GridCollectionViewCell()
fileprivate var prototypeCellWidthConstraint: NSLayoutConstraint!
fileprivate var prototypeHeader = SaveStatesCollectionHeaderView()
private var prototypeCell = GridCollectionViewCell()
private var prototypeCellWidthConstraint: NSLayoutConstraint!
private var prototypeHeader = SaveStatesCollectionHeaderView()
fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource<SaveState>(fetchedResultsController: NSFetchedResultsController())
private let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource<SaveState, UIImage>
fileprivate let imageOperationQueue = RSTOperationQueue()
fileprivate let imageCache = NSCache<NSURL, UIImage>()
private var emulatorCoreSaveState: SaveStateProtocol?
fileprivate var emulatorCoreSaveState: SaveStateProtocol?
fileprivate let dateFormatter: DateFormatter
private let dateFormatter: DateFormatter
required init?(coder aDecoder: NSCoder)
{
self.dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<SaveState, UIImage>(fetchedResultsController: NSFetchedResultsController())
self.dateFormatter = DateFormatter()
self.dateFormatter.timeStyle = .short
self.dateFormatter.dateStyle = .short
super.init(coder: aDecoder)
self.prepareDataSource()
}
}
@ -90,21 +92,8 @@ extension SaveStatesViewController
{
super.viewDidLoad()
self.vibrancyView = UIVisualEffectView(effect: nil)
self.placeholderView = RSTPlaceholderView(frame: CGRect(x: 0, y: 0, width: self.vibrancyView.bounds.width, height: self.vibrancyView.bounds.height))
self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.placeholderView.textLabel.text = NSLocalizedString("No Save States", comment: "")
self.placeholderView.textLabel.textColor = UIColor.white
self.placeholderView.detailTextLabel.textColor = UIColor.white
self.vibrancyView.contentView.addSubview(self.placeholderView)
self.dataSource.proxy = self
self.dataSource.placeholderView = self.vibrancyView
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell as! GridCollectionViewCell, for: indexPath)
}
self.collectionView?.dataSource = self.dataSource
self.collectionView?.prefetchDataSource = self.dataSource
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2
@ -118,11 +107,11 @@ extension SaveStatesViewController
{
case .saving:
self.title = NSLocalizedString("Save State", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the + button in the top right.", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the + button in the top right.", comment: "")
case .loading:
self.title = NSLocalizedString("Load State", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the Save State option in the pause menu.", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the Save State option in the pause menu.", comment: "")
self.navigationItem.rightBarButtonItem = nil
}
@ -156,10 +145,57 @@ extension SaveStatesViewController
}
}
private extension SaveStatesViewController
{
func prepareDataSource()
{
self.dataSource.proxy = self
self.vibrancyView = UIVisualEffectView(effect: nil)
self.placeholderView = RSTPlaceholderView(frame: CGRect(x: 0, y: 0, width: self.vibrancyView.bounds.width, height: self.vibrancyView.bounds.height))
self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.placeholderView.textLabel.text = NSLocalizedString("No Save States", comment: "")
self.placeholderView.textLabel.textColor = UIColor.white
self.placeholderView.detailTextLabel.textColor = UIColor.white
self.vibrancyView.contentView.addSubview(self.placeholderView)
self.dataSource.placeholderView = self.vibrancyView
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell as! GridCollectionViewCell, for: indexPath)
}
self.dataSource.prefetchHandler = { [unowned self] (saveState, indexPath, completionHandler) in
let imageOperation = LoadImageURLOperation(url: saveState.imageFileURL)
imageOperation.resultHandler = { (image, error) in
completionHandler(image, error)
}
if self.isAppearing
{
imageOperation.start()
imageOperation.waitUntilFinished()
return nil
}
return imageOperation
}
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard let image = image, let cell = cell as? GridCollectionViewCell else { return }
cell.imageView.backgroundColor = nil
cell.imageView.image = image
cell.isImageViewVibrancyEnabled = false
}
}
}
private extension SaveStatesViewController
{
//MARK: - Update -
func updateDataSource()
{
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
@ -194,7 +230,7 @@ private extension SaveStatesViewController
//MARK: - Configure Views -
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath, ignoreExpensiveOperations ignoreOperations: Bool = false)
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath)
{
let saveState = self.dataSource.item(at: indexPath)
@ -213,35 +249,9 @@ private extension SaveStatesViewController
cell.isImageViewVibrancyEnabled = true
}
if !ignoreOperations
{
let imageOperation = LoadImageURLOperation(url: saveState.imageFileURL)
imageOperation.resultsCache = self.imageCache
imageOperation.resultHandler = { (image, error) in
if let image = image
{
DispatchQueue.main.async {
cell.imageView.backgroundColor = nil
cell.imageView.image = image
cell.isImageViewVibrancyEnabled = false
}
}
}
// Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail
if self.isAppearing
{
imageOperation.isImmediate = true
}
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying)
}
let deltaCore = Delta.core(for: self.game.type)!
let dimensions = deltaCore.emulatorConfiguration.videoBufferInfo.outputDimensions
let dimensions = deltaCore.videoFormat.dimensions
cell.maximumImageSize = CGSize(width: self.prototypeCellWidthConstraint.constant, height: (self.prototypeCellWidthConstraint.constant / dimensions.width) * dimensions.height)
cell.textLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
@ -259,6 +269,7 @@ private extension SaveStatesViewController
switch section
{
case .auto: title = NSLocalizedString("Auto Save", comment: "")
case .quick: title = NSLocalizedString("Quick Save", comment: "")
case .general: title = NSLocalizedString("General", comment: "")
case .locked: title = NSLocalizedString("Locked", comment: "")
}
@ -301,6 +312,7 @@ private extension SaveStatesViewController
let game = backgroundContext.object(with: self.game.objectID) as! Game
saveState = SaveState.insertIntoManagedObjectContext(backgroundContext)
saveState.type = .general
saveState.game = game
}
@ -365,7 +377,7 @@ private extension SaveStatesViewController
func rename(_ saveState: SaveState, with name: String?)
{
var name = name
if (name ?? "").characters.count == 0
if (name ?? "").count == 0
{
// When text is nil, we know to show the timestamp instead
name = nil
@ -471,6 +483,7 @@ private extension SaveStatesViewController
switch saveState.type
{
case .auto: break
case .quick: break
case .general:
let lockAction = Action(title: NSLocalizedString("Lock", comment: ""), style: .default, action: { [unowned self] action in
self.lockSaveState(saveState)
@ -549,13 +562,13 @@ private extension SaveStatesViewController
//MARK: - 3D Touch -
extension SaveStatesViewController: UIViewControllerPreviewingDelegate
{
fileprivate func prepareEmulatorCoreSaveState()
private func prepareEmulatorCoreSaveState()
{
guard let emulatorCore = self.emulatorCore else { return }
// Store reference to current game state before we stop emulation so we can resume it if user decides to not load a save state
let fileURL = FileManager.uniqueTemporaryURL()
let fileURL = FileManager.default.uniqueTemporaryURL()
self.emulatorCoreSaveState = emulatorCore.saveSaveState(to: fileURL)
if self.emulatorCoreSaveState != nil
@ -577,7 +590,7 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate
let saveState = self.dataSource.item(at: indexPath)
let actions = self.actionsForSaveState(saveState)?.previewActions ?? []
let previewImage = self.imageCache.object(forKey: saveState.imageFileURL as NSURL) ?? UIImage(contentsOfFile: saveState.imageFileURL.path)
let previewImage = self.dataSource.prefetchItemCache.object(forKey: saveState) ?? UIImage(contentsOfFile: saveState.imageFileURL.path)
let previewGameViewController = PreviewGameViewController()
previewGameViewController.game = self.game
@ -593,7 +606,7 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate
let gameViewController = viewControllerToCommit as! PreviewGameViewController
gameViewController.emulatorCore?.pause()
let fileURL = FileManager.uniqueTemporaryURL()
let fileURL = FileManager.default.uniqueTemporaryURL()
if let saveState = gameViewController.emulatorCore?.saveSaveState(to: fileURL)
{
gameViewController.emulatorCore?.stop()
@ -634,11 +647,11 @@ extension SaveStatesViewController
{
case .saving:
let section = self.correctedSectionForSectionIndex((indexPath as NSIndexPath).section)
let section = self.correctedSectionForSectionIndex(indexPath.section)
switch section
{
case .auto: break
case .general:
case .quick, .general:
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.performAndWait() {
let temporarySaveState = backgroundContext.object(with: saveState.objectID) as! SaveState
@ -655,12 +668,6 @@ extension SaveStatesViewController
case .loading: self.loadSaveState(saveState)
}
}
override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
{
let operation = self.imageOperationQueue[indexPath as NSCopying]
operation?.cancel()
}
}
//MARK: - <UICollectionViewDelegateFlowLayout> -
@ -668,8 +675,7 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
// No need to load images from disk just to determine size, so we pass true for ignoreExpensiveOperations
self.configure(self.prototypeCell, for: indexPath, ignoreExpensiveOperations: true)
self.configure(self.prototypeCell, for: indexPath)
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
return size

View File

@ -10,15 +10,15 @@ import UIKit
class PauseStoryboardSegue: UIStoryboardSegue
{
fileprivate let animator: UIViewPropertyAnimator
fileprivate let presentationController: PausePresentationController
private let animator: UIViewPropertyAnimator
private let presentationController: PausePresentationController
override init(identifier: String?, source: UIViewController, destination: UIViewController)
{
let timingParameters = UISpringTimingParameters(mass: 3.0, stiffness: 750, damping: 65, initialVelocity: CGVector(dx: 0, dy: 0))
self.animator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
self.presentationController = PausePresentationController(presentedViewController: destination, presenting: source)
self.presentationController = PausePresentationController(presentedViewController: destination, presenting: source, presentationAnimator: self.animator)
super.init(identifier: identifier, source: source, destination: destination)
}
@ -29,6 +29,9 @@ class PauseStoryboardSegue: UIStoryboardSegue
self.destination.modalPresentationStyle = .custom
self.destination.modalPresentationCapturesStatusBarAppearance = true
// Manually set tint color, since calling layoutIfNeeded will cause view to load, but with default system tint color.
self.destination.view.tintColor = .white
// We need to force layout of destinationViewController.view _before_ animateTransition(using:)
// Otherwise, we'll get "Unable to simultaneously satisfy constraints" errors
self.destination.view.frame = self.source.view.frame

View File

@ -37,6 +37,13 @@ class PauseTransitionCoordinator: NSObject, UIViewControllerAnimatedTransitionin
destinationViewController.view.layoutIfNeeded()
if let navigationController = destinationViewController.navigationController
{
// Layout before animation to prevent strange bar button item layout during animation.
navigationController.view.setNeedsLayout()
navigationController.view.layoutIfNeeded()
}
UIView.animate(withDuration: self.transitionDuration(using: transitionContext), delay:0, options:RSTSystemTransitionAnimationCurve, animations: {
sourceViewController.view.frame.origin.y = self.presenting ? -sourceViewController.view.bounds.height : transitionContext.containerView.bounds.height

View File

@ -12,18 +12,9 @@ import DeltaCore
import Roxas
extension ControllerSkinsViewController
{
enum Section: Int
{
case standard
case custom
}
}
class ControllerSkinsViewController: UITableViewController
{
var gameType: GameType! {
var system: System! {
didSet {
self.updateDataSource()
}
@ -35,11 +26,16 @@ class ControllerSkinsViewController: UITableViewController
}
}
fileprivate let dataSource = RSTFetchedResultsTableViewDataSource<ControllerSkin>(fetchedResultsController: NSFetchedResultsController())
private let dataSource: RSTFetchedResultsTableViewPrefetchingDataSource<ControllerSkin, UIImage>
fileprivate let imageOperationQueue = RSTOperationQueue()
fileprivate let imageCache = NSCache<ControllerSkinImageCacheKey, UIImage>()
required init?(coder aDecoder: NSCoder)
{
self.dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<ControllerSkin, UIImage>(fetchedResultsController: NSFetchedResultsController())
super.init(coder: aDecoder)
self.prepareDataSource()
}
}
extension ControllerSkinsViewController
@ -48,11 +44,8 @@ extension ControllerSkinsViewController
{
super.viewDidLoad()
self.dataSource.proxy = self
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell as! ControllerSkinTableViewCell, for: indexPath)
}
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
}
override func didReceiveMemoryWarning()
@ -65,57 +58,53 @@ extension ControllerSkinsViewController
private extension ControllerSkinsViewController
{
//MARK: - Update
func prepareDataSource()
{
self.dataSource.proxy = self
self.dataSource.cellConfigurationHandler = { (cell, item, indexPath) in
let cell = cell as! ControllerSkinTableViewCell
cell.controllerSkinImageView.image = nil
cell.activityIndicatorView.startAnimating()
}
self.dataSource.prefetchHandler = { [unowned self] (controllerSkin, indexPath, completionHandler) in
let imageOperation = LoadControllerSkinImageOperation(controllerSkin: controllerSkin, traits: self.traits, size: UIScreen.main.defaultControllerSkinSize)
imageOperation.resultHandler = { (image, error) in
completionHandler(image, error)
}
// Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail
if self.isAppearing
{
imageOperation.start()
imageOperation.waitUntilFinished()
return nil
}
return imageOperation
}
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard let image = image, let cell = cell as? ControllerSkinTableViewCell else { return }
cell.controllerSkinImageView.image = image
cell.activityIndicatorView.stopAnimating()
}
}
func updateDataSource()
{
guard let gameType = self.gameType, let traits = self.traits else { return }
guard let system = self.system, let traits = self.traits else { return }
let configuration = ControllerSkinConfigurations(traits: traits)
let fetchRequest: NSFetchRequest<ControllerSkin> = ControllerSkin.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND (%K & %d) == %d", #keyPath(ControllerSkin.gameType), gameType.rawValue, #keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue, configuration.rawValue)
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND (%K & %d) == %d", #keyPath(ControllerSkin.gameType), system.gameType.rawValue, #keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue, configuration.rawValue)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(ControllerSkin.isStandard), ascending: false), NSSortDescriptor(key: #keyPath(ControllerSkin.name), ascending: true)]
self.dataSource.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(ControllerSkin.name), cacheName: nil)
}
//MARK: - Configure Cells
func configure(_ cell: ControllerSkinTableViewCell, for indexPath: IndexPath)
{
let controllerSkin = self.dataSource.item(at: indexPath)
cell.controllerSkinImageView.image = nil
cell.activityIndicatorView.startAnimating()
let size = UIScreen.main.defaultControllerSkinSize
let imageOperation = LoadControllerSkinImageOperation(controllerSkin: controllerSkin, traits: self.traits, size: size)
imageOperation.resultsCache = self.imageCache
imageOperation.resultHandler = { (image, error) in
guard let image = image else { return }
if !imageOperation.isImmediate
{
UIView.transition(with: cell.controllerSkinImageView, duration: 0.2, options: .transitionCrossDissolve, animations: {
cell.controllerSkinImageView.image = image
}, completion: nil)
}
else
{
cell.controllerSkinImageView.image = image
}
cell.activityIndicatorView.stopAnimating()
}
// Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail
if self.isAppearing
{
imageOperation.isImmediate = true
}
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying)
}
}
extension ControllerSkinsViewController
@ -127,39 +116,12 @@ extension ControllerSkinsViewController
}
}
extension ControllerSkinsViewController: UITableViewDataSourcePrefetching
{
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath])
{
for indexPath in indexPaths
{
let controllerSkin = self.dataSource.item(at: indexPath)
let size = UIScreen.main.defaultControllerSkinSize
let imageOperation = LoadControllerSkinImageOperation(controllerSkin: controllerSkin, traits: self.traits, size: size)
imageOperation.resultsCache = self.imageCache
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying)
}
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath])
{
for indexPath in indexPaths
{
let operation = self.imageOperationQueue[indexPath as NSCopying]
operation?.cancel()
}
}
}
extension ControllerSkinsViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let controllerSkin = self.dataSource.item(at: indexPath)
Settings.setPreferredControllerSkin(controllerSkin, for: self.gameType, traits: self.traits)
Settings.setPreferredControllerSkin(controllerSkin, for: self.system, traits: self.traits)
_ = self.navigationController?.popViewController(animated: true)
}
@ -176,10 +138,4 @@ extension ControllerSkinsViewController
return height
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
{
let operation = self.imageOperationQueue[indexPath as NSCopying]
operation?.cancel()
}
}

View File

@ -1,5 +1,5 @@
//
// GameTypeControllerSkinsViewController.swift
// SystemControllerSkinsViewController.swift
// Delta
//
// Created by Riley Testut on 9/30/16.
@ -10,30 +10,30 @@ import UIKit
import DeltaCore
extension GameTypeControllerSkinsViewController
extension SystemControllerSkinsViewController
{
fileprivate enum Section: Int
private enum Section: Int
{
case portrait
case landscape
}
}
class GameTypeControllerSkinsViewController: UITableViewController
class SystemControllerSkinsViewController: UITableViewController
{
var gameType: GameType!
var system: System!
@IBOutlet fileprivate var portraitImageView: UIImageView!
@IBOutlet fileprivate var landscapeImageView: UIImageView!
@IBOutlet private var portraitImageView: UIImageView!
@IBOutlet private var landscapeImageView: UIImageView!
}
extension GameTypeControllerSkinsViewController
extension SystemControllerSkinsViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.title = self.gameType.localizedShortName
self.title = self.system.localizedShortName
}
override func viewWillAppear(_ animated: Bool)
@ -53,7 +53,7 @@ extension GameTypeControllerSkinsViewController
guard let cell = sender as? UITableViewCell, let indexPath = self.tableView.indexPath(for: cell) else { return }
let controllerSkinsViewController = segue.destination as! ControllerSkinsViewController
controllerSkinsViewController.gameType = self.gameType
controllerSkinsViewController.system = self.system
var traits = DeltaCore.ControllerSkin.Traits.defaults(for: self.view)
@ -68,7 +68,7 @@ extension GameTypeControllerSkinsViewController
}
}
extension GameTypeControllerSkinsViewController
extension SystemControllerSkinsViewController
{
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
@ -98,15 +98,15 @@ extension GameTypeControllerSkinsViewController
}
}
private extension GameTypeControllerSkinsViewController
private extension SystemControllerSkinsViewController
{
func updateControllerSkins()
{
let portraitTraits = DeltaCore.ControllerSkin.Traits(deviceType: .iphone, displayMode: DeltaCore.ControllerSkin.DisplayMode.fullScreen, orientation: .portrait)
let landscapeTraits = DeltaCore.ControllerSkin.Traits(deviceType: .iphone, displayMode: DeltaCore.ControllerSkin.DisplayMode.fullScreen, orientation: .landscape)
let portraitControllerSkin = Settings.preferredControllerSkin(for: self.gameType, traits: portraitTraits)
let landscapeControllerSkin = Settings.preferredControllerSkin(for: self.gameType, traits: landscapeTraits)
let portraitControllerSkin = Settings.preferredControllerSkin(for: self.system, traits: portraitTraits)
let landscapeControllerSkin = Settings.preferredControllerSkin(for: self.system, traits: landscapeTraits)
self.portraitImageView.image = portraitControllerSkin?.image(for: portraitTraits, preferredSize: UIScreen.main.defaultControllerSkinSize)
self.landscapeImageView.image = landscapeControllerSkin?.image(for: landscapeTraits, preferredSize: UIScreen.main.defaultControllerSkinSize)

View File

@ -0,0 +1,508 @@
//
// ControllerInputsViewController.swift
// Delta
//
// Created by Riley Testut on 7/1/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
import DeltaCore
import SMCalloutView
class ControllerInputsViewController: UIViewController
{
var gameController: GameController! {
didSet {
self.gameController.addReceiver(self, inputMapping: nil)
}
}
var system: System = .snes {
didSet {
guard self.system != oldValue else { return }
self.updateSystem()
}
}
private lazy var managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.newBackgroundContext()
private var inputMappings = [System: GameControllerInputMapping]()
private let supportedActionInputs: [ActionInput] = [.quickSave, .quickLoad, .fastForward]
private var gameViewController: DeltaCore.GameViewController!
private var actionsMenuViewController: GridMenuViewController!
private var calloutViews = [AnyInput: InputCalloutView]()
private var activeCalloutView: InputCalloutView?
@IBOutlet private var actionsMenuViewControllerHeightConstraint: NSLayoutConstraint!
@IBOutlet private var cancelTapGestureRecognizer: UITapGestureRecognizer!
override func viewDidLoad()
{
super.viewDidLoad()
self.gameViewController.controllerView.addReceiver(self)
self.navigationController?.navigationBar.barStyle = .black
NSLayoutConstraint.activate([self.gameViewController.gameView.centerYAnchor.constraint(equalTo: self.actionsMenuViewController.view.centerYAnchor)])
self.preparePopoverMenuController()
self.updateSystem()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if self.actionsMenuViewController.preferredContentSize.height > 0
{
self.actionsMenuViewControllerHeightConstraint.constant = self.actionsMenuViewController.preferredContentSize.height
}
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
if self.calloutViews.isEmpty
{
self.prepareCallouts()
}
}
}
extension ControllerInputsViewController
{
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard let identifier = segue.identifier else { return }
switch identifier
{
case "embedGameViewController": self.gameViewController = segue.destination as! DeltaCore.GameViewController
case "embedActionsMenuViewController":
self.actionsMenuViewController = segue.destination as! GridMenuViewController
self.prepareActionsMenuViewController()
case "cancelControllerInputs": break
case "saveControllerInputs":
self.managedObjectContext.performAndWait {
self.managedObjectContext.saveWithErrorLogging()
}
default: break
}
}
}
private extension ControllerInputsViewController
{
func updateSystem()
{
guard self.isViewLoaded else { return }
// Update popoverMenuButton to display correctly on iOS 10.
if let popoverMenuButton = self.navigationItem.popoverMenuController?.popoverMenuButton
{
popoverMenuButton.title = self.system.localizedShortName
popoverMenuButton.bounds.size = popoverMenuButton.intrinsicContentSize
self.navigationController?.navigationBar.layoutIfNeeded()
}
// Update controller view's controller skin.
self.gameViewController.controllerView.controllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: self.system.gameType)
// Fetch input mapping if it hasn't already been fetched.
if let gameController = self.gameController, let playerIndex = gameController.playerIndex, self.inputMappings[self.system] == nil
{
self.managedObjectContext.performAndWait {
let inputMapping = GameControllerInputMapping.inputMapping(for: gameController, gameType: self.system.gameType, in: self.managedObjectContext) ?? {
let deltaCoreInputMapping = gameController.defaultInputMapping as? DeltaCore.GameControllerInputMapping ?? DeltaCore.GameControllerInputMapping(gameControllerInputType: gameController.inputType)
let inputMapping = GameControllerInputMapping(inputMapping: deltaCoreInputMapping, context: self.managedObjectContext)
inputMapping.gameControllerInputType = gameController.inputType
inputMapping.gameType = self.system.gameType
inputMapping.playerIndex = Int16(playerIndex)
return inputMapping
}()
inputMapping.name = String.localizedStringWithFormat("Custom %@", gameController.name)
self.inputMappings[self.system] = inputMapping
}
}
// Update callouts, if view is already on screen.
if self.view.window != nil
{
self.calloutViews.forEach { $1.dismissCallout(animated: true) }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.calloutViews = [:]
self.prepareCallouts()
}
}
}
func preparePopoverMenuController()
{
let listMenuViewController = ListMenuViewController()
listMenuViewController.title = NSLocalizedString("Game System", comment: "")
let navigationController = UINavigationController(rootViewController: listMenuViewController)
let popoverMenuController = PopoverMenuController(popoverViewController: navigationController)
self.navigationItem.popoverMenuController = popoverMenuController
let items = System.supportedSystems.map { [unowned self, weak popoverMenuController, weak listMenuViewController] system -> MenuItem in
let item = MenuItem(text: system.localizedShortName, image: #imageLiteral(resourceName: "CheatCodes")) { [weak popoverMenuController, weak listMenuViewController] item in
listMenuViewController?.items.forEach { $0.isSelected = ($0 == item) }
popoverMenuController?.isActive = false
self.system = system
}
item.isSelected = (system == self.system)
return item
}
listMenuViewController.items = items
}
func prepareActionsMenuViewController()
{
var items = [MenuItem]()
for input in self.supportedActionInputs
{
let image: UIImage
let text: String
switch input
{
case .quickSave:
image = #imageLiteral(resourceName: "SaveSaveState")
text = NSLocalizedString("Quick Save", comment: "")
case .quickLoad:
image = #imageLiteral(resourceName: "LoadSaveState")
text = NSLocalizedString("Quick Load", comment: "")
case .fastForward:
image = #imageLiteral(resourceName: "FastForward")
text = NSLocalizedString("Fast Forward", comment: "")
}
let item = MenuItem(text: text, image: image) { [unowned self] (item) in
guard let calloutView = self.calloutViews[AnyInput(input)] else { return }
self.toggle(calloutView)
}
items.append(item)
}
self.actionsMenuViewController.items = items
self.actionsMenuViewController.isVibrancyEnabled = false
}
func prepareCallouts()
{
guard
let controllerView = self.gameViewController.controllerView,
let traits = controllerView.controllerSkinTraits,
let items = controllerView.controllerSkin?.items(for: traits),
let controllerViewInputMapping = controllerView.defaultInputMapping,
let inputMapping = self.inputMappings[self.system]
else { return }
// Implicit assumption that all skins used for controller input mapping don't have multiple items with same input.
let mappedInputs = items.flatMap { $0.inputs.allInputs.flatMap(controllerViewInputMapping.input(forControllerInput:)) } + (self.supportedActionInputs as [Input])
// Create callout view for each on-screen input.
for input in mappedInputs
{
let calloutView = InputCalloutView()
calloutView.delegate = self
self.calloutViews[AnyInput(input)] = calloutView
}
self.managedObjectContext.performAndWait {
// Update callout views with controller inputs that map to callout views' associated controller skin inputs.
for input in inputMapping.supportedControllerInputs
{
let mappedInput = self.mappedInput(for: input)
if let calloutView = self.calloutViews[mappedInput]
{
if let previousInput = calloutView.input
{
// Ensure the input we display has a higher priority.
calloutView.input = (input.displayPriority > previousInput.displayPriority) ? input : previousInput
}
else
{
calloutView.input = input
}
}
}
}
// Present only callout views that are associated with a controller input.
for calloutView in self.calloutViews.values
{
if let presentationRect = self.presentationRect(for: calloutView), calloutView.input != nil
{
calloutView.presentCallout(from: presentationRect, in: self.view, constrainedTo: self.view, animated: true)
}
}
}
}
private extension ControllerInputsViewController
{
func updateActiveCalloutView(with controllerInput: Input?)
{
guard let inputMapping = self.inputMappings[self.system] else { return }
guard let activeCalloutView = self.activeCalloutView else { return }
guard let input = self.calloutViews.first(where: { $0.value == activeCalloutView })?.key else { return }
if let controllerInput = controllerInput
{
for (_, calloutView) in self.calloutViews
{
guard let calloutInput = calloutView.input else { continue }
if calloutInput == controllerInput
{
// Hide callout views that previously displayed the controller input.
calloutView.input = nil
calloutView.dismissCallout(animated: true)
}
}
}
self.managedObjectContext.performAndWait {
for supportedInput in inputMapping.supportedControllerInputs
{
let mappedInput = self.mappedInput(for: supportedInput)
if mappedInput == input
{
// Set all existing controller inputs that currently map to "input" to instead map to nil.
inputMapping.set(nil, forControllerInput: supportedInput)
}
}
if let controllerInput = controllerInput
{
inputMapping.set(input, forControllerInput: controllerInput)
}
}
activeCalloutView.input = controllerInput
self.toggle(activeCalloutView)
}
func toggle(_ calloutView: InputCalloutView)
{
if let activeCalloutView = self.activeCalloutView, activeCalloutView != calloutView
{
self.toggle(activeCalloutView)
}
let menuItem: MenuItem?
if let input = self.calloutViews.first(where: { $0.value == calloutView })?.key, let index = self.supportedActionInputs.index(where: { $0 == input })
{
menuItem = self.actionsMenuViewController.items[index]
}
else
{
menuItem = nil
}
switch calloutView.state
{
case .normal:
calloutView.state = .listening
menuItem?.isSelected = true
self.activeCalloutView = calloutView
case .listening:
calloutView.state = .normal
menuItem?.isSelected = false
self.activeCalloutView = nil
}
calloutView.dismissCallout(animated: true)
if let presentationRect = self.presentationRect(for: calloutView)
{
if calloutView.state == .listening || calloutView.input != nil
{
calloutView.presentCallout(from: presentationRect, in: self.view, constrainedTo: self.view, animated: true)
}
}
}
}
extension ControllerInputsViewController: UIGestureRecognizerDelegate
{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool
{
return self.activeCalloutView != nil
}
@IBAction private func handleTapGesture(_ tapGestureRecognizer: UITapGestureRecognizer)
{
self.updateActiveCalloutView(with: nil)
}
}
private extension ControllerInputsViewController
{
func mappedInput(for input: Input) -> AnyInput
{
guard let inputMapping = self.inputMappings[self.system] else {
fatalError("Input mapping for current system does not exist.")
}
guard let mappedInput = inputMapping.input(forControllerInput: input) else {
fatalError("Mapped input for provided input does not exist.")
}
if let standardInput = StandardGameControllerInput(input: mappedInput)
{
if let gameInput = standardInput.input(for: self.system.gameType)
{
return AnyInput(gameInput)
}
}
return AnyInput(mappedInput)
}
func presentationRect(for calloutView: InputCalloutView) -> CGRect?
{
guard let input = self.calloutViews.first(where: { $0.value == calloutView })?.key else { return nil }
guard
let controllerView = self.gameViewController.controllerView,
let traits = controllerView.controllerSkinTraits,
let items = controllerView.controllerSkin?.items(for: traits)
else { return nil }
if let item = items.first(where: { $0.inputs.allInputs.contains(where: { $0.stringValue == input.stringValue })})
{
// Input is a controller skin input.
let itemFrame: CGRect?
switch item.inputs
{
case .standard: itemFrame = item.frame
case let .directional(up, down, left, right):
switch input.stringValue
{
case up.stringValue:
itemFrame = CGRect(x: item.frame.minX + item.frame.width / 3,
y: item.frame.minY,
width: item.frame.width / 3,
height: item.frame.height / 3)
case down.stringValue:
itemFrame = CGRect(x: item.frame.minX + item.frame.width / 3,
y: item.frame.minY + (item.frame.height / 3) * 2,
width: item.frame.width / 3,
height: item.frame.height / 3)
case left.stringValue:
itemFrame = CGRect(x: item.frame.minX,
y: item.frame.minY + (item.frame.height / 3),
width: item.frame.width / 3,
height: item.frame.height / 3)
case right.stringValue:
itemFrame = CGRect(x: item.frame.minX + (item.frame.width / 3) * 2,
y: item.frame.minY + (item.frame.height / 3),
width: item.frame.width / 3,
height: item.frame.height / 3)
default: itemFrame = nil
}
}
if let itemFrame = itemFrame
{
var presentationFrame = itemFrame.applying(CGAffineTransform(scaleX: controllerView.bounds.width, y: controllerView.bounds.height))
presentationFrame = self.view.convert(presentationFrame, from: controllerView)
return presentationFrame
}
}
else if let index = self.supportedActionInputs.index(where: { $0 == input })
{
// Input is an ActionInput.
let indexPath = IndexPath(item: index, section: 0)
if let attributes = self.actionsMenuViewController.collectionViewLayout.layoutAttributesForItem(at: indexPath)
{
let presentationFrame = self.view.convert(attributes.frame, from: self.actionsMenuViewController.view)
return presentationFrame
}
}
else
{
// Input is not an on-screen input.
}
return nil
}
}
extension ControllerInputsViewController: GameControllerReceiver
{
func gameController(_ gameController: GameController, didActivate controllerInput: DeltaCore.Input)
{
guard self.isViewLoaded else { return }
switch gameController
{
case self.gameViewController.controllerView:
if let calloutView = self.calloutViews[AnyInput(controllerInput)]
{
self.toggle(calloutView)
}
case self.gameController: self.updateActiveCalloutView(with: controllerInput)
default: break
}
}
func gameController(_ gameController: GameController, didDeactivate input: DeltaCore.Input)
{
}
}
extension ControllerInputsViewController: SMCalloutViewDelegate
{
func calloutViewClicked(_ calloutView: SMCalloutView)
{
guard let calloutView = calloutView as? InputCalloutView else { return }
self.toggle(calloutView)
}
}

View File

@ -9,39 +9,62 @@
import UIKit
import DeltaCore
private enum ControllersSettingsSection: Int
import Roxas
extension ControllersSettingsViewController
{
case none
case localDevice
case externalControllers
private enum Section: Int
{
case none
case localDevice
case externalControllers
case customizeControls
}
}
private class LocalDeviceController: ExternalController
private class LocalDeviceController: NSObject, GameController
{
override var name: String {
var name: String {
return UIDevice.current.name
}
var playerIndex: Int? {
set { Settings.localControllerPlayerIndex = newValue }
get { return Settings.localControllerPlayerIndex }
}
let inputType: GameControllerInputType = .standard
var defaultInputMapping: GameControllerInputMappingProtocol?
}
class ControllersSettingsViewController: UITableViewController
{
var playerIndex: Int? {
didSet
{
if let playerIndex = self.playerIndex
var playerIndex: Int! {
didSet {
self.title = NSLocalizedString("Player \(self.playerIndex + 1)", comment: "")
}
}
private var gameController: GameController? {
didSet {
// Order matters since localDeviceController changes Settings.localControllerPlayerIndex, which sends out NSNotification.
if oldValue == self.localDeviceController
{
self.title = NSLocalizedString("Player \(playerIndex + 1)", comment: "")
self.gameController?.playerIndex = self.playerIndex
oldValue?.playerIndex = nil
}
else
{
self.title = NSLocalizedString("Controllers", comment: "")
oldValue?.playerIndex = nil
self.gameController?.playerIndex = self.playerIndex
}
}
}
fileprivate var connectedControllers = ExternalControllerManager.shared.connectedControllers.sorted(by: { $0.playerIndex ?? NSIntegerMax < $1.playerIndex ?? NSIntegerMax })
private var connectedControllers = ExternalGameControllerManager.shared.connectedControllers.sorted(by: { $0.playerIndex ?? NSIntegerMax < $1.playerIndex ?? NSIntegerMax })
fileprivate lazy var localDeviceController: LocalDeviceController = {
private lazy var localDeviceController: LocalDeviceController = {
let device = LocalDeviceController()
device.playerIndex = Settings.localControllerPlayerIndex
@ -52,146 +75,76 @@ class ControllersSettingsViewController: UITableViewController
{
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(ControllersSettingsViewController.externalControllerDidConnect(_:)), name: .externalControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ControllersSettingsViewController.externalControllerDidDisconnect(_:)), name: .externalControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ControllersSettingsViewController.externalGameControllerDidConnect(_:)), name: .externalGameControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ControllersSettingsViewController.externalGameControllerDidDisconnect(_:)), name: .externalGameControllerDidDisconnect, object: nil)
}
override func viewDidLoad()
{
super.viewDidLoad()
}
//MARK: - Storyboards -
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard let indexPath = self.tableView.indexPathForSelectedRow else { return }
var controllers = self.connectedControllers
controllers.append(self.localDeviceController)
// Reset previous controller
if let playerIndex = self.playerIndex, let index = controllers.index(where: { $0.playerIndex == playerIndex })
let gameControllers = [self.localDeviceController as GameController] + self.connectedControllers
for gameController in gameControllers
{
let controller = controllers[index]
controller.playerIndex = nil
}
switch ControllersSettingsSection(rawValue: (indexPath as NSIndexPath).section)!
{
case .none: break
case .localDevice: self.localDeviceController.playerIndex = self.playerIndex
case .externalControllers:
let controller = self.connectedControllers[(indexPath as NSIndexPath).row]
controller.playerIndex = self.playerIndex
}
// Updates in case we reset it above, as well as if we updated in the switch statement
Settings.localControllerPlayerIndex = self.localDeviceController.playerIndex
}
}
private extension ControllersSettingsViewController
{
dynamic func externalControllerDidConnect(_ notification: Notification)
{
guard let controller = notification.object as? ExternalController else { return }
if let playerIndex = controller.playerIndex
{
self.connectedControllers.insert(controller, at: playerIndex)
}
else
{
self.connectedControllers.append(controller)
}
if let index = self.connectedControllers.index(of: controller)
{
if self.connectedControllers.count == 1
if gameController.playerIndex == self.playerIndex
{
self.tableView.insertSections(IndexSet(integer: ControllersSettingsSection.externalControllers.rawValue), with: .fade)
self.gameController = gameController
break
}
else
{
self.tableView.insertRows(at: [IndexPath(row: index, section: ControllersSettingsSection.externalControllers.rawValue)], with: .automatic)
}
}
}
dynamic func externalControllerDidDisconnect(_ notification: Notification)
{
guard let controller = notification.object as? ExternalController else { return }
if let index = self.connectedControllers.index(of: controller)
{
self.connectedControllers.remove(at: index)
if self.connectedControllers.count == 0
{
self.tableView.deleteSections(IndexSet(integer: ControllersSettingsSection.externalControllers.rawValue), with: .fade)
}
else
{
self.tableView.deleteRows(at: [IndexPath(row: index, section: ControllersSettingsSection.externalControllers.rawValue)], with: .automatic)
}
}
if controller.playerIndex == self.playerIndex
{
self.tableView.reloadSections(IndexSet(integer: ControllersSettingsSection.none.rawValue), with: .none)
}
}
}
extension ControllersSettingsViewController
{
override func numberOfSections(in tableView: UITableView) -> Int
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if self.connectedControllers.count == 0
{
return 2
}
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
switch ControllersSettingsSection(rawValue: section)!
{
case .none: return 1
case .localDevice: return 1
case .externalControllers: return self.connectedControllers.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.detailTextLabel?.text = nil
cell.accessoryType = .none
guard let identifier = segue.identifier else { return }
if (indexPath as NSIndexPath).section == ControllersSettingsSection.none.rawValue
switch identifier
{
case "controllerInputsSegue":
let controllerInputsViewController = (segue.destination as! UINavigationController).topViewController as! ControllerInputsViewController
controllerInputsViewController.gameController = self.gameController
controllerInputsViewController.system = .snes
default: break
}
}
@IBAction private func unwindFromControllerControlsViewController(_ segue: UIStoryboardSegue)
{
}
}
private extension ControllersSettingsViewController
{
func configure(_ cell: UITableViewCell, for indexPath: IndexPath)
{
cell.accessoryType = .none
cell.detailTextLabel?.text = nil
cell.textLabel?.textColor = .darkText
switch Section(rawValue: indexPath.section)!
{
case .none:
cell.textLabel?.text = NSLocalizedString("None", comment: "")
if Settings.localControllerPlayerIndex != self.playerIndex && !self.connectedControllers.contains(where: { $0.playerIndex == self.playerIndex })
{
cell.accessoryType = .checkmark
}
}
else
{
let controller: ExternalController
if (indexPath as NSIndexPath).section == ControllersSettingsSection.localDevice.rawValue
case .localDevice, .externalControllers:
let controller: GameController
if indexPath.section == Section.localDevice.rawValue
{
controller = self.localDeviceController
}
else if (indexPath as NSIndexPath).section == ControllersSettingsSection.externalControllers.rawValue
else if indexPath.section == Section.externalControllers.rawValue
{
controller = self.connectedControllers[(indexPath as NSIndexPath).row]
controller = self.connectedControllers[indexPath.row]
}
else
{
@ -205,25 +158,216 @@ extension ControllersSettingsViewController
cell.accessoryType = .checkmark
}
else
{
{
if let playerIndex = controller.playerIndex
{
cell.detailTextLabel?.text = NSLocalizedString("Player \(playerIndex + 1)", comment: "")
}
}
case .customizeControls:
cell.textLabel?.text = NSLocalizedString("Customize Controls…", comment: "")
cell.textLabel?.textColor = self.view.tintColor
}
}
}
private extension ControllersSettingsViewController
{
@objc func externalGameControllerDidConnect(_ notification: Notification)
{
guard let controller = notification.object as? GameController else { return }
if let playerIndex = controller.playerIndex
{
// Keep connected controllers sorted.
self.connectedControllers.insert(controller, at: playerIndex)
}
else
{
self.connectedControllers.append(controller)
}
if let index = self.connectedControllers.index(where: { $0 == controller })
{
self.tableView.beginUpdates()
if self.connectedControllers.count == 1
{
self.tableView.insertSections(IndexSet(integer: Section.externalControllers.rawValue), with: .fade)
if self.connectedControllers.first?.playerIndex == self.playerIndex
{
self.tableView.insertSections(IndexSet(integer: Section.customizeControls.rawValue), with: .fade)
}
}
else
{
self.tableView.insertRows(at: [IndexPath(row: index, section: Section.externalControllers.rawValue)], with: .automatic)
}
self.tableView.endUpdates()
}
if controller.playerIndex == self.playerIndex
{
self.tableView.reloadSections(IndexSet(integer: Section.none.rawValue), with: .none)
self.tableView.reloadSections(IndexSet(integer: Section.localDevice.rawValue), with: .none)
}
}
@objc func externalGameControllerDidDisconnect(_ notification: Notification)
{
guard let controller = notification.object as? GameController else { return }
if let index = self.connectedControllers.index(where: { $0 == controller })
{
self.connectedControllers.remove(at: index)
self.tableView.beginUpdates()
if self.connectedControllers.count == 0
{
self.tableView.deleteSections(IndexSet(integer: Section.externalControllers.rawValue), with: .fade)
if controller.playerIndex != nil
{
self.tableView.deleteSections(IndexSet(integer: Section.customizeControls.rawValue), with: .fade)
}
}
else
{
self.tableView.deleteRows(at: [IndexPath(row: index, section: Section.externalControllers.rawValue)], with: .automatic)
}
self.tableView.endUpdates()
}
if controller.playerIndex == self.playerIndex
{
self.tableView.reloadSections(IndexSet(integer: Section.none.rawValue), with: .none)
self.tableView.reloadSections(IndexSet(integer: Section.localDevice.rawValue), with: .none)
}
}
}
extension ControllersSettingsViewController
{
override func numberOfSections(in tableView: UITableView) -> Int
{
if self.connectedControllers.count == 0
{
return 2
}
if self.gameController == nil || Settings.localControllerPlayerIndex == self.playerIndex
{
return 3
}
return 4
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
switch Section(rawValue: section)!
{
case .none: return 0
case .localDevice: return 1
case .externalControllers: return self.connectedControllers.count
case .customizeControls: return 1
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: RSTCellContentGenericCellIdentifier, for: indexPath)
self.configure(cell, for: indexPath)
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
switch ControllersSettingsSection(rawValue: section)!
switch Section(rawValue: section)!
{
case .none: return nil
case .localDevice: return NSLocalizedString("Local Device", comment: "")
case .externalControllers: return self.connectedControllers.count > 0 ? NSLocalizedString("External Controllers", comment: "") : ""
case .customizeControls: return nil
}
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
if section == Section.none.rawValue
{
return 1
}
return UITableViewAutomaticDimension
}
}
extension ControllersSettingsViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let previousIndexPath: IndexPath?
if let gameController = self.gameController
{
if gameController == self.localDeviceController
{
previousIndexPath = IndexPath(row: 0, section: Section.localDevice.rawValue)
}
else if let row = self.connectedControllers.index(where: { $0 == gameController })
{
previousIndexPath = IndexPath(row: row, section: Section.externalControllers.rawValue)
}
else
{
previousIndexPath = nil
}
}
else
{
previousIndexPath = IndexPath(row: 0, section: Section.none.rawValue)
}
switch Section(rawValue: indexPath.section)!
{
case .none: self.gameController = nil
case .localDevice: self.gameController = self.localDeviceController
case .externalControllers: self.gameController = self.connectedControllers[indexPath.row]
case .customizeControls:
guard let cell = tableView.cellForRow(at: indexPath) else { return }
self.performSegue(withIdentifier: "controllerInputsSegue", sender: cell)
return
}
self.tableView.beginUpdates()
if let previousIndexPath = previousIndexPath, let cell = tableView.cellForRow(at: previousIndexPath)
{
// Must configure cell directly, or else a strange animation occurs when reloading row on iOS 11.
self.configure(cell, for: previousIndexPath)
}
self.tableView.reloadRows(at: [indexPath], with: .none)
if self.numberOfSections(in: self.tableView) > self.tableView.numberOfSections
{
self.tableView.insertSections(IndexSet(integer: Section.customizeControls.rawValue), with: .fade)
}
else if self.numberOfSections(in: self.tableView) < self.tableView.numberOfSections
{
self.tableView.deleteSections(IndexSet(integer: Section.customizeControls.rawValue), with: .fade)
}
self.tableView.endUpdates()
}
}

View File

@ -0,0 +1,84 @@
//
// InputCalloutView.swift
// Delta
//
// Created by Riley Testut on 7/9/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import SMCalloutView
import DeltaCore
extension InputCalloutView
{
enum State
{
case normal
case listening
}
}
class InputCalloutView: SMCalloutView
{
var input: Input? {
didSet {
self.updateState()
}
}
var state: State = .normal {
didSet {
self.updateState()
}
}
private let textLabel: UILabel
init()
{
self.textLabel = UILabel()
self.textLabel.font = UIFont.boldSystemFont(ofSize: 18.0)
self.textLabel.textAlignment = .center
super.init(frame: CGRect.zero)
self.titleView = self.textLabel
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func tintColorDidChange()
{
super.tintColorDidChange()
self.updateTintColor()
}
}
private extension InputCalloutView
{
func updateState()
{
switch self.state
{
case .normal: self.textLabel.text = self.input?.localizedName
case .listening: self.textLabel.text = NSLocalizedString("Press Button", comment: "")
}
self.updateTintColor()
self.textLabel.sizeToFit()
}
func updateTintColor()
{
switch self.state
{
case .normal: self.textLabel.textColor = self.tintColor
case .listening: self.textLabel.textColor = .red
}
}
}

View File

@ -9,8 +9,6 @@
import Foundation
import DeltaCore
import SNESDeltaCore
import GBADeltaCore
import Roxas
@ -25,7 +23,7 @@ extension Settings
{
case name
case gameType
case system
case traits
}
@ -42,12 +40,14 @@ struct Settings
/// Controllers
static var localControllerPlayerIndex: Int? = 0 {
didSet {
guard self.localControllerPlayerIndex != oldValue else { return }
NotificationCenter.default.post(name: .settingsDidChange, object: nil, userInfo: [NotificationUserInfoKey.name: Name.localControllerPlayerIndex])
}
}
static var translucentControllerSkinOpacity: CGFloat {
set {
guard newValue != self.translucentControllerSkinOpacity else { return }
UserDefaults.standard.translucentControllerSkinOpacity = newValue
NotificationCenter.default.post(name: .settingsDidChange, object: nil, userInfo: [NotificationUserInfoKey.name: Name.translucentControllerSkinOpacity])
}
@ -72,9 +72,9 @@ struct Settings
UserDefaults.standard.register(defaults: defaults)
}
static func preferredControllerSkin(for gameType: GameType, traits: DeltaCore.ControllerSkin.Traits) -> ControllerSkin?
static func preferredControllerSkin(for system: System, traits: DeltaCore.ControllerSkin.Traits) -> ControllerSkin?
{
guard let userDefaultsKey = self.preferredControllerSkinKey(for: gameType, traits: traits) else { return nil }
guard let userDefaultsKey = self.preferredControllerSkinKey(for: system, traits: traits) else { return nil }
let identifier = UserDefaults.standard.string(forKey: userDefaultsKey)
@ -86,7 +86,7 @@ struct Settings
if let identifier = identifier
{
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@", #keyPath(ControllerSkin.gameType), gameType.rawValue, #keyPath(ControllerSkin.identifier), identifier)
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@", #keyPath(ControllerSkin.gameType), system.gameType.rawValue, #keyPath(ControllerSkin.identifier), identifier)
if let controllerSkin = try DatabaseManager.shared.viewContext.fetch(fetchRequest).first
{
@ -96,11 +96,11 @@ struct Settings
// Controller skin doesn't exist, so fall back to standard controller skin
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == YES", #keyPath(ControllerSkin.gameType), gameType.rawValue, #keyPath(ControllerSkin.isStandard))
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == YES", #keyPath(ControllerSkin.gameType), system.gameType.rawValue, #keyPath(ControllerSkin.isStandard))
if let controllerSkin = try DatabaseManager.shared.viewContext.fetch(fetchRequest).first
{
Settings.setPreferredControllerSkin(controllerSkin, for: gameType, traits: traits)
Settings.setPreferredControllerSkin(controllerSkin, for: system, traits: traits)
return controllerSkin
}
}
@ -112,26 +112,29 @@ struct Settings
return nil
}
static func setPreferredControllerSkin(_ controllerSkin: ControllerSkin, for gameType: GameType, traits: DeltaCore.ControllerSkin.Traits)
static func setPreferredControllerSkin(_ controllerSkin: ControllerSkin, for system: System, traits: DeltaCore.ControllerSkin.Traits)
{
guard let userDefaultKey = self.preferredControllerSkinKey(for: gameType, traits: traits) else { return }
guard let userDefaultKey = self.preferredControllerSkinKey(for: system, traits: traits) else { return }
guard UserDefaults.standard.string(forKey: userDefaultKey) != controllerSkin.identifier else { return }
UserDefaults.standard.set(controllerSkin.identifier, forKey: userDefaultKey)
NotificationCenter.default.post(name: .settingsDidChange, object: controllerSkin, userInfo: [NotificationUserInfoKey.name: Name.preferredControllerSkin, NotificationUserInfoKey.gameType: gameType, NotificationUserInfoKey.traits: traits])
NotificationCenter.default.post(name: .settingsDidChange, object: controllerSkin, userInfo: [NotificationUserInfoKey.name: Name.preferredControllerSkin, NotificationUserInfoKey.system: system, NotificationUserInfoKey.traits: traits])
}
}
private extension Settings
{
static func preferredControllerSkinKey(for gameType: GameType, traits: DeltaCore.ControllerSkin.Traits) -> String?
static func preferredControllerSkinKey(for system: System, traits: DeltaCore.ControllerSkin.Traits) -> String?
{
let systemName: String
switch gameType
switch system
{
case GameType.snes: systemName = "snes"
case GameType.gba: systemName = "gba"
default: return nil
case .snes: systemName = "snes"
case .gba: systemName = "gba"
case .gbc: systemName = "gbc"
}
let orientation: String

View File

@ -7,37 +7,49 @@
//
import UIKit
import DeltaCore
extension SettingsViewController
import Roxas
private extension SettingsViewController
{
fileprivate enum Section: Int
enum Section: Int
{
case controllers
case controllerSkins
case controllerOpacity
}
fileprivate enum Segue: String
enum Segue: String
{
case controllers = "controllersSegue"
case controllerSkins = "controllerSkinsSegue"
}
enum ControllerSkinsRow: Int
{
case snes
case gba
case gbc
}
}
class SettingsViewController: UITableViewController
{
@IBOutlet fileprivate var controllerOpacityLabel: UILabel!
@IBOutlet fileprivate var controllerOpacitySlider: UISlider!
@IBOutlet private var controllerOpacityLabel: UILabel!
@IBOutlet private var controllerOpacitySlider: UISlider!
fileprivate var selectionFeedbackGenerator: UISelectionFeedbackGenerator?
private var selectionFeedbackGenerator: UISelectionFeedbackGenerator?
private var previousSelectedRowIndexPath: IndexPath?
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalControllerDidConnect(_:)), name: .externalControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalControllerDidDisconnect(_:)), name: .externalControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalGameControllerDidConnect(_:)), name: .externalGameControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalGameControllerDidDisconnect(_:)), name: .externalGameControllerDidDisconnect, object: nil)
}
override func viewDidLoad()
@ -52,8 +64,15 @@ class SettingsViewController: UITableViewController
{
super.viewWillAppear(animated)
if let indexPath = self.tableView.indexPathForSelectedRow
if let indexPath = self.previousSelectedRowIndexPath
{
if indexPath.section == Section.controllers.rawValue
{
// Update and temporarily re-select selected row.
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableViewScrollPosition.none)
}
self.tableView.deselectRow(at: indexPath, animated: true)
}
}
@ -72,6 +91,8 @@ class SettingsViewController: UITableViewController
let indexPath = self.tableView.indexPath(for: cell)
else { return }
self.previousSelectedRowIndexPath = indexPath
switch segueType
{
case Segue.controllers:
@ -79,13 +100,14 @@ class SettingsViewController: UITableViewController
controllersSettingsViewController.playerIndex = indexPath.row
case Segue.controllerSkins:
let gameTypeControllerSkinsViewController = segue.destination as! GameTypeControllerSkinsViewController
let systemControllerSkinsViewController = segue.destination as! SystemControllerSkinsViewController
switch indexPath.row
let row = ControllerSkinsRow(rawValue: indexPath.row)!
switch row
{
case 0: gameTypeControllerSkinsViewController.gameType = .snes
case 1: gameTypeControllerSkinsViewController.gameType = .gba
default: break
case .snes: systemControllerSkinsViewController.system = .snes
case .gba: systemControllerSkinsViewController.system = .gba
case .gbc: systemControllerSkinsViewController.system = .gbc
}
}
}
@ -100,18 +122,6 @@ private extension SettingsViewController
}
}
private extension SettingsViewController
{
@IBAction func unwindFromControllersSettingsViewController(_ segue: UIStoryboardSegue)
{
let indexPath = self.tableView.indexPathForSelectedRow
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: UITableViewScrollPosition.none)
}
}
private extension SettingsViewController
{
@IBAction func beginChangingControllerOpacity(with sender: UISlider)
@ -143,12 +153,12 @@ private extension SettingsViewController
private extension SettingsViewController
{
dynamic func externalControllerDidConnect(_ notification: Notification)
@objc func externalGameControllerDidConnect(_ notification: Notification)
{
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
}
dynamic func externalControllerDidDisconnect(_ notification: Notification)
@objc func externalGameControllerDidDisconnect(_ notification: Notification)
{
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
}
@ -162,6 +172,7 @@ extension SettingsViewController
switch section
{
case .controllers: return 1 // Temporarily hide other controller indexes until controller logic is finalized
case .controllerSkins: return System.supportedSystems.count
default: return super.tableView(tableView, numberOfRowsInSection: sectionIndex)
}
}
@ -169,24 +180,29 @@ extension SettingsViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = super.tableView(tableView, cellForRowAt: indexPath)
if indexPath.section == Section.controllers.rawValue
let section = Section(rawValue: indexPath.section)!
switch section
{
case .controllers:
if indexPath.row == Settings.localControllerPlayerIndex
{
cell.detailTextLabel?.text = UIDevice.current.name
}
else if let index = ExternalControllerManager.shared.connectedControllers.index(where: { $0.playerIndex == indexPath.row })
else if let index = ExternalGameControllerManager.shared.connectedControllers.index(where: { $0.playerIndex == indexPath.row })
{
let controller = ExternalControllerManager.shared.connectedControllers[index]
let controller = ExternalGameControllerManager.shared.connectedControllers[index]
cell.detailTextLabel?.text = controller.name
}
else
{
cell.detailTextLabel?.text = nil
}
case .controllerSkins: cell.textLabel?.text = System.supportedSystems[indexPath.row].localizedName
default: break
}
return cell
}
@ -194,7 +210,7 @@ extension SettingsViewController
{
let cell = tableView.cellForRow(at: indexPath)
let section = Section(rawValue: indexPath.section)!
switch section
{
case Section.controllers: self.performSegue(withIdentifier: Segue.controllers.rawValue, sender: cell)

View File

@ -60,6 +60,20 @@
<string>com.pkware.zip-archive</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>GBC Game</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.rileytestut.delta.game.gbc</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
@ -82,17 +96,17 @@
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>gamefaqs.net</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.1</string>
</dict>
</dict>
<dict>
<key>gamefaqs.net</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.1</string>
</dict>
</dict>
</dict>
<key>UIFileSharingEnabled</key>
<true/>
@ -112,6 +126,8 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSPhotoLibraryUsageDescription</key>
<string>Press &quot;OK&quot; to allow Delta to use images from your Photo Library as game artwork.</string>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
@ -179,6 +195,38 @@
<string>deltaskin</string>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.rileytestut.delta.game</string>
</array>
<key>UTTypeDescription</key>
<string>GBC Game</string>
<key>UTTypeIdentifier</key>
<string>com.rileytestut.delta.game.gbc</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>gbc</string>
<string>gb</string>
</array>
</dict>
</dict>
</array>
<key>Fabric</key>
<dict>
<key>APIKey</key>
<string>d542629b4f6625cfd5564d27318550321272076d</string>
<key>Kits</key>
<array>
<dict>
<key>KitInfo</key>
<dict/>
<key>KitName</key>
<string>Crashlytics</string>
</dict>
</array>
</dict>
</dict>
</plist>

103
Delta/Systems/System.swift Normal file
View File

@ -0,0 +1,103 @@
//
// System.swift
// Delta
//
// Created by Riley Testut on 4/30/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import DeltaCore
import SNESDeltaCore
import GBADeltaCore
import GBCDeltaCore
extension GameType
{
init?(fileExtension: String)
{
switch fileExtension.lowercased()
{
case "smc", "sfc", "fig": self = .snes
case "gba": self = .gba
case "gbc", "gb": self = .gbc
default: return nil
}
}
}
enum System
{
case snes
case gba
case gbc
static var supportedSystems: [System] {
return [.snes, .gba, .gbc]
}
}
extension System
{
var localizedName: String {
switch self
{
case .snes: return NSLocalizedString("Super Nintendo", comment: "")
case .gba: return NSLocalizedString("Game Boy Advance", comment: "")
case .gbc: return NSLocalizedString("Game Boy Color", comment: "")
}
}
var localizedShortName: String {
switch self
{
case .snes: return NSLocalizedString("SNES", comment: "")
case .gba: return NSLocalizedString("GBA", comment: "")
case .gbc: return NSLocalizedString("GBC", comment: "")
}
}
var year: Int {
switch self
{
case .snes: return 1990
case .gba: return 2001
case .gbc: return 1998
}
}
}
extension System
{
var deltaCore: DeltaCoreProtocol {
switch self
{
case .snes: return SNES.core
case .gba: return GBA.core
case .gbc: return GBC.core
}
}
}
extension System
{
var gameType: GameType {
switch self
{
case .snes: return .snes
case .gba: return .gba
case .gbc: return .gbc
}
}
init?(gameType: GameType)
{
switch gameType
{
case GameType.snes: self = .snes
case GameType.gba: self = .gba
case GameType.gbc: self = .gbc
default: return nil
}
}
}

2
External/Roxas vendored

@ -1 +1 @@
Subproject commit 9143d01afa25f07da8f1cdb5390b49f4efb1a75f
Subproject commit 192682bb389830944d257946d870e1d50ae4c42b

View File

@ -7,4 +7,7 @@ target 'Delta' do
pod 'FileMD5Hash', '~> 2.0.0'
pod 'SQLite.swift', '~> 0.11.0'
pod 'SDWebImage', '~> 3.8'
end
pod 'Fabric', '~> 1.6.0'
pod 'Crashlytics', '~> 3.8.0'
pod 'SMCalloutView'
end

View File

@ -1,22 +1,32 @@
PODS:
- Crashlytics (3.8.6):
- Fabric (~> 1.6.3)
- Fabric (1.6.13)
- FileMD5Hash (2.0.0)
- SDWebImage (3.8.2):
- SDWebImage/Core (= 3.8.2)
- SDWebImage/Core (3.8.2)
- SQLite.swift (0.11.3):
- SQLite.swift/standard (= 0.11.3)
- SQLite.swift/standard (0.11.3)
- SMCalloutView (2.1.5)
- SQLite.swift (0.11.4):
- SQLite.swift/standard (= 0.11.4)
- SQLite.swift/standard (0.11.4)
DEPENDENCIES:
- Crashlytics (~> 3.8.0)
- Fabric (~> 1.6.0)
- FileMD5Hash (~> 2.0.0)
- SDWebImage (~> 3.8)
- SMCalloutView
- SQLite.swift (~> 0.11.0)
SPEC CHECKSUMS:
Crashlytics: 95d05f4e4c19a771250c4bd9ce344d996de32bbf
Fabric: 2fb5676bc811af011a04513451f463dac6803206
FileMD5Hash: 3ed69cc19a21ff4d30ae8833fc104275ad2c7de0
SDWebImage: '098e97e6176540799c27e804c96653ee0833d13c'
SQLite.swift: 99b36c22084427f0abbeb957556ce1528cf10bb3
SMCalloutView: 5c0ee363dc8e7204b2fda17dfad38c93e9e23481
SQLite.swift: 3e3bee21da701b5b9f87c4a672cb54f233505692
PODFILE CHECKSUM: e583277236d810a5e080371e73349e52a4aaa25e
PODFILE CHECKSUM: a1fb0ce1f1bb0e73380460cc4f946d297a4d5ff1
COCOAPODS: 1.2.0
COCOAPODS: 1.2.1

1
Pods/Crashlytics/Crashlytics.framework/README generated vendored Normal file
View File

@ -0,0 +1 @@
We've now combined all our supported platforms into a single podspec. As a result, we moved our submit script to a new location for Cocoapods projects: ${PODS_ROOT}/Crashlytics/submit. To avoid breaking functionality that references the old location of the submit, we've placed this dummy script that calls to the correct location, while providing a helpful warning if it is invoked. This bridge for backwards compatibility will be removed in a future release, so please heed the warning!

6
Pods/Crashlytics/Crashlytics.framework/submit generated vendored Executable file
View File

@ -0,0 +1,6 @@
if [[ -z $PODS_ROOT ]]; then
echo "error: The submit binary delivered by cocoapods is in a new location, under '$"{"PODS_ROOT"}"/Crashlytics/submit'. This script was put in place for backwards compatibility, but it relies on PODS_ROOT, which does not have a value in your current setup. Please update the path to the submit binary to fix this issue."
else
echo "warning: The submit script is now located at '$"{"PODS_ROOT"}"/Crashlytics/submit'. To remove this warning, update your path to point to this new location."
sh "${PODS_ROOT}/Crashlytics/submit" "$@"
fi

39
Pods/Crashlytics/README.md generated Normal file
View File

@ -0,0 +1,39 @@
![Crashlytics Header](https://docs.fabric.io/ios/cocoapod-readmes/cocoapods-crashlytics-header.png)
Part of [Google Fabric](https://get.fabric.io), [Crashlytics](http://try.crashlytics.com/) offers the most powerful, yet lightest weight crash reporting solution for iOS. Crashlytics also provides real-time analytics through [Answers](https://answers.io/) and app distributions to testers using [Beta](http://try.crashlytics.com/beta/).
## Setup
1. Visit [https://fabric.io/sign_up](https://fabric.io/sign_up) to create your Fabric account and to download Fabric.app.
1. Open Fabric.app, login and select the Crashlytics SDK.
![Fabric Plugin](https://docs.fabric.io/ios/cocoapod-readmes/cocoapods-fabric-plugin.png)
1. The Fabric app automatically detects when a project uses CocoaPods and gives you the option to install via the Podfile or Xcode.
![Fabric Installation Options](https://docs.fabric.io/ios/cocoapod-readmes/cocoapods-pod-installation-option.png)
1. Select the Podfile option and follow the installation instructions to update your Podfile. **Note:** the Crashlytics Pod includes Answers. If you have Answers included as a separate Pod it should be removed from your Podfile to avoid duplicate symbol errors.
```
pod 'Fabric'
pod 'Crashlytics'
```
1. Run `pod install`
1. Add a Run Script Build Phase and build your app.
![Fabric Run Script Build Phase](https://docs.fabric.io/ios/cocoapod-readmes/cocoapods-rsbp.png)
1. Initialize the SDK by inserting code outlined in the Fabric.app.
1. Run your app to finish the installation.
## Resources
* [Documentation](https://docs.fabric.io/apple/crashlytics/overview.html)
* [Forums](https://stackoverflow.com/questions/tagged/google-fabric)
* [Website](http://try.crashlytics.com/)
* Follow us on Twitter: [@fabric](https://twitter.com/fabric) and [@crashlytics](https://twitter.com/crashlytics)

BIN
Pods/Crashlytics/iOS/Crashlytics.framework/Crashlytics generated vendored Executable file

Binary file not shown.

View File

@ -0,0 +1,31 @@
//
// ANSCompatibility.h
// AnswersKit
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#pragma once
#if !__has_feature(nullability)
#define nonnull
#define nullable
#define _Nullable
#define _Nonnull
#endif
#ifndef NS_ASSUME_NONNULL_BEGIN
#define NS_ASSUME_NONNULL_BEGIN
#endif
#ifndef NS_ASSUME_NONNULL_END
#define NS_ASSUME_NONNULL_END
#endif
#if __has_feature(objc_generics)
#define ANS_GENERIC_NSARRAY(type) NSArray<type>
#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary<key_type, object_key>
#else
#define ANS_GENERIC_NSARRAY(type) NSArray
#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
#endif

View File

@ -0,0 +1,210 @@
//
// Answers.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "ANSCompatibility.h"
NS_ASSUME_NONNULL_BEGIN
/**
* This class exposes the Answers Events API, allowing you to track key
* user user actions and metrics in your app.
*/
@interface Answers : NSObject
/**
* Log a Sign Up event to see users signing up for your app in real-time, understand how
* many users are signing up with different methods and their success rate signing up.
*
* @param signUpMethodOrNil The method by which a user logged in, e.g. Twitter or Digits.
* @param signUpSucceededOrNil The ultimate success or failure of the login
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logSignUpWithMethod:(nullable NSString *)signUpMethodOrNil
success:(nullable NSNumber *)signUpSucceededOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Log In event to see users logging into your app in real-time, understand how many
* users are logging in with different methods and their success rate logging into your app.
*
* @param loginMethodOrNil The method by which a user logged in, e.g. email, Twitter or Digits.
* @param loginSucceededOrNil The ultimate success or failure of the login
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logLoginWithMethod:(nullable NSString *)loginMethodOrNil
success:(nullable NSNumber *)loginSucceededOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Share event to see users sharing from your app in real-time, letting you
* understand what content they're sharing from the type or genre down to the specific id.
*
* @param shareMethodOrNil The method by which a user shared, e.g. email, Twitter, SMS.
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logShareWithMethod:(nullable NSString *)shareMethodOrNil
contentName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Invite Event to track how users are inviting other users into
* your application.
*
* @param inviteMethodOrNil The method of invitation, e.g. GameCenter, Twitter, email.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logInviteWithMethod:(nullable NSString *)inviteMethodOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Purchase event to see your revenue in real-time, understand how many users are making purchases, see which
* items are most popular, and track plenty of other important purchase-related metrics.
*
* @param itemPriceOrNil The purchased item's price.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param purchaseSucceededOrNil Was the purchase succesful or unsuccesful
* @param itemNameOrNil The human-readable form of the item's name. Example:
* @param itemTypeOrNil The type, or genre of the item. Example: Song
* @param itemIdOrNil The machine-readable, unique item identifier Example: SKU
* @param customAttributesOrNil A dictionary of custom attributes to associate with this purchase.
*/
+ (void)logPurchaseWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil
currency:(nullable NSString *)currencyOrNil
success:(nullable NSNumber *)purchaseSucceededOrNil
itemName:(nullable NSString *)itemNameOrNil
itemType:(nullable NSString *)itemTypeOrNil
itemId:(nullable NSString *)itemIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Level Start Event to track where users are in your game.
*
* @param levelNameOrNil The level name
* @param customAttributesOrNil A dictionary of custom attributes to associate with this level start event.
*/
+ (void)logLevelStart:(nullable NSString *)levelNameOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Level End event to track how users are completing levels in your game.
*
* @param levelNameOrNil The name of the level completed, E.G. "1" or "Training"
* @param scoreOrNil The score the user completed the level with.
* @param levelCompletedSuccesfullyOrNil A boolean representing whether or not the level was completed succesfully.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logLevelEnd:(nullable NSString *)levelNameOrNil
score:(nullable NSNumber *)scoreOrNil
success:(nullable NSNumber *)levelCompletedSuccesfullyOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Add to Cart event to see users adding items to a shopping cart in real-time, understand how
* many users start the purchase flow, see which items are most popular, and track plenty of other important
* purchase-related metrics.
*
* @param itemPriceOrNil The purchased item's price.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param itemNameOrNil The human-readable form of the item's name. Example:
* @param itemTypeOrNil The type, or genre of the item. Example: Song
* @param itemIdOrNil The machine-readable, unique item identifier Example: SKU
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logAddToCartWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil
currency:(nullable NSString *)currencyOrNil
itemName:(nullable NSString *)itemNameOrNil
itemType:(nullable NSString *)itemTypeOrNil
itemId:(nullable NSString *)itemIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Start Checkout event to see users moving through the purchase funnel in real-time, understand how many
* users are doing this and how much they're spending per checkout, and see how it related to other important
* purchase-related metrics.
*
* @param totalPriceOrNil The total price of the cart.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param itemCountOrNil The number of items in the cart.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logStartCheckoutWithPrice:(nullable NSDecimalNumber *)totalPriceOrNil
currency:(nullable NSString *)currencyOrNil
itemCount:(nullable NSNumber *)itemCountOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Rating event to see users rating content within your app in real-time and understand what
* content is most engaging, from the type or genre down to the specific id.
*
* @param ratingOrNil The integer rating given by the user.
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logRating:(nullable NSNumber *)ratingOrNil
contentName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Content View event to see users viewing content within your app in real-time and
* understand what content is most engaging, from the type or genre down to the specific id.
*
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logContentViewWithName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Search event allows you to see users searching within your app in real-time and understand
* exactly what they're searching for.
*
* @param queryOrNil The user's query.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logSearchWithQuery:(nullable NSString *)queryOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Custom Event to see user actions that are uniquely important for your app in real-time, to see how often
* they're performing these actions with breakdowns by different categories you add. Use a human-readable name for
* the name of the event, since this is how the event will appear in Answers.
*
* @param eventName The human-readable name for the event.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event. Attribute keys
* must be <code>NSString</code> and and values must be <code>NSNumber</code> or <code>NSString</code>.
* @discussion How we treat <code>NSNumbers</code>:
* We will provide information about the distribution of values over time.
*
* How we treat <code>NSStrings</code>:
* NSStrings are used as categorical data, allowing comparison across different category values.
* Strings are limited to a maximum length of 100 characters, attributes over this length will be
* truncated.
*
* When tracking the Tweet views to better understand user engagement, sending the tweet's length
* and the type of media present in the tweet allows you to track how tweet length and the type of media influence
* engagement.
*/
+ (void)logCustomEventWithName:(NSString *)eventName
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,33 @@
//
// CLSAttributes.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#pragma once
#define CLS_DEPRECATED(x) __attribute__ ((deprecated(x)))
#if !__has_feature(nullability)
#define nonnull
#define nullable
#define _Nullable
#define _Nonnull
#endif
#ifndef NS_ASSUME_NONNULL_BEGIN
#define NS_ASSUME_NONNULL_BEGIN
#endif
#ifndef NS_ASSUME_NONNULL_END
#define NS_ASSUME_NONNULL_END
#endif
#if __has_feature(objc_generics)
#define CLS_GENERIC_NSARRAY(type) NSArray<type>
#define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary<key_type, object_key>
#else
#define CLS_GENERIC_NSARRAY(type) NSArray
#define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
#endif

View File

@ -0,0 +1,64 @@
//
// CLSLogging.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#ifdef __OBJC__
#import "CLSAttributes.h"
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#endif
/**
*
* The CLS_LOG macro provides as easy way to gather more information in your log messages that are
* sent with your crash data. CLS_LOG prepends your custom log message with the function name and
* line number where the macro was used. If your app was built with the DEBUG preprocessor macro
* defined CLS_LOG uses the CLSNSLog function which forwards your log message to NSLog and CLSLog.
* If the DEBUG preprocessor macro is not defined CLS_LOG uses CLSLog only.
*
* Example output:
* -[AppDelegate login:] line 134 $ login start
*
* If you would like to change this macro, create a new header file, unset our define and then define
* your own version. Make sure this new header file is imported after the Crashlytics header file.
*
* #undef CLS_LOG
* #define CLS_LOG(__FORMAT__, ...) CLSNSLog...
*
**/
#ifdef __OBJC__
#ifdef DEBUG
#define CLS_LOG(__FORMAT__, ...) CLSNSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#define CLS_LOG(__FORMAT__, ...) CLSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#endif
#endif
/**
*
* Add logging that will be sent with your crash data. This logging will not show up in the system.log
* and will only be visible in your Crashlytics dashboard.
*
**/
#ifdef __OBJC__
OBJC_EXTERN void CLSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
OBJC_EXTERN void CLSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0);
/**
*
* Add logging that will be sent with your crash data. This logging will show up in the system.log
* and your Crashlytics dashboard. It is not recommended for Release builds.
*
**/
OBJC_EXTERN void CLSNSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
OBJC_EXTERN void CLSNSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0);
NS_ASSUME_NONNULL_END
#endif

View File

@ -0,0 +1,103 @@
//
// CLSReport.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "CLSAttributes.h"
NS_ASSUME_NONNULL_BEGIN
/**
* The CLSCrashReport protocol is deprecated. See the CLSReport class and the CrashyticsDelegate changes for details.
**/
@protocol CLSCrashReport <NSObject>
@property (nonatomic, copy, readonly) NSString *identifier;
@property (nonatomic, copy, readonly) NSDictionary *customKeys;
@property (nonatomic, copy, readonly) NSString *bundleVersion;
@property (nonatomic, copy, readonly) NSString *bundleShortVersionString;
@property (nonatomic, readonly, nullable) NSDate *crashedOnDate;
@property (nonatomic, copy, readonly) NSString *OSVersion;
@property (nonatomic, copy, readonly) NSString *OSBuildVersion;
@end
/**
* The CLSReport exposes an interface to the phsyical report that Crashlytics has created. You can
* use this class to get information about the event, and can also set some values after the
* event has occured.
**/
@interface CLSReport : NSObject <CLSCrashReport>
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
/**
* Returns the session identifier for the report.
**/
@property (nonatomic, copy, readonly) NSString *identifier;
/**
* Returns the custom key value data for the report.
**/
@property (nonatomic, copy, readonly) NSDictionary *customKeys;
/**
* Returns the CFBundleVersion of the application that generated the report.
**/
@property (nonatomic, copy, readonly) NSString *bundleVersion;
/**
* Returns the CFBundleShortVersionString of the application that generated the report.
**/
@property (nonatomic, copy, readonly) NSString *bundleShortVersionString;
/**
* Returns the date that the report was created.
**/
@property (nonatomic, copy, readonly) NSDate *dateCreated;
/**
* Returns the os version that the application crashed on.
**/
@property (nonatomic, copy, readonly) NSString *OSVersion;
/**
* Returns the os build version that the application crashed on.
**/
@property (nonatomic, copy, readonly) NSString *OSBuildVersion;
/**
* Returns YES if the report contains any crash information, otherwise returns NO.
**/
@property (nonatomic, assign, readonly) BOOL isCrash;
/**
* You can use this method to set, after the event, additional custom keys. The rules
* and semantics for this method are the same as those documented in Crashlytics.h. Be aware
* that the maximum size and count of custom keys is still enforced, and you can overwrite keys
* and/or cause excess keys to be deleted by using this method.
**/
- (void)setObjectValue:(nullable id)value forKey:(NSString *)key;
/**
* Record an application-specific user identifier. See Crashlytics.h for details.
**/
@property (nonatomic, copy, nullable) NSString * userIdentifier;
/**
* Record a user name. See Crashlytics.h for details.
**/
@property (nonatomic, copy, nullable) NSString * userName;
/**
* Record a user email. See Crashlytics.h for details.
**/
@property (nonatomic, copy, nullable) NSString * userEmail;
@end
NS_ASSUME_NONNULL_END

Some files were not shown because too many files have changed in this diff Show More