Compare commits

...

511 Commits

Author SHA1 Message Date
忆海16
7399e8cd5c xibme 2024-05-29 14:55:03 +08:00
忆海16
8161aa6d8c 第一次提交 2024-05-27 11:54:20 +08:00
忆海16
18c51d8d1b 第一次提交 2024-05-27 11:53:32 +08:00
Riley Testut
af01b97faf Fixes N64 games crashing when compiled with Xcode 15
# Conflicts:
#	Delta.xcodeproj/project.pbxproj
2023-09-18 16:27:22 -05:00
Riley Testut
62ebf05a8f Updates wording of “Database Repaired” alert to mention conflicts.txt 2023-08-11 21:52:01 -05:00
Riley Testut
452e3dab06 Writes text file of possibly corrupted save files to Files apps instead of actually conflicting them
In practice, the number of conflicts was far too high for the number of save files that actually matter.
2023-08-11 21:51:32 -05:00
Riley Testut
43fedf6fcc Fixes compiling with Xcode 14 2023-08-11 21:37:21 -05:00
Riley Testut
9ab8cf29b6 Updates app version to 1.5b4 2023-08-11 18:57:28 -05:00
Riley Testut
e6cd5475e9 Replaces Logger.debug() usage with Logger.notice()
Debug logs aren’t exported from release builds, so we now use `notice` level instead.
2023-08-11 18:57:28 -05:00
Riley Testut
9c31b8a864 Updates wording of “Database Repaired” alert 2023-08-11 18:57:28 -05:00
Riley Testut
5c7e3cc5b9 [ExpFeat] Adds filter button to change RepairSaveStatesViewController’s date range
* Recent (Last month)
* All
2023-08-11 18:57:28 -05:00
Riley Testut
d5f910ff00 Adds missing throws to SaveState.awakeFromSync() 2023-08-11 18:57:28 -05:00
Riley Testut
d1643dbc8f Throws SyncValidationError when downloading corrupted versions of “verified” SaveStates
After reviewing save states upon first launch, Delta will upload the verified game ID as metadata to ensure other devices don’t download remote versions with incorrect relationships.
2023-08-11 18:57:27 -05:00
Riley Testut
dc3a5b479c [Contributors] Credits Chris Rittenhouse for Alternate App Icons 2023-08-11 15:58:43 -05:00
Riley Testut
b3d8dbc554 [ExpFeat] Fixes Controller Skin alternate app icon filename 2023-08-11 15:57:39 -05:00
Riley Testut
f3534e4415 [ExpFeat] Alternate App Icons (#259)
commit 34ed9726034adbb439515ccb3d576311603bfe2b
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Thu Aug 3 15:31:47 2023 -0400

    Removes `experimentalFeatures` property from `Settings`

commit 4e31d22d56d68439a340707d2fdbac3763df1e3a
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Thu Aug 3 15:17:47 2023 -0400

    Reorder icons

commit 7509eaa29946b622ce0ad920a740f0212db5e771
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Thu Aug 3 15:17:24 2023 -0400

    Moves icon changing code to `AlternateAppIcons`

commit 84821ef99ded74c066563041618aec3c4acb30d7
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Thu Aug 3 14:33:15 2023 -0400

    Fixes icon authors

commit 0b821830f09b434fa375c37a2504c21ee5f90bdb
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Fri Jul 21 01:44:00 2023 -0400

    Adds comments for functional alternate app icon code

commit e8dd6165619bc143f13dde51e5160af2695ef17e
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Fri Jul 21 01:30:03 2023 -0400

    Adds implementation for changing app icon

commit 1e07483d18d1e865804ae34d6011844b63cb33cc
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Fri Jul 21 01:28:24 2023 -0400

    Adds Alternate App Icon experimental feature and options

commit 46ed2ce3ae9593d4cc95f29d6caa09f33f2ce62d
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Fri Jul 21 01:26:51 2023 -0400

    Adds extension to handle retrieving app icon images

commit 9dddb5f98f4523e7877f97c66a5ae95e6513cc09
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Fri Jul 21 01:24:19 2023 -0400

    Adds experimental features as a property of `Settings`

commit d74038fd418a0e6fcd45942cb67e722904fc3e24
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Fri Jul 21 01:23:02 2023 -0400

    Adds alternate icon assets

Co-authored-by: Chris Rittenhouse <dev@litritt.com>
2023-08-10 19:46:08 -05:00
Riley Testut
061f5abd3e Allows explicitly downloading corrupted record versions from RecordVersionsViewController 2023-08-10 19:33:27 -05:00
Riley Testut
be047b28a6 Fixes crash when restoring remote record without local version 2023-08-10 19:33:27 -05:00
Riley Testut
fcdd3c7840 Throws SyncValidationError when downloading corrupted Game or GameSave record 2023-08-10 19:33:27 -05:00
Riley Testut
ca8c2cb8c5 [ExpFeat] Adds “Review Save States” to fix save states associated with wrong game 2023-08-10 19:33:27 -05:00
Riley Testut
a9f15144ed Repairs corrupted Game, GameSave, and SaveState relationships on initial launch
Automatically fixes Game and GameSaves, but requires user to manually review + fix all recent SaveStates.
2023-08-10 19:33:27 -05:00
Riley Testut
a80ac04650 Fixes corrupted Harmony database relationships after heavyweight migration 2023-08-10 19:33:26 -05:00
Riley Testut
25e237cfcb [Delta Sync] Uses local modification date when uploading changes
Previously we used the server’s modification dates, which could be confusing when comparing against local modification dates since the same version may have different dates.
2023-07-10 19:00:39 -05:00
Riley Testut
3227ee4c49 Migrates Core Data model from v6 to v7 2023-07-10 16:09:07 -05:00
Riley Testut
45ed97c255 Actually fixes crash loading save states on iOS 17
The underlying issue causing the crash is that we were returning cached *supplementary view* layout attributes by accident from layoutAttributesForItem(at:).

Now, we only cache layout attributes for cells.
2023-07-10 15:39:31 -05:00
Riley Testut
31578e2e34 Updated mogenerator target to use Homebrew version 2023-07-07 18:22:33 -05:00
Riley Testut
e33a7c662f [Delta Sync] Delays sync until after interactive Settings dismissal has completed
Avoids false-positives when user scrolls to top of a Settings screen.
2023-07-07 18:22:33 -05:00
Riley Testut
8ea40a4728 [Delta Sync] Fixes queued syncs sometimes fetching outdated changes
Rare, but more common when performing initial sync.
2023-07-07 18:22:32 -05:00
Riley Testut
29f152fcb3 [Delta Sync] Repairs references to remote files when signing out then back in
Fixes redundant uploads and potentially ophaned remote files, the latter due to no remote files being deleted if LocalRecord.remoteFiles is empty.
2023-07-07 18:22:26 -05:00
Riley Testut
5779927831 [Delta Sync] Delays seeding Harmony database until initial sync 2023-07-07 18:19:07 -05:00
Riley Testut
f184639c6b [Delta Sync] Fixes incorrect mass conflicts when signing out then back in 2023-07-07 18:17:13 -05:00
Riley Testut
043fb923ae [Delta Sync] Displays activity indicator when signing-in
Without an indicator it can feel like the app has froze, when really it’s just preparing to sync.
2023-07-07 14:59:41 -05:00
Riley Testut
19fb333a67 Toggles Delta Sync switch back on if user cancels “Disable Syncing?” warning alert 2023-07-06 19:11:09 -05:00
Riley Testut
35a8f90a1c Converts Delta Sync action sheets into alerts
Implicitly fixes crashing on iPad.
2023-07-06 18:44:18 -05:00
Riley Testut
c898f72847 Fixes “file does not exist” sync error after restoring previous version
Due to a bug, it was possible for Harmony records to contain duplicate file entries after restoring a previous version. Harmony would then try to download _all_ files, including the duplicates, and would fail if any of them no longer exist.

These changes fix the underlying bug, and also fix existing corrupted records on the fly.
2023-06-28 13:45:44 -05:00
Riley Testut
981c868f6e Fixes “Harmony.sqlite couldn’t be opened” sync error when there are more than 1000 games 2023-06-28 13:37:20 -05:00
Riley Testut
15e228f287 Adds GameSave.sha1 to sync hash between devices
Fixes redundant record uploads post-sync due to comparing hashes against locally-cached hash via extended attributes.
2023-06-28 13:33:16 -05:00
Riley Testut
707116a39b Automatically resolves Cheat + ControllerSkin sync conflicts 2023-06-27 18:58:41 -05:00
Riley Testut
750740ac16 Updates app version to 1.5b3 (69) 2023-06-22 13:11:13 -05:00
Riley Testut
6909b6248f Skips checksum verification when importing zipped games
Verification can take an annoyingly long time, especially for larger ROMs.
2023-06-19 19:19:40 -05:00
Riley Testut
8bd6fe1e11 Replaces UIDocumentBrowserViewController with UIDocumentPickerViewController on iOS 17+
Prior to iOS 17, UIDocumentPickerViewController was too buggy to reliably use with iCloud Drive.
2023-06-19 19:19:40 -05:00
Riley Testut
4cf705f141 [ExpFeat] Fixes saving screenshots to Photos as JPEGs 2023-06-16 16:57:00 -05:00
Riley Testut
99417b418a [ExpFeat] Adds missing @unknown default to PHAuthorizationStatus switch statement 2023-06-16 16:46:35 -05:00
Riley Testut
648e9b8393 [ExpFeat] Fixes saving screenshots to Photos with .limited authorization 2023-06-16 16:44:24 -05:00
Riley Testut
69eff8fa28 Fixes displaying system name in dark text (again) when remapping inputs on iOS 17 2023-06-16 15:55:51 -05:00
Riley Testut
3e858c652f Fixes crash loading save states on iOS 17
Crashes due to GridCollectionViewLayout returning outdated cached layout information when inserting “Auto” save states section for the first time.

To fix this, we now clear the layout cache in invalidateLayout(with:) when relevant.
2023-06-16 15:25:49 -05:00
Riley Testut
21147969ea Fixes GridCollectionViewCell "Unable to simultaneously satisfy constraints" runtime error 2023-06-16 15:18:02 -05:00
Riley Testut
731de7023f Merge branch 'app_store' into develop 2023-06-16 15:14:39 -05:00
Riley Testut
810bc4572c Replaces app icon variants with single 1024x1024 version 2023-06-16 15:11:59 -05:00
Riley Testut
4e8580b8f5 Obfuscates private API usage to pass TestFlight App Review 2023-06-16 15:04:47 -05:00
Riley Testut
2e21141bc6 [Pods] Removes Fabric + Crashlytics 2023-05-10 13:36:44 -05:00
Riley Testut
08a40b3516 [Pods] Updates GoogleSignIn dependency to 5.0.2
* GoogleSignIn (4.4.0 -> 5.0.2)
* GTMSessionFetcher (1.5.0 -> 1.7.2)
* GoogleToolboxForMac (Removed)
2023-05-10 13:30:30 -05:00
Riley Testut
7b9ab2488e Fixes accidentally deleting Games directory during sync in rare circumstances
If a Game with empty filename happens to be deleted (e.g. during sync merge), it will accidentally delete the *entire* Games folder due to its fileURL being “Games/[empty]”.

To prevent this, we now explicitly check that a Game’s identifier isn’t empty AND that that Game.fileURL doesn’t point to a directory before deleting local files.
2023-05-02 14:29:23 -05:00
Riley Testut
00121bd31f [iPad] Fixes crash when resolving sync merge conflicts 2023-05-02 14:29:23 -05:00
Riley Testut
ea260cb8a6 Updates Settings to use large titles where appropriate
* SettingsViewController
* PreferredControllerSkinsViewController
2023-05-02 14:29:23 -05:00
Riley Testut
b0bd5ba906 Updates most Settings view controllers to use .insetGrouped style
Excludes PreferredControllerSkinsViewController and ControllerSkinsViewController
2023-04-28 17:22:20 -05:00
Riley Testut
726c4ab93b Resolves Settings.storyboard ambiguity warnings
Expands static UITableViewController subclasses to show all static cells.

Wow I wish I knew this years ago…
2023-04-28 16:54:21 -05:00
Riley Testut
5dc91c87c6 Updates app version to 1.5b2 2023-04-28 16:04:51 -05:00
Riley Testut
de7a812cbd [Contributors] Credits Chris Rittenhouse for experimental features
* Show Status Bar
* Game Screenshots
* Toast Notifications
2023-04-28 15:56:51 -05:00
Riley Testut
b1a3a5076f Merge branch 'experimental_features' into develop
# Conflicts:
#	Delta.xcodeproj/project.pbxproj
#	Delta/Emulation/GameViewController.swift
#	Delta/Pause Menu/PauseViewController.swift
2023-04-28 15:51:08 -05:00
Riley Testut
9470caf83a Sorts Delta’s Info.plist items alphabetically 2023-04-28 15:42:22 -05:00
Riley Testut
bdee5d17a5 Updates Xcode project file format
* Adds DEVELOPMENT_TEAM to Delta target
* Updates LD_RUNPATH_SEARCH_PATHS format
2023-04-28 15:42:22 -05:00
Riley Testut
85d53162c1 Fixes internal location of CharacterSet+Filename.swift in Xcode project 2023-04-28 15:42:22 -05:00
Riley Testut
75814ca04d [Experimental Feature] Adds VariableFastForward.allowUnrestrictedSpeeds @Option
When enabled, the user can choose any integer speed from 1x to 8x for their preferred Fast Forward speed, regardless of the system’s maximumFastForwardSpeed.
2023-04-28 15:42:22 -05:00
Riley Testut
2ead48ad40 [Features] Supports dynamic @Option values that may change over time
Uses @autoclosure to keep call site the same, but allows a single picker @Option to show different values depending on other factors (e.g. other @Options).
2023-04-28 15:42:11 -05:00
Riley Testut
6fd7f9e1d5 [Experimental Feature] Implements VariableFastForward feature 2023-04-28 15:42:10 -05:00
Riley Testut
7fceccc114 [Experimental Feature] Toast Notifications (#244)
commit c340cf842fbf5fea476a6637efe4928dbd734eba
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Thu Apr 27 17:24:30 2023 -0400

    Addresses Riley's requested changes
    - Minor code structure change in extension
    - Minor changes to text and phrasing

commit 2a928dfa637dfb503e861dc863f6f85f5240941a
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Wed Apr 26 20:11:38 2023 -0400

    Adds implementation for Toast Notifications Experimental Feature

commit 4fa7d139669994eff888c41bf7af9ac0b6cd2a75
Author: Chris Rittenhouse <dev@litritt.com>
Date:   Wed Apr 26 20:11:04 2023 -0400

    Adds @Feature and @Options for Toast Notifications Experimental Feature

Co-authored-by: Chris Rittenhouse <dev@litritt.com>
2023-04-28 15:32:13 -05:00
Chris Rittenhouse
6bdc05f640
[Experimental Feature] Game Screenshots (#242)
* Adds extension to handle interactions with Photos app
- isAuthorized to check/ask for authorization
- saveUIImage to save a UIImage to Photos

* Adds a Pause Menu button for taking game screenshots

* Adds @Feature and @Options for Game Screenshots

* Implements Game Screenshots feature in GameViewController

* Updates project settings

* Passes call to save to Photos as a closure into authorization prompt
- Ensures that the screenshot is saved when the user is first prompted for access to Photos
- More elegant extension code

---------

Co-authored-by: Riley Testut <riley@rileytestut.com>
2023-04-28 14:35:26 -05:00
Chris Rittenhouse
05e94902b8
[Experimental Feature] Show Status Bar (#241)
* Adds @Feature for Show Status Bar
* Shows the Status Bar if the @Feature is enabled
* Forces light content on Status Bar
2023-04-28 14:26:57 -05:00
Riley Testut
233ef7d418 Removes accidental togglePreviewsEnabled: action selector from isAltJITEnabled switch 2023-04-27 16:39:13 -05:00
Riley Testut
5cef975a9e Fixes incorrect controller skin appearance after rotating device 2023-04-27 15:20:14 -05:00
Riley Testut
39522fda58 [Experimental Feature] Supports AirPlay controller skins
Allows users to customize controller skin when AirPlaying games to an external display.
2023-04-27 14:58:51 -05:00
Riley Testut
1137189b57 Adds Nintendo DS AirPlay settings to customize how games appear on TV
* Top Screen Only = Shows just the top screen on TV
* Layout Horizontally = Places screens side-by-side on TV (instead of stacked vertically)
2023-04-26 16:07:17 -05:00
Riley Testut
32e7c1f93e Supports AirPlaying games to external displays 2023-04-26 14:56:45 -05:00
Riley Testut
7ad5df1949 [Features] Fixes potential crash when handling .settingsDidChange Notification
Handling .settingsDidChange notification in the same run loop as changing an @Option’s value can result in exclusive memory access violation due to accessing Option.settingsKey. As a workaround, we delay posting the notification until a later run loop via Task.
2023-04-25 15:14:16 -05:00
Riley Testut
77b26210ab [Experimental Features] Adds (unimplemented) VariableFastForward feature 2023-04-21 14:51:45 -05:00
Riley Testut
20749c5419 Adds “Experimental Features” section to SettingsViewController 2023-04-21 14:51:45 -05:00
Riley Testut
14adb41ff8 [Previews] Adds example “Random Dancing” experimental feature 2023-04-21 14:51:45 -05:00
Riley Testut
8efefd19a0 [Previews] Adds example “Custom Tint Color” feature to preview ExperimentalFeaturesView 2023-04-21 14:51:45 -05:00
Riley Testut
bd0c72e847 Adds ExperimentalFeaturesView to browse and configure experimental features 2023-04-21 14:03:10 -05:00
Riley Testut
5b4f9ea593 [Features] Removes redundant public access modifier from SettingsUserInfoKey constants 2023-04-21 13:43:24 -05:00
Riley Testut
ef47d78c64 [Features] Adds displayInline(_:) View modifier
Controls whether @Option detailView will be displayed inline in the Feature detail view in Delta’s settings, or as a full-screen view that’s pushed onto navigation stack when tapped.
2023-04-14 18:23:34 -05:00
Riley Testut
9f40223e6c [Features] Provides default toggle view for Bool @Options 2023-04-14 18:15:26 -05:00
Riley Testut
6d95924145 [Features] Provides default picker view for @Options with pre-set values
To use, pass in a collection of values to `values` parameter in @Option initializer.
2023-04-14 18:10:55 -05:00
Riley Testut
240b74de94 [Features] Supports user-facing @Options with custom SwiftUI views
@Options with non-nil names will be exposed in Delta’s settings and can be configured by users via provided SwiftUI view.
2023-04-14 16:41:16 -05:00
Riley Testut
4d30ef2929 [Features] Supports @Feature-specific settings via @Option property wrapper 2023-04-14 16:17:31 -05:00
Riley Testut
415450a943 [Features] Adds UserDefaults+OptionValues to read/write arbitrary OptionValues 2023-04-14 15:38:19 -05:00
Riley Testut
80a9132ff5 [Features] Adds OptionValue + LocalizedOptionValue 2023-04-13 17:22:18 -05:00
Riley Testut
d613cc9ad7 [Features] Adds @Feature property wrapper
Simplifies defining feature flags that can enabled + disabled via UserDefaults.
2023-04-13 16:20:54 -05:00
Riley Testut
3fb7e8b4b7 [Features] Moves Notification.Name.settingsDidChange to DeltaFeatures
Re-exports as Settings.didChangeNotification to allow accessing without explicitly importing DeltaFeatures.
2023-04-13 15:57:25 -05:00
Riley Testut
9406cfe6cc [Features] Moves Settings.Name + Settings.NotificationUserInfoKey to DeltaFeatures
Also refactors from enums to RawRepresentable structs to support arbitrary values.
2023-04-13 15:43:43 -05:00
Riley Testut
c3af9f7209 [Features] Adds DeltaFeatures target 2023-04-13 15:08:17 -05:00
Riley Testut
af0d239de1 [Previews] Links target with DeltaCore and Roxas pods 2023-04-13 15:00:33 -05:00
Riley Testut
ff65b15277 [Pods] Updates Cocoapods to 1.12 2023-04-13 14:59:24 -05:00
Riley Testut
91d40cbeea [Previews] Adds DeltaPreviews target for previewing SwiftUI views
The main Delta target cannot use SwiftUI Previews due to linker errors with Systems.framework, so we’ve made a separate target specifically for previewing SwiftUI views.
2023-04-13 14:58:58 -05:00
Riley Testut
21f628fd1e [Contributors] Credits Ian Clawson for Local Multiplayer 2023-02-28 19:00:51 -06:00
Riley Testut
f5b124b175 Updates app version to 1.5b1 2023-02-28 18:52:51 -06:00
Riley Testut
10af836105 Supports local multiplayer (up to 4 players)
Heavily based on Ian Clawson’s PRs, including #128
2023-02-28 18:49:32 -06:00
Riley Testut
5a019e5950 Optimizes default input mappings for Joy-Cons, Switch Online, and other non-MFi controllers
* Uses Home button as Menu button when possible
* Supports “Start/Select” buttons for Switch NES controllers
* Rotates single JoyCon face buttons 90º
* Re-maps single JoyCon control stick from dPad to leftAnalogStick
* Re-maps L1/R1/L2/R2 buttons for N64 core
2023-02-28 18:12:14 -06:00
Riley Testut
c8860c6aaa Raises deployment target to iOS 14.0 2023-02-28 17:31:28 -06:00
Riley Testut
fb0975f0d6 Updates app version to 1.4 2023-02-28 17:31:28 -06:00
Riley Testut
dd314a12af Automatically disables AltJIT for non-BETA builds 2023-02-27 12:51:14 -06:00
Riley Testut
68ad2185dd Hides AltJIT setting for non-BETA builds 2023-02-27 12:46:04 -06:00
Riley Testut
66e5258368 Updates Chris Rittenhouse credits
Replaces GitHub link with personal website, and fixes incorrect Twitter handle.
2023-02-06 15:35:05 -06:00
Riley Testut
fcc19ae830 Updates app version to 1.4rc 2023-02-06 15:15:58 -06:00
Riley Testut
6a683be907 Replaces cheatbase.sqlite with trimmed cheatbase.zip
CheatBase now only contains two tables, CHEATS and CHEAT_CATEGORIES.

Delta unzips SQLite database from cheatbase.zip on launch whenever CheatBase.cheatsVersion changes.
2023-02-06 15:11:27 -06:00
Riley Testut
d79002ea6b Changes cheat activation alert title to “How to Activate" 2023-02-06 14:47:06 -06:00
Riley Testut
913cb788a2 Improves CheatBase error messages 2023-02-06 14:45:48 -06:00
Riley Testut
45665138b2 Adds “Contributors” section to Credits
Lists everyone who has contributed to Delta in some way besides the core team, as well as what they contributed.

Moves Grant Gliner and Chris Rittenhouse to Contributors from main Credits.
2023-02-06 14:35:43 -06:00
Riley Testut
d1c45c9ad0 Adds Shane Gill to Credits 2023-02-06 13:53:15 -06:00
Riley Testut
d31229001f Fixed incorrect font size for Credits section in Settings
All labels should use 17pt system font, but Credits used 16pt by accident
2023-02-06 13:41:48 -06:00
Riley Testut
c3a83d7542 Updates app version to 1.4b5 2023-01-31 14:36:38 -06:00
Riley Testut
5bc2f08084 Limits “Search CheatBase” option to DS games
CheatBase currently only contains cheats for DS games, so no use showing option for other systems (yet).
2023-01-31 14:36:38 -06:00
Riley Testut
041cce64b0 Prefetches CheatBase cheats + disables “Search CheatBase” option if there aren’t any 2023-01-31 14:36:38 -06:00
Riley Testut
74ccf1a246 Shrinks cheatbase.sqlite file size by removing unused columns 2023-01-31 14:36:38 -06:00
Riley Testut
a135ea236d Integrates CheatBase to browse and easily add cheats for recognized games
Limited to DS games right now.
2023-01-31 14:36:37 -06:00
Riley Testut
d204ea35bd Adds CheatBase dependency 2023-01-31 14:36:11 -06:00
Riley Testut
77983e73dd Adds “Respect Silent Mode” setting to configure whether Delta plays game audio in Silent Mode
Delta will also now automatically mute game audio if another app is playing audio.

[Missed] Mutes game audio correctly
2023-01-31 14:34:32 -06:00
Riley Testut
6aee8c84cd Updates app version to 1.4b4 2022-11-04 14:33:37 -05:00
Riley Testut
45757878d8 Fixes app freezing when using Hold Button 2022-11-04 14:30:39 -05:00
Riley Testut
8ccd86de0f Fixes re-activating sustained inputs 2022-11-04 14:28:14 -05:00
Riley Testut
ceb66d46be Updates Swift Packages 2022-10-24 15:25:45 -05:00
Riley Testut
0f2792fdbc Fixes “Hold Button” for continuous inputs mapped to discrete inputs 2022-10-24 15:19:42 -05:00
Riley Testut
eeae27cc24 Fixes using analog sticks as dpad inputs with Xbox controllers 2022-10-20 14:12:00 -05:00
Riley Testut
7677421ef4 Updates app version to 1.4b3 2022-10-19 17:22:46 -05:00
Riley Testut
d17a1f3d8f Fixes remapping continuous inputs 2022-10-19 17:14:22 -05:00
Riley Testut
7d93470738 Fixes taps sometimes not dismissing active callout view when remapping inputs 2022-10-19 17:07:56 -05:00
Riley Testut
a2b6771715 [iPad] Fixes software keyboard controller bugs on iPadOS 16.1 RC 2022-10-19 17:02:19 -05:00
Riley Testut
aa7ef0041f Enables parallelized compiling for Delta target
No longer uses “Manual” Build Order to determine compilation order.
2022-09-20 13:58:34 -05:00
Riley Testut
ee536f9ce5 [README] Updates compilation instructions
* Adds instructions for updating Systems.xcworkspace
* Asks user to install Git LFS
2022-08-17 10:51:47 -05:00
Riley Testut
b2ab33bcd1 Fixes “Revision.h file not found” compiler error 2022-08-17 10:51:15 -05:00
Riley Testut
48be35cbf2 Fixes displaying system name in dark text when remapping inputs on iOS 16 2022-08-15 16:18:04 -05:00
Riley Testut
fd2cf223dc Fixes missing controller skin when remapping inputs 2022-08-15 16:11:44 -05:00
Riley Testut
8155bc5ca8 Updates app version to 1.4b2 2022-08-15 11:43:46 -05:00
Riley Testut
cd6de86fb1 Fixes compiling with Xcode 13.x 2022-08-15 11:42:46 -05:00
Riley Testut
b2568efeb1 Updates dependencies 2022-08-14 11:10:04 -05:00
Riley Testut
5805b859f5 [iPad] Dismisses keyboard controller when pausing 2022-08-12 19:38:28 -05:00
Riley Testut
d061f56951 [iPad] Fixes keyboard controller not appearing when selecting buttons to hold 2022-08-12 19:37:26 -05:00
Riley Testut
c3e9cfe526 [iPad] Fixes keyboard controller sometimes using incorrect skin orientation 2022-08-12 19:35:37 -05:00
Riley Testut
3cf87afa2d [iPad] Initial support for Split View / Stage Manager controller skins
When Delta is in Split View, Slide Over, or windowed with Stage Manager, it will now present a special full-width “keyboard controller”, allowing for much easier gameplay than being constrained to the app window.

Each system’s controller skin has been updated to support Split View, which means every system can now be played without a game controller/keyboard when not in full screen.
2022-08-12 19:33:02 -05:00
Riley Testut
ea871c7520 Removes unnecessary ControllerSkin.inputs(for:at:)
ControllerSkinProtocol no longer requires this method, so we can just remove it.
2022-08-12 19:26:13 -05:00
Riley Testut
e1ee540d27 [iPad] Fixes automatically pausing + resuming emulation with Stage Manager on iOS 16 beta 5
We use DeltaCore’s new UIWindow subclass GameWindow in SceneDelegate to fix issues that were introduced with iOS 16 beta 5.

Also removes touchControllerSkin.layoutGuide = self.view.safeAreaLayoutGuide assignment which no longer compiles due to TouchControllerSkin refactoring.
2022-08-12 19:23:23 -05:00
Riley Testut
bb812c7f02 [iPad] Uses window size to determine TouchControllerSkin axis, not interface orientation
iPads in landscape orientation should only place DS screens side-by-side if the app window is wider than it is tall. Otherwise, it should use the default vertical screen layout.
2022-08-12 19:11:58 -05:00
Riley Testut
6ba648ed17 [iPad] Initial support for Split View / Stage Manager
Requires game controller (or hardware keyboard) to play games because we still need to update controller skins to support Split View.
2022-08-12 19:05:02 -05:00
Riley Testut
05a66a140e Fixes not detecting keyboard presses when remapping inputs 2022-08-11 17:42:36 -05:00
Riley Testut
52a68e28dd Switches to UIScene-based app lifecycle 2022-07-25 17:24:57 -05:00
Riley Testut
4829b393c5 Updates app version to 1.4b1 2022-06-01 10:00:06 -07:00
Riley Testut
b11766c973 [iPad] Updates standard controller skins to support iPad 2022-05-31 18:08:49 -07:00
Riley Testut
c09bfead65 Updates Cocoapods to 1.11.2 2022-05-31 18:05:30 -07:00
Riley Testut
7b1db2614f Fixes ControllerInputsViewController’s system picker UI 2022-05-31 18:03:26 -07:00
Riley Testut
973238e1a4 Disables AltJIT by default
melonDS save states created with JIT enabled cannot be loaded later without JIT, which made it very easy to accidentally replace a save state with one that could only be loaded when JIT is available.

We plan to remove AltJIT eventually for this reason, but for now we’ll just disable it by default.
2022-05-31 17:59:01 -07:00
Riley Testut
d4e22942b8 [Harmony] Fixes potential Core Data threading violation 2022-05-31 17:55:23 -07:00
Riley Testut
7c934cebe1 Merge branch 'ipad' into develop
# Conflicts:
#	Delta.xcodeproj/project.pbxproj
2022-05-31 17:51:31 -07:00
Riley Testut
b134c73301 Merge branch 'altjit' into develop
# Conflicts:
#	Delta.xcodeproj/project.pbxproj
2022-05-31 17:41:37 -07:00
Riley Testut
fce4fc1bec [iPad] Temporarily disables split view support 2022-05-16 14:54:42 -07:00
Riley Testut
977f3d8005 Fixes previous game controller remaining selected when changing controller 2022-05-09 16:44:29 -07:00
Riley Testut
edab6ea432 Fixes sharing games + exporting save files
* Makes temporary copy instead of symbolic link when exporting game
* Sanitizes game name before exporting game
* Fixes prematurely deleting temporary game
* Exported save file names now match exported game names
2022-04-28 18:13:25 -07:00
Riley Testut
9e437797d9 Improves CopyDeepLinkActivity
* Uses SF Symbol instead of bundled image
* Actually calls activityDidFinish(_:)
2022-04-28 16:57:06 -07:00
Riley Testut
25afda3b60 Replaces UIAlertController with UIMenu for importing games 2022-04-28 16:54:18 -07:00
Riley Testut
836297718b [iPad] Fixes game context menu actions 2022-04-28 16:38:14 -07:00
Riley Testut
da8415f4aa Improves SaveStatesViewController UI on iPad 2022-04-28 16:15:48 -07:00
Riley Testut
68c1b05313 Improves GameCollectionViewController UI on iPad 2022-04-28 15:47:05 -07:00
Riley Testut
2a4dbabae5 Fixes crash when presenting ImportController on iPad 2022-04-28 12:49:16 -07:00
Riley Testut
aafe673811 Fixes invisible navigation bar + toolbar on iPadOS 15 2022-04-27 17:57:55 -07:00
Riley Testut
adccf8fca5 Fixes ControllerInputsViewController UI on iPad 2022-04-27 16:15:09 -07:00
Riley Testut
d333672b95 Enables basic iPad support 2022-04-26 12:23:04 -07:00
Riley Testut
0df313188d Fixes keyboard support on iOS 15+ 2022-04-26 12:21:59 -07:00
Riley Testut
fd0427d2ad Fixes BIOSError compilation error when compiling with Xcode 13
Xcode 12 and older incorrectly let us declare BIOSError.incorrectSize as @available(iOS 13), but Swift doesn't support @available for enum cases with associated values. Xcode 13 now properly enforces this restriction, so we instead mark the enum itself as @available(iOS 13).
2022-04-19 14:20:40 -07:00
Riley Testut
394030ad43 Ensures melonDS is preferred DS core for release versions 2021-12-02 13:36:37 -08:00
Riley Testut
5f2e021560 Updates app version to 1.3.1 2021-12-02 13:14:39 -08:00
Riley Testut
17e1bf710b Dynamically maps outdated artwork URLs to correct URLs 2021-12-02 13:12:07 -08:00
Riley Testut
baf895939d Updates OpenVGDB to latest version
Fixes broken OpenVGDB cover art URLs due to host moving from gamefaqs1.cbsistatic.com to gamefaqs.gamespot.com.
2021-12-02 13:12:01 -08:00
Riley Testut
2705849cf2 Ensures melonDS is preferred DS core for release versions 2021-11-17 11:43:00 -08:00
Riley Testut
79bf977904 Updates app version to 1.3.1b 2021-11-16 16:39:30 -08:00
Riley Testut
11041ef1e9 Dynamically maps outdated artwork URLs to correct URLs 2021-11-16 16:31:58 -08:00
Riley Testut
08e870c94c Updates OpenVGDB to latest version
Fixes broken OpenVGDB cover art URLs due to host moving from gamefaqs1.cbsistatic.com to gamefaqs.gamespot.com.
2021-11-16 16:27:09 -08:00
Riley Testut
9492f3165e Hides AltJIT setting on unsupported devices 2021-11-16 15:04:19 -08:00
Riley Testut
f14bb0f890 Supports AltJIT
Automatically enables JIT for MelonDS core when on the same WiFi as AltServer.
2021-11-16 14:39:48 -08:00
Riley Testut
829d127269 Fixes BIOSError compilation error when compiling with Xcode 13
Xcode 12 and older incorrectly let us declare BIOSError.incorrectSize as @available(iOS 13), but Swift doesn't support @available for enum cases with associated values. Xcode 13 now properly enforces this restriction, so we instead mark the enum itself as @available(iOS 13).
2021-08-09 16:55:19 -07:00
Riley Testut
f1f75d8cc0 Updates app version to 1.3 2021-04-21 12:43:39 -07:00
Riley Testut
3128783105 Fixes melonDS graphical bugs when fast forwarding with JIT disabled 2021-04-21 12:41:35 -07:00
Riley Testut
abd7338a08 Replaces placeholder README with detailed README
Adds following sections to README:

* Intro
* Supported Systems
* Features
* Installation Instructions
* Project Requirements
* Licensing
* Contact Me
2021-04-21 12:41:16 -07:00
Riley Testut
c396698ca0 Fixes visible grid lines on DS controller skin buttons 2021-03-22 12:31:20 -07:00
Riley Testut
cd87438fb4 Updates app version to 1.3b5 2021-03-11 15:54:35 -06:00
Riley Testut
eafabb7f43 Enables melonDS core for public versions 2021-03-11 14:39:31 -06:00
Riley Testut
de315eacf5 Updates Licenses screen to include melonDS and Genesis Plus GX 2021-03-11 14:39:30 -06:00
Riley Testut
af6b92a441 Fixes erroneous sync error when syncing deleted files that were never uploaded 2021-03-11 13:49:15 -06:00
Riley Testut
d2215ed91e Updates DS controller skin 2021-03-11 13:27:24 -06:00
Riley Testut
20a470ddd1 Fixes crash playing DS games when another app is using microphone 2021-03-11 13:26:23 -06:00
Riley Testut
9f58aac350 Removes “(Beta)” from DS’s short name 2021-03-11 12:21:35 -06:00
Riley Testut
931a16c544 Hides Genesis skin credit for public versions 2021-03-10 14:33:57 -06:00
Riley Testut
8f7e7280f9 Removes support for 128KB DS firmwares
128KB DS firmwares come from DSi/3DS and aren’t bootable, so we consider them unsupported.
2021-03-10 14:32:09 -06:00
Riley Testut
8888b72d29 Limits DSi support to beta versions 2021-03-10 14:27:56 -06:00
Riley Testut
6cfca53dc8 Fixes game artwork not updating immediately when changed 2021-03-10 14:19:26 -06:00
Riley Testut
1bfe030dd9 Deletes temporary image file after changing artwork 2021-03-10 14:16:02 -06:00
Riley Testut
98ed657f8a Improves support for transparent and/or rotated custom artwork 2021-03-10 14:14:14 -06:00
Riley Testut
c22518ce23 Updates app version to 1.3b4 2021-02-24 13:29:43 -06:00
Riley Testut
58921cfb7f Fixes legacy plist format for GBA Game and Delta Skin UTIs 2021-02-24 13:25:07 -06:00
Riley Testut
dae3164d53 Credits Chris Rittenhouse (@litritt_z) for Genesis skin 2021-02-24 13:19:21 -06:00
Riley Testut
5ccf209af7 Changes Genesis placeholder skin to temporary one by @litritt_z 2021-02-24 13:17:25 -06:00
Riley Testut
bb6fbfea37 Replaces placeholder DS Home Screen image 2021-02-24 13:02:21 -06:00
Riley Testut
edb2af4dd5 Compares DSi BIOS files against unsupported files
DSi BIOS files can have various hashes, so rather than compare them against an expected hash, we now compare them against unsupported hashes and throw an error if it matches one.
2021-02-18 17:16:44 -06:00
Riley Testut
7c3b67fbfb Adds “Import Controller Skin” button to ControllerSkinsViewController 2021-02-18 14:07:39 -06:00
Riley Testut
bf2461fae1 Enables syncing DS(i) BIOS files
Excludes DSi NAND for now, since it is ~240MB and Harmony can’t selectively download files (yet).
2021-02-18 13:45:49 -06:00
Riley Testut
f316e7cca0 Fixes erroneous microphone indicator after returning from background 2021-02-12 14:57:57 -06:00
Riley Testut
3f70300afb Fixes incorrectly previewing DS home screen instead of DSi home screen 2021-02-12 13:18:55 -06:00
Riley Testut
2c52821e72 Fixes treating DS & DSi Home Screens as the same game 2021-02-12 13:14:33 -06:00
Riley Testut
4ed4b8ba06 Fixes misaligned ControllerInputsViewController callout views on iOS 14.5 2021-02-12 13:03:47 -06:00
Riley Testut
c3c6fb32cc Fixes ControllerInputsViewController DS layout
Presents ControllerInputsViewController full screen so DS portrait skin is not distorted.
2021-02-12 13:01:58 -06:00
Riley Testut
6f3a7501af Unlinks Cocoapods’ DeltaCore
Fixes runtime conflicts with Systems’ own DeltaCore.
2021-02-11 14:25:43 -06:00
Riley Testut
1b859ce769 [Systems] Fixes bitcode error when archiving 2021-02-10 12:31:27 -06:00
Riley Testut
46cb7db897 Disables cheats for Genesis games 2021-02-10 12:29:40 -06:00
Riley Testut
a29e4e61eb Emulates Sega Genesis games 2021-02-09 17:31:42 -06:00
Riley Testut
6a01a74608 Adds GPGXDeltaCore dependency
Links indirectly via new Systems.framework to avoid conflicts between Cocoapods and SwiftPM.
2021-02-09 16:51:47 -06:00
Riley Testut
945ffb1ed7 Enables new Xcode build system 2021-02-09 14:13:37 -06:00
Riley Testut
0a4c927277 Supports new Xcode build system 2021-02-09 14:13:13 -06:00
Riley Testut
c67b72068a Adds missing imports 2021-02-09 12:26:16 -06:00
Riley Testut
5127d5a65a Updates app version to 1.3b3 2021-01-21 15:18:36 -06:00
Riley Testut
c97611482e Fixes crash when previewing DS games without BIOS files 2021-01-19 14:41:55 -06:00
Riley Testut
f81f6cbf3d Hides DS “Home Screen” until BIOS files have been imported 2021-01-19 14:31:16 -06:00
Riley Testut
a079e68713 Changes default DS core from DeSmuME to melonDS 2021-01-19 14:20:00 -06:00
Riley Testut
66cfc272c1 Lowers MelonDSDeltaCore non-JIT maximum Fast Forward speed to 1.5x
Without JIT, the processor throttles very quickly at 2x. 1.5x allows at least a couple minutes before throttling kicks in.
2021-01-19 14:13:53 -06:00
Riley Testut
1871f69aca Disables JIT on iOS 14.4
JIT no longer works on iOS 14.4 beta 2, so disable for now until it (hopefully) works again.
2021-01-19 13:04:27 -06:00
Riley Testut
7034b1dd8a Fixes missing Fast Forward option for MelonDSDeltaCore 2021-01-19 12:49:55 -06:00
Riley Testut
3e0a983048 Validates melonDS BIOS files
Compares file sizes and MD5 hashes (when relevant) to ensure BIOS files are correct.
2021-01-19 12:44:54 -06:00
Riley Testut
826cf7b0b1 Fixes performance issues when previewing games + save states 2021-01-13 14:41:27 -06:00
Riley Testut
1b874ce9c1 Displays preview save state image even if context menu previews are disabled 2021-01-13 14:41:27 -06:00
Riley Testut
0ad0e752f8 Lowers N64DeltaCore maximum Fast Forward speed to 1.5x for some older devices 2021-01-13 14:41:03 -06:00
Riley Testut
d0823b1acb Raises MelonDSDeltaCore maximum Fast Forward speed to 3x 2021-01-11 14:14:02 -06:00
Riley Testut
474cae0a5b Updates MelonDSDeltaCore to 0.9.1 2021-01-11 14:05:10 -06:00
Riley Testut
0d5e7e97cc Adds “Context Menu Previews” setting
Controls whether Delta previews games and save states when using context menus. Enabled by default.
2021-01-04 14:30:56 -06:00
Riley Testut
a58d201b1b Fixes crash launching home screen shortcut when AppIconShortcutsViewController is visible 2021-01-04 14:22:44 -06:00
Riley Testut
58346140a8 Unhides “Home Screen Shortcuts” setting on devices without 3D Touch
All devices on iOS 13 or later support either 3D Touch or Haptic Touch, which means all devices now support home screen shortcuts.
2021-01-04 14:20:25 -06:00
Riley Testut
cb77be106a Renames “App Icon Shortcuts” setting to “Home Screen Shortcuts” 2021-01-04 14:19:42 -06:00
Riley Testut
ced5d6099e Fixes crash when customizing app icon shortcuts 2021-01-04 14:01:05 -06:00
Riley Testut
9f2f1bad77 Updates pods 2020-12-22 13:03:45 -06:00
Riley Testut
c7329136ac Supports touch inputs when external controller is connected 2020-12-10 15:34:16 -06:00
Riley Testut
a63de5db4d Updates MelonDSDeltaCore pod 2020-12-10 15:28:56 -06:00
Riley Testut
543e271563 Updates app version to 1.3b2 2020-12-02 12:09:43 -06:00
Riley Testut
ee79b6c8c9 Fixes garbled audio during emulation 2020-11-30 16:31:23 -06:00
Riley Testut
1f3f9d4bd1 Fixes MelonDS crash on devices without JIT support 2020-11-30 14:41:42 -06:00
Riley Testut
ffb6d7b02a Merge branch 'melonDS_0.9' into develop
# Conflicts:
#	Delta/Base.lproj/Settings.storyboard
2020-11-24 13:24:46 -06:00
Riley Testut
64df7b97ab Supports MelonDS JIT 2020-11-24 13:11:02 -06:00
Riley Testut
a1f80e74b5 Emulates DS microphone 2020-11-24 13:08:38 -06:00
Riley Testut
358accbcb7 Adds DSi support
Requires DSi BIOS files.
2020-11-24 13:06:29 -06:00
Riley Testut
8a273b31d2
Merge pull request #82 from ericlewis/support-dark-mode
feat: support iOS 13+ dark mode
2020-11-06 11:59:30 -08:00
Eric Lewis
15cab0bca8 feat: support iOS 13+ dark mode 2020-11-06 11:45:25 -05:00
Riley Testut
865c681df0 Updates app version to 1.2 2020-10-21 10:58:15 -07:00
Riley Testut
86c7ca3ce8 Updates app version to 1.2b3 2020-10-12 11:42:21 -07:00
Riley Testut
77512147ef Fixes swapped DS screens 2020-10-12 11:37:50 -07:00
Riley Testut
b85230b0ff Improves sync error messages when a game is missing or can’t be synced 2020-06-12 13:00:44 -07:00
Riley Testut
3cb3b9a10b Fixes various issues with controller skin filters 2020-05-05 11:18:46 -07:00
Riley Testut
e4404d179e Fixes NES games crashing after playing an N64 game 2020-05-01 11:28:16 -07:00
Riley Testut
9c99578206 Fixes misplaced DS screens when using external controller 2020-05-01 11:23:48 -07:00
Riley Testut
5c9332e61e Fixes importing games + controller skins from Files
Exposing Documents directory in Files app requires us to support opening files in place (despite LSSupportsOpeningDocumentsInPlace set to NO in Info.plist), so we now coordinate access to any external file URL
2020-04-28 14:44:06 -07:00
Riley Testut
a739926e17 Updates app version to 1.2b2 2020-04-27 14:40:15 -07:00
Riley Testut
fd79b04ff0 Updates DeltaCore dependency 2020-04-27 14:39:51 -07:00
Riley Testut
75869c06fd Fixes overwriting save file when previewing games 2020-04-27 13:13:34 -07:00
Riley Testut
e11e4437c5 Adds coreID to SaveState’s Harmony metadata for backwards compatibility 2020-04-27 13:03:28 -07:00
Riley Testut
f0d245ef8d Fixes always using melonDS core even if DeSmuME core is selected 2020-04-27 13:01:09 -07:00
Riley Testut
610697be5d Updates Harmony to add future backwards compatibility
Harmony will now persist properties from future model versions and later upload them, allowing for basic backwards compatibility.
2020-04-27 12:25:55 -07:00
Riley Testut
a9a2819b19 Adds Export Save File option to game context menu 2020-04-24 13:25:01 -07:00
Riley Testut
ec6fb6c03c Fixes N64 games crashing due to stripped library symbols 2020-04-24 13:20:54 -07:00
Riley Testut
1f6ad51c99 Adds support for 8 character NES Game Genie cheat codes 2020-04-23 18:04:37 -07:00
Riley Testut
0e8c9fbc5c Exposes Documents directory in Files app 2020-04-23 18:02:02 -07:00
Riley Testut
0b6567d98f Fixes incorrect game name color after quitting emulation 2020-04-23 17:25:09 -07:00
Riley Testut
01320b4dec Adds SaveState.coreIdentifier to filter out save states from other cores
Prevents DeSmuME save states from appearing while using melonDS core and vice versa.
2020-04-23 17:24:04 -07:00
Riley Testut
77da71cd62 Adds “Home Screen” DS game to boot into DS main menu 2020-04-23 17:18:59 -07:00
Riley Testut
6f336a82a5 Adds DS settings UI to import melonDS BIOS + switch DS cores 2020-04-23 17:16:44 -07:00
Riley Testut
35fe306c12 Switches to melonDS-based core for DS games 2020-04-23 16:52:29 -07:00
Riley Testut
427ec9da73 Fixes incorrect cheat code formatting 2020-04-23 16:19:05 -07:00
Riley Testut
9dc823f25c Fixes missing GameType declarations with Xcode 11.4 2020-03-18 13:17:44 -07:00
Riley Testut
d99b54f8ff Fixes broken submodule reference 2020-02-11 19:02:21 -08:00
Riley Testut
38591bf374 Updates app version to 1.2b 2020-02-11 16:36:21 -08:00
Riley Testut
4ba2fa8d21 Adds support for new Nintendo DS controller skin 2020-02-11 16:34:57 -08:00
Riley Testut
8b5ac435a6 Fixes assigning non-X controller skins to games on X-devices 2020-02-11 16:32:20 -08:00
Riley Testut
89db6b0d3a Migrates from Core Data model v4 to v5 2020-02-11 15:59:47 -08:00
Riley Testut
59beb243c0 Adds Save State and Fast Forward controller skin actions 2020-02-07 16:01:49 -08:00
Riley Testut
0373b757f7 Adds ability to assign controller skins per game 2020-02-07 15:57:11 -08:00
Riley Testut
cd7e9652ab Replaces peek & pop with context menus on iOS 13+ 2020-02-06 14:37:21 -08:00
Riley Testut
748f930186 Updates app version to 1.1.2 2020-02-03 19:31:12 -08:00
Riley Testut
6cca0f244f Replaces frameworks with static libraries
As of iOS 13.3.1, apps installed with free developer accounts that contain embedded frameworks fail to launch. To work around this, we now link all dependencies via Cocoapods as static libraries.
2020-02-03 19:28:23 -08:00
Riley Testut
82bfbd027a Updates app version to 1.1.1 2019-12-11 16:12:01 -08:00
Riley Testut
a0f60de926 Dynamically maps old album artwork URLs to new ones 2019-12-11 16:11:25 -08:00
Riley Testut
763e410ce9 Fixes broken OpenVGDB cover art URLs
Host moved from http://img.gamefaqs.net to https://gamefaqs1.cbsistatic.com
2019-12-10 12:30:33 -08:00
Riley Testut
4e772c077b Updates Roxas 2019-10-16 10:44:51 -07:00
Riley Testut
346c794244 Updates bundle version to 15 2019-10-14 19:00:05 -07:00
Riley Testut
63a8dfd5b9 Adds button to change save states sort order 2019-10-14 18:55:47 -07:00
Riley Testut
bcfbb7d7a0 Presents UIImagePickerController full screen
Workaround for iOS 13 interactive dismissal bug
2019-10-14 17:24:46 -07:00
Riley Testut
963ab6a586 Fixes app freezing when opening deep links 2019-10-14 17:24:18 -07:00
Riley Testut
f47f515f90 Fixes incorrect SaveStatesCollectionHeaderView text color on iOS 13 2019-10-14 17:04:39 -07:00
Riley Testut
da00488a55 Fixes cheats not wrapping in CheatTextView on iOS 13 2019-10-14 16:54:23 -07:00
Riley Testut
6501b6523b Presents UIDocumentBrowserViewController full screen
Workaround for iOS 13 interactive dismissal bug
2019-10-14 16:35:38 -07:00
Riley Testut
e965309e5c Updates app version to 1.1 and bundle version to 14 2019-10-11 15:43:01 -07:00
Riley Testut
63c932561e Fixes incorrect GamesStoryboardSegue animation on iOS 13
The additional toolbars to extend the edges beyond the navigation controller's bounds weren't positioned correctly on iOS 13.
2019-10-10 19:26:13 -07:00
Riley Testut
6afff591ff Updates GamesViewController navigation bar styling for iOS 13 2019-10-10 19:24:44 -07:00
Riley Testut
f560c95cc2 Updates PauseViewController navigation bar styling for iOS 13 2019-10-10 19:23:57 -07:00
Riley Testut
4aa7a100e3 Adds ability to delete controller skins 2019-10-10 12:13:48 -07:00
Riley Testut
d7ed26c372 Fixes incorrect controller view sizing when changing skins 2019-10-10 01:21:28 -07:00
Riley Testut
5574f8668a Asynchronously loads images when viewing controller skins 2019-10-10 00:29:14 -07:00
Riley Testut
a6b9a4567c Opts-out of dark mode (for now) 2019-10-10 00:27:00 -07:00
Riley Testut
465c7280aa Allows importing controller skins from document browser 2019-10-10 00:25:44 -07:00
Riley Testut
5c574f5ea3 Starts syncing after dismissing Settings with gesture 2019-10-03 16:05:35 -07:00
Riley Testut
2c05e1b70f Adds option to disable haptic feedback for touch inputs 2019-09-30 17:31:13 -07:00
Riley Testut
ef64a15e37 Adds ability to reset controller mappings to default mapping 2019-09-30 17:27:06 -07:00
Riley Testut
1703244da8 Explicitly starts syncing when dismissing SettingsViewController via Done button 2019-09-30 16:41:36 -07:00
Riley Testut
bb600d1e98 Automatically presents keyboard in GamesDatabaseBrowserViewController 2019-09-30 16:29:20 -07:00
Riley Testut
ce1ff171ce Fixes GamesViewController remaining visible when loading save states 2019-09-30 16:07:12 -07:00
Riley Testut
5337636f43 Opens Patreon page in-app when AltStore not installed 2019-09-30 15:32:24 -07:00
Riley Testut
0c6f28e70a Updates bundle version to 13 2019-09-30 15:23:34 -07:00
Riley Testut
a0d07f5e40 Updates README 2019-09-28 13:07:24 -07:00
Riley Testut
8642fad707 Updates DeltaCore 2019-09-28 02:02:50 -07:00
Riley Testut
ac156ab220 Updates version number to 1.0 2019-09-25 03:49:41 -07:00
Riley Testut
cca4c0bc14 Updates NESDeltaCore 2019-09-25 03:47:58 -07:00
Riley Testut
9412bf0df8 Adds Credits + Licenses + AltStore Patreon deep link 2019-09-25 03:46:27 -07:00
Riley Testut
86fd55b17a Limits N64 Fast Forwarding to devices with A9 or better 2019-09-21 17:58:39 -07:00
Riley Testut
6b494e1113 Fixes dismissing document browser on background thread 2019-09-19 13:32:06 -07:00
Riley Testut
eaae38481e Uses NSSecureUnarchiveFromData for transformable properties 2019-09-19 13:30:49 -07:00
Riley Testut
1e350e1369 Automatically resolves GameCollection + GameControllerInputMapping conflicts 2019-09-19 13:29:51 -07:00
Riley Testut
50c96bc2ee Dramatically reduces OpenVGDB size
Removes unused columns and games for unsupported systems.
2019-09-18 15:32:17 -07:00
Riley Testut
962d45e4f9 Removes temporary database backup on launch 2019-09-18 14:56:12 -07:00
Riley Testut
cec169c9b2 Adds BETA flag for debug builds 2019-09-18 12:41:20 -07:00
Riley Testut
08c61ad66b Improves error message when Dropbox prevents downloading games due to copyright 2019-09-18 12:39:36 -07:00
Riley Testut
adffbc03a2 Updates version to 0.9 and build to 11 2019-09-15 18:57:15 -07:00
Riley Testut
ebf3fc6c27 Adds support for building Delta Lite (and Delta Lite beta)
Limits Delta Lite to NES games, and Delta Lite beta to NES and GBC games.
2019-09-15 18:57:15 -07:00
Riley Testut
6e380814aa Updates DeltaCore 2019-09-15 18:57:15 -07:00
Riley Testut
c93a550ddd Updates N64 controller skin 2019-09-15 18:56:26 -07:00
Riley Testut
1528672e0e Updates Harmony 2019-09-14 14:38:30 -07:00
Riley Testut
34a1c78199 Fixes issue importing games that previously failed to import 2019-09-14 14:33:23 -07:00
Riley Testut
24d6da3af2 Updates NES controller skin 2019-09-14 14:31:56 -07:00
Riley Testut
628f942984 Fixes save errors for some DS games 2019-09-14 14:30:01 -07:00
Riley Testut
aa05e57afc Limits DS support to beta builds 2019-09-12 15:25:13 -07:00
Riley Testut
6f0137339a Removes Sync button from Games screen 2019-09-06 19:04:17 -07:00
Riley Testut
46ca21a37c Updates pods 2019-09-06 19:01:28 -07:00
Riley Testut
31d306e95f Adds “No Connected Controllers” cell when there are no game controllers 2019-09-06 18:58:43 -07:00
Riley Testut
7cf89e32f7 Prevents being refreshed by AltStore when in foreground 2019-09-06 17:26:40 -07:00
Riley Testut
a09a875c92 Renames DS short name to “DS (Beta)” 2019-08-14 19:04:15 -07:00
Riley Testut
3189c502c5 Fixes images not prefetching in AppIconShortcutsViewController 2019-08-07 16:55:44 -07:00
Riley Testut
e91b6bcd6b Uses constant bundleID for Fabric regardless of actual bundleID
AltStore resigns apps with unique bundle identifiers per-user, so we temporarily swizzle Bundle.infoDictionary to return a constant bundle identifier for Fabric so they can all be grouped together.
2019-08-07 16:51:00 -07:00
Riley Testut
b63853d7ce Renames “Sustain Buttons” to “Hold Buttons” 2019-08-07 14:49:12 -07:00
Riley Testut
ef57d882b3 Resets sustained inputs when changing games 2019-08-07 14:42:08 -07:00
Riley Testut
f5f09f22d4 Adds support for copying and opening deep link URLs 2019-08-07 13:27:53 -07:00
Riley Testut
5282265fd5 Fixes deep links not working when current game is paused 2019-08-07 13:15:59 -07:00
Riley Testut
3ac77f5707 Displays message when there are are games but no app icon shortcuts 2019-08-07 13:04:06 -07:00
Riley Testut
7a257bc9ca Presents confirmation alert when signing out of Delta Sync 2019-08-07 12:46:56 -07:00
Riley Testut
6e6c7a68bd Adds AppIconShortcutsViewController placeholder message 2019-08-07 12:46:20 -07:00
Riley Testut
0d5a7ba19f Merge branch 'feature/ds' into develop 2019-08-05 23:05:08 -07:00
Riley Testut
2a81710d07 Adds support for DS games 2019-08-05 23:03:08 -07:00
Riley Testut
c57b2ef725 Updates iOS deployment target to iOS 12.2 2019-08-05 22:59:54 -07:00
Riley Testut
cb2caa7ef1 Replaces screen edge gesture hack with preferredScreenEdgesDeferringSystemGestures
We want priority over system gestures when tapping near edges of screen. Previously, we needed to access the private screen edge gesture recognizer, but now we can use preferredScreenEdgesDeferringSystemGestures.
2019-08-05 22:58:59 -07:00
Riley Testut
687d088827 Updates cores to latest versions 2019-08-05 15:11:42 -07:00
Riley Testut
42e37517b1 Updates bundle version to 10 2019-08-02 12:05:07 -07:00
Riley Testut
0e47662353 Updates version number to 0.8.2 2019-08-02 12:04:37 -07:00
Riley Testut
67ab6887b4 Fixes crash when opening Delta with non-Dropbox URL scheme 2019-08-02 12:03:35 -07:00
Riley Testut
8113c4888d Limits controller customization screen to portrait orientation 2019-07-14 17:41:18 -07:00
Riley Testut
877cf88806 Fixes pause menu buttons obscured by notch 2019-07-14 17:29:05 -07:00
Riley Testut
283453b387 Fixes checking UIView.window on background thread 2019-06-21 14:11:21 -07:00
Riley Testut
bf435b88b3 Continues playing audio when other app audio supports mixing 2019-06-21 14:06:26 -07:00
Riley Testut
0a8c3b2b0f Fixes memory leak crash after playing games for a few minutes 2019-06-21 14:02:50 -07:00
Riley Testut
c62a079de9 Merge branch 'feature/n64' into develop 2019-06-21 12:08:13 -07:00
Riley Testut
1b75cdf65f Removes iCloud entitlement 2019-06-21 12:07:39 -07:00
Riley Testut
0046bfaf46 Adds support for N64 games 2019-06-21 12:07:30 -07:00
Riley Testut
9b28d42814 Updates input + video logic to support revised DeltaCore API
- Continuous inputs
- OpenGLES-based rendering
- Thumbsticks in controller skins
2019-04-30 15:42:30 -07:00
Riley Testut
da0ec57856 Improves reliability when previewing games/save states 2019-04-30 15:34:22 -07:00
Riley Testut
7da6a5d8a5 Fixes issue where cheats don’t wrap onto next line correctly 2019-04-30 15:10:07 -07:00
Riley Testut
3e5ebc7c32 Merge branch 'feature/dropbox' into develop 2019-03-26 11:17:55 -07:00
Riley Testut
9cfcf67c72 Renames “Inputs” to “Controllers” in Settings 2019-03-26 11:17:42 -07:00
Riley Testut
3ecee031be Compares hashes before marking game saves as updated 2019-03-26 00:55:52 -07:00
Riley Testut
bf2752496a Fixes nav bar disappearing when selecting SyncStatusViewController search result 2019-03-25 18:27:21 -07:00
Riley Testut
78dc2fedeb Updates version number to 0.7.1 2019-03-25 18:27:21 -07:00
Riley Testut
4856b9a540 Fixes NES controller skin not appearing
Fixes incorrect controller skin
2019-03-25 18:27:20 -07:00
Riley Testut
9bebfd6415 Resets previous Harmony beta database upon first launch 2019-03-25 17:18:50 -07:00
Riley Testut
de616021e2 Displays both name + email address of sync service account 2019-03-25 15:58:56 -07:00
Riley Testut
ca145ba681 Renames various sync-related terminology 2019-03-25 15:47:37 -07:00
Riley Testut
df7a8df19a Improves handling of authentication errors 2019-03-25 15:46:39 -07:00
Riley Testut
84e44f5aee Displays more detailed sync progress in RSTToastView 2019-03-25 15:46:01 -07:00
Riley Testut
72f4da6bc4 Hides SyncResultViewController Done button when not presented modally 2019-03-25 13:00:09 -07:00
Riley Testut
9db68aa9e4 Fixes Game Save errors not being grouped with Game errors 2019-03-22 13:04:34 -07:00
Riley Testut
8bc9d02e4c Improves UI for restoring records/resolving conflicts 2019-03-20 11:22:39 -07:00
Riley Testut
7464ce1412 Fixes crash when searching in SyncStatusViewController 2019-03-20 10:53:14 -07:00
Riley Testut
935ad9b7c2 Displays record name in RecordSyncStatusViewController 2019-03-20 10:47:43 -07:00
Riley Testut
483ad69678 Adds support for syncing with Dropbox 2019-03-20 10:47:17 -07:00
Riley Testut
8f6b8d763a Updates DriveService client ID 2019-03-20 00:08:59 -07:00
Riley Testut
28335c8deb Switches back to legacy build system
Temporary (hopefully) until I’m able to spend more time figuring out how to definitively fix the dependency cycle errors
2019-03-04 13:44:54 -08:00
Riley Testut
f4374ed54a Adds support for WarioWare: Twisted! 2019-03-01 13:36:00 -08:00
Riley Testut
803d180a9b Adds ability to import game save files 2019-03-01 11:42:37 -08:00
Riley Testut
43b4aeac51 Fixes SNES games crashing when stopping emulation 2019-02-28 17:59:24 -08:00
Riley Testut
6dbe908a66 Improves error message when displaying RecordError.other error 2019-02-25 19:02:22 -08:00
Riley Testut
3b05afd21e Automatically syncs when entering/returning from background 2019-02-25 17:13:47 -08:00
Riley Testut
2c0709fa38 Fixes various memory leaks 2019-02-25 17:13:00 -08:00
Riley Testut
6b8414ccdc Updates dependencies 2019-02-25 15:21:35 -08:00
Riley Testut
fe6701c82c Migrates to Swift 5 2019-02-25 13:50:11 -08:00
Riley Testut
14e2eefc42 Updates Harmony dependency 2019-02-25 12:29:45 -08:00
Riley Testut
4778d48b67 Hides syncing status Settings row when no syncing service is selected 2019-02-21 16:13:05 -08:00
Riley Testut
0babc81914 Fixes deep links not working when app is not running 2019-02-21 15:56:49 -08:00
Riley Testut
e63a525671 Fixes black screen when running NES games on device without debugger 2019-02-21 15:24:53 -08:00
Riley Testut
a377c1631a Adds placeholder view for SyncStatusViewController 2019-02-21 12:47:19 -08:00
Riley Testut
bec8d89acc Improves Delta scheme build dependencies to prevent build cycles 2019-02-21 12:46:48 -08:00
Riley Testut
b3a72ee2aa Updates version number to 0.7 2019-02-20 17:20:27 -08:00
Riley Testut
786450c8e9 Updates dependencies 2019-02-20 17:20:11 -08:00
Riley Testut
3443fe4e4f Adds NES UTI + Document declarations 2019-02-20 16:18:49 -08:00
Riley Testut
d03cc90a29 Merge branch 'feature/nes' into develop
# Conflicts:
#	.gitmodules
#	Delta.xcodeproj/xcshareddata/xcschemes/Delta.xcscheme
#	Delta.xcworkspace/contents.xcworkspacedata
#	Delta/Base.lproj/Settings.storyboard
#	Delta/Settings/SettingsViewController.swift
2019-02-06 15:40:11 -08:00
Riley Testut
986b329178 Merge branch 'feature/harmony' into develop 2019-02-06 14:21:47 -08:00
Riley Testut
4f30dce4ec Increases syncing RSTToastView font size 2019-02-05 14:20:32 -08:00
Riley Testut
90c04ee62e Prevents launching games while downloading their game saves
This minimizes potential for data loss.
2019-02-05 14:14:39 -08:00
Riley Testut
708ebb1a7f Backs up Database folder on first launch
Allows beta testers to recover data while testing Harmony syncing if something goes wrong
2019-02-01 13:02:16 -08:00
Riley Testut
1e144e5657 Adds support for NES games 2019-02-01 12:44:32 -08:00
Riley Testut
7523102982 Adjusts keyboard input display priorities to favor special character keys 2019-02-01 12:25:08 -08:00
Riley Testut
04a6b10b39 Merge branch 'feature/bugfixes' into develop 2019-02-01 11:38:14 -08:00
Riley Testut
4bbfee5e8f Seeds Harmony database on launch if not yet seeded 2019-01-31 17:03:40 -08:00
Riley Testut
d446e3612f Fixes unauthorized access and rate limit Harmony errors 2019-01-31 17:03:25 -08:00
Riley Testut
3b7cb49d89 Adds support for migrating to latest Core Data model from any previous version 2019-01-31 12:19:34 -08:00
Riley Testut
17e20a6a7c Adds complete support for external keyboards
Fixes misc. other controller skin issues
2019-01-29 16:34:51 -08:00
Riley Testut
65342e0b55 Displays correct local modification date in RecordSyncStatusViewController 2019-01-29 15:11:25 -08:00
Riley Testut
3bd0a35c61 Adds support for syncing GameSaves 2019-01-29 15:07:46 -08:00
Riley Testut
dbe298f2a7 Adds complete support for (de)authenticating users 2019-01-19 16:10:55 -08:00
Riley Testut
878506e34f Adds ability to view previous sync results from SyncStatusViewController 2019-01-19 13:42:03 -08:00
Riley Testut
86beaaaaa4 Adds ability to view details of record errors in SyncResultViewController
Also updates error handling to match revised Harmony errors.
2019-01-18 16:08:04 -08:00
Riley Testut
38ae10db78 Disables parallelized building 2019-01-17 16:40:38 -08:00
Riley Testut
bace668739 Adds SyncResultViewController to view errors that occured during sync 2018-12-04 17:21:04 -08:00
Riley Testut
eaa8429bd8 Adds SaveState.localizedName 2018-12-04 17:06:42 -08:00
Riley Testut
9a186ffea9 Changes Cheat.name to non-optional 2018-12-04 17:05:14 -08:00
Riley Testut
007092f875 Disables OS_ACTIVITY_MODE 2018-12-04 17:04:02 -08:00
Riley Testut
5c531fcbee Fixes non-syncable save states appearing in GameSyncStatusViewController 2018-12-04 16:21:15 -08:00
Riley Testut
144fd83167 Adds Harmony schemes 2018-12-04 16:21:15 -08:00
Riley Testut
f8c47fcb86 Adds RecordVersionsViewController
Allows users to restore remote versions of records, or resolve conflicts
2018-12-04 16:21:10 -08:00
Riley Testut
b226698760 Adds RecordSyncStatusViewController
Views local and remote status of a record, and enable/disable syncing
2018-11-27 14:46:38 -08:00
Riley Testut
fa4803373b Displays Harmony conflict count in SettingsViewController 2018-11-27 14:42:43 -08:00
Riley Testut
c4487433cd Adds Delta colors to asset catalog 2018-11-27 14:39:38 -08:00
Riley Testut
97b456d9b7 Adds UIAlertController extension to display Errors 2018-11-27 14:38:29 -08:00
Riley Testut
1f298f8e79 Fixes potential crash when launching games 2018-11-26 16:55:23 -08:00
Riley Testut
ca4ccfc3ae Adds basic GameSyncStatusViewController to view status of game-related records 2018-11-20 14:47:47 -06:00
Riley Testut
5354d779c1 Adds SyncStatusViewController to view basic sync status of games 2018-11-20 13:04:55 -06:00
Riley Testut
c1fc0d1ce3 Fixes game artwork not refreshing after syncing 2018-11-20 13:04:31 -06:00
Riley Testut
c3301f9384 Displays basic toast view when syncing starts/finishes 2018-11-14 19:01:55 -08:00
Riley Testut
88601fb952 Adds temporary Sync button to initiate syncs 2018-11-14 19:01:34 -08:00
Riley Testut
a3108e6c3a Adds support for syncing GameControllerInputMappings 2018-11-14 15:36:16 -08:00
Riley Testut
557529b1e7 Adds support for syncing ControllerSkins 2018-11-14 14:26:19 -08:00
Riley Testut
5b64ca7c7b Adds support for syncing Cheats 2018-11-14 14:10:19 -08:00
Riley Testut
fb9272cd6c Adds support for syncing SaveStates 2018-11-14 13:42:36 -08:00
Riley Testut
f858a3eb07 Adds support for syncing Games and GameCollections 2018-11-14 13:18:48 -08:00
Riley Testut
db9f43334d Adds basic SyncManager implementation
Handles authenticating with Google Drive
2018-11-13 17:46:47 -08:00
Riley Testut
4c913d5be0 Refactors LaunchViewController into RSTLaunchViewController subclass 2018-11-13 17:27:04 -08:00
Riley Testut
c1cfdad0a7 Misc. updates to DatabaseManager
- Subclasses RSTPersistentContainer
- Removes need for FileMD5Hash pod
- Adds start() as better way to load database
- Uses merged Harmony model
2018-11-13 17:26:21 -08:00
Riley Testut
d3b7767374 Fixes occasional crash when importing several games 2018-11-13 16:14:52 -08:00
Riley Testut
746ed9638b Changes Game.gameCollections to single Game.gameCollection 2018-11-13 16:13:03 -08:00
Riley Testut
8429c795a0 Configures project to support syncing with Google Drive 2018-11-13 16:03:49 -08:00
Riley Testut
116cb16538 Adds Harmony dependency 2018-11-12 15:23:21 -08:00
Riley Testut
e3c4e52981 Merge branch 'develop' into feature/harmony 2018-11-12 15:00:20 -08:00
Riley Testut
2f4171b9e2 Fixes issue where keyboard may appear on app launch 2018-11-12 14:59:21 -08:00
Riley Testut
d1596fa7c5 Fixes missing ZIPFoundation error when running on device 2018-11-12 14:51:49 -08:00
Riley Testut
7b60c1e067 Updates deployment target to iOS 12.0 2018-11-12 14:51:29 -08:00
Riley Testut
545c5a223f Adds basic UI for selecting syncing services 2018-11-12 12:43:30 -08:00
Riley Testut
e27f1afb1a Updates dependencies to Swift 4.2 and Xcode 10.1 recommended project settings 2018-11-12 12:12:17 -08:00
Riley Testut
94d7edd707 Updates project to recommended Xcode 10.1 settings 2018-11-12 11:57:21 -08:00
Riley Testut
52c15eeb60 Fixes warnings 2018-11-12 11:52:11 -08:00
Riley Testut
4939a7da25 Removes 4.2 migrator helper functions 2018-11-12 11:51:04 -08:00
Riley Testut
ba653037b1 Migrates to Swift 4.2 via Xcode 10.1 migrator 2018-11-12 11:45:55 -08:00
Riley Testut
15b23c13e7 Updates delta cores + replaces ZipZap with ZIPFoundation
Replaces ZipZap with ZIPFoundation due to DeltaCore now using ZIPFoundation
2018-11-08 17:13:39 -08:00
Riley Testut
f293a2843f Merge branch 'feature/app_icon_shortcuts' into develop 2018-01-05 00:30:46 -06:00
Riley Testut
a9c3e85df8 Adds support for 3D Touch app icon game shortcuts 2018-01-04 14:17:59 -06:00
Riley Testut
2e36b8125e Fixes broken mogenerator run script 2018-01-04 14:17:27 -06:00
Riley Testut
d3d56d3454 Fixes rare crash when updating connected controllers 2018-01-04 14:13:04 -06:00
Riley Testut
5939e20399 Fixes crash when attempting to use invalid clipboard image when changing game artwork 2017-12-21 15:39:35 -06:00
Riley Testut
a371bb71a9 Fixes issue where SaveStatesViewController blurred background didn’t extend to bottom on iPhone X 2017-12-21 15:19:32 -06:00
Riley Testut
d30f4db894 Merge branch 'feature/iphone_x' into develop 2017-12-19 01:55:22 -06:00
Riley Testut
18d6bd262a Adds support for iPhone X-optimized controller skins 2017-12-19 01:54:10 -06:00
Riley Testut
892f1cab2d Adds version number text in SettingsViewController 2017-12-18 23:46:32 -06:00
Riley Testut
dff18d2b6d Fixes search sanitization issues when searching for games 2017-12-18 23:25:34 -06:00
Riley Testut
407b801243 Fixes issue where importing via UIDocumentBrowserViewController would fail due to invalid permissions 2017-12-18 23:14:27 -06:00
Riley Testut
a5acf30600 Fixes issue where importing games not in OpenVGDB results in incorrect name 2017-12-18 23:14:04 -06:00
Riley Testut
36a8739479 Fixes GamesDatabaseBrowserViewController iOS 11 issues 2017-12-18 22:22:03 -06:00
Riley Testut
6836fb5bae Moves core ControllerSkin translucency logic to ControllerView 2017-12-18 18:14:28 -06:00
Riley Testut
bb418038e2 Fixes misc. iPhone X layout issues 2017-11-29 01:41:05 -08:00
Riley Testut
c0ffa10fff Merge branch 'master' into develop 2017-10-16 20:38:26 -07:00
Riley Testut
5aa8420513 Merge branch 'hotfix/beta4_migration' 2017-10-16 20:38:26 -07:00
Riley Testut
19d438f566 Fixes issue where database from beta 4 failed to migrate correctly 2017-10-16 20:37:51 -07:00
Riley Testut
48c257ac81 Merge branch 'master' into develop 2017-10-16 13:47:49 -07:00
Riley Testut
93ffb83e94 Merge branch 'release/beta5' 2017-10-16 13:47:49 -07:00
Riley Testut
6cd2f4e0dc Merge branch 'develop' into release/beta5 2017-10-16 13:45:35 -07:00
Riley Testut
9f27d3bc0e Merge branch 'feature/game_search' into develop 2017-10-16 13:04:09 -07:00
Riley Testut
52c39dbc4d Adds ability to search for games from GamesViewController 2017-10-16 13:03:00 -07:00
Riley Testut
5709320415 Prevents iOS simulator from automatically assigning external game controller indexes 2017-10-16 13:00:25 -07:00
Riley Testut
99d8f11c9b Fixes occasional crash when deleting or locking/unlocking save states 2017-10-12 18:13:55 -07:00
Riley Testut
6f33ed5824 Updates Assets 2017-10-12 16:07:32 -07:00
Riley Testut
6d2208a0f8 Updates project structure to match and automatically keep in sync with file system 2017-10-12 15:54:55 -07:00
Riley Testut
a8e5e3ab09 Merge branch 'feature/swift4' into develop 2017-10-12 15:36:46 -07:00
Riley Testut
fb11f6dec7 Replaces fileprivate with private. THANK FUCKING GOD. 2017-10-12 15:33:16 -07:00
Riley Testut
090c84df61 Updates Xcode project to recommended settings 2017-10-12 15:09:52 -07:00
Riley Testut
a04ad250c3 Updates Pods 2017-10-12 15:08:54 -07:00
Riley Testut
356d843a9b Removes use of deprecated String.characters property 2017-10-12 14:43:36 -07:00
Riley Testut
fb5036acf8 Manually fixes Swift 4 migrator errors 2017-10-12 14:40:15 -07:00
Riley Testut
d709204107 Migrates to Swift 4 via Xcode migrator 2017-10-12 13:46:11 -07:00
Riley Testut
df0c307be4 Fixes animation when launching a game when a game is already running 2017-10-12 11:13:49 -07:00
Riley Testut
c16562c8ca Merge branch 'feature/ios11' into develop 2017-10-12 10:45:54 -07:00
Riley Testut
cc007afceb Merge branch 'feature/external_controllers' into develop 2017-10-12 10:44:09 -07:00
Riley Testut
a58835e3ad Automatically uses external game controllers when connected 2017-10-12 10:27:56 -07:00
Riley Testut
61440ef994 Adds support for handling ActionInputs from GameControllers
• Quick Save
• Quick Load
• Fast Forward
2017-10-10 14:54:20 -07:00
Riley Testut
c6875c44b6 Renames Core Data model from Model to Delta 2017-09-28 19:25:20 -07:00
Riley Testut
94cbdbe159 Persists customized input mappings between app launches 2017-09-28 12:55:05 -07:00
Riley Testut
d70105e30e Adds ControllerInputsViewController to customize external game controller inputs
Includes necessary code changes to use refactored DeltaCore Input logic
2017-09-27 13:08:41 -07:00
Riley Testut
aa5dc88114 Fixes incorrect animations when presenting pause menu as well as selecting options 2017-09-26 17:21:24 -07:00
Riley Testut
a592d6e2ad Fixes incorrect GamesViewController content insets 2017-09-26 13:09:15 -07:00
Riley Testut
2da455bc2f Replaces UIDocumentPickerViewController with UIDocumentBrowserViewController on iOS 11 2017-09-26 12:58:39 -07:00
Riley Testut
c0b3a04110 Fixes GameViewController “cannot satisfy constraints” error 2017-07-07 22:39:09 -05:00
Riley Testut
812a773fba Refactors UICollectionViewControllers/UITableViewControllers to use prefetching RSTCellContentDataSources 2017-07-07 21:58:29 -05:00
Riley Testut
0c567de380 Updates GameMetadata + GamesDatabase to include release identifier 2017-07-07 21:26:47 -05:00
Riley Testut
7695a800c6 Fixes SQLite.swift compiler errors in Xcode 9
Adds post install action to Podfile to modify SQLite.swift source files.
2017-06-29 13:35:33 -05:00
Riley Testut
a8176cb276 Fixes refactored FileManager.uniqueTemporaryURL usages 2017-06-29 13:35:33 -05:00
Riley Testut
e5f232b7b2 Uses UIAlertController instead of UIDocumentMenuViewController for Cydia Impactor builds 2017-05-09 20:42:46 -07:00
Riley Testut
f81830230f Adds explicit GBCDeltaCore target to Delta’s build targets 2017-05-09 20:38:39 -07:00
2295 changed files with 180895 additions and 5910 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@
## Build generated ## Build generated
build/ build/
DerivedData DerivedData
.build
## Various settings ## Various settings
*.pbxuser *.pbxuser

21
.gitmodules vendored
View File

@ -13,3 +13,24 @@
[submodule "Cores/GBCDeltaCore"] [submodule "Cores/GBCDeltaCore"]
path = Cores/GBCDeltaCore path = Cores/GBCDeltaCore
url = git@github.com:rileytestut/GBCDeltaCore.git url = git@github.com:rileytestut/GBCDeltaCore.git
[submodule "External/Harmony"]
path = External/Harmony
url = https://github.com/rileytestut/Harmony.git
[submodule "Cores/NESDeltaCore"]
path = Cores/NESDeltaCore
url = git@github.com:rileytestut/NESDeltaCore.git
[submodule "Cores/N64DeltaCore"]
path = Cores/N64DeltaCore
url = git@github.com:rileytestut/N64DeltaCore.git
[submodule "Cores/DSDeltaCore"]
path = Cores/DSDeltaCore
url = https://github.com/rileytestut/DSDeltaCore.git
[submodule "Cores/MelonDSDeltaCore"]
path = Cores/MelonDSDeltaCore
url = https://github.com/rileytestut/MelonDSDeltaCore.git
[submodule "Cores/GPGXDeltaCore"]
path = Cores/GPGXDeltaCore
url = https://github.com/rileytestut/GPGXDeltaCore.git
[submodule "External/CheatBase"]
path = External/CheatBase
url = https://github.com/rileytestut/CheatBase.git

5168
Artwork/Icon.ai Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

1
Cores/DSDeltaCore Submodule

@ -0,0 +1 @@
Subproject commit 6c84366b3a76045782905293c9616e33f5da1a35

@ -1 +1 @@
Subproject commit caaf2e8e4bd847a2442382e121b07b4d5f139000 Subproject commit c1db5f51cd455a7033801cc19dc3dbfcb6f2b42c

@ -1 +1 @@
Subproject commit 4eedbd481457e3c236d1f3ffab3ba2f49c6d18df Subproject commit 8ea36dff87bc1f787765de45fa8ccbcc1256a0e3

@ -1 +1 @@
Subproject commit 0e622c7887e781363e36f6c7bfcd67fba3cb00ac Subproject commit 81f8ffba56823637706689fb5c6bc634ee4d9b32

1
Cores/GPGXDeltaCore Submodule

@ -0,0 +1 @@
Subproject commit 18c595887a12ef23e0d54c63f83c91c99e7f4827

@ -0,0 +1 @@
Subproject commit 697ba731981824f53460f6e0193f159f71f22ba2

1
Cores/N64DeltaCore Submodule

@ -0,0 +1 @@
Subproject commit c8816c51f82210a9c4cc62b1a7c53fa21bc705ee

1
Cores/NESDeltaCore Submodule

@ -0,0 +1 @@
Subproject commit 78a092d4e795f83153e98749b5cbeb66cf812d7e

@ -1 +1 @@
Subproject commit bdc95aeb5e7f50263c34187cbe38ddddb4a2576e Subproject commit d5717291325578f64d519822aeb2be81217c67f3

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "0830" LastUpgradeVersion = "1020"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "NO" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
@ -14,52 +14,10 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFADAFF719AE7BB70050CF31" BlueprintIdentifier = "33C94426DAF58519DC6806AF4C44C9E7"
BuildableName = "Roxas.framework" BuildableName = "libPods-Delta.a"
BlueprintName = "Roxas" BlueprintName = "Pods-Delta"
ReferencedContainer = "container:External/Roxas/Roxas.xcodeproj"> ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF46895C1AACF36800A2586D"
BuildableName = "DeltaCore.framework"
BlueprintName = "DeltaCore"
ReferencedContainer = "container:Cores/DeltaCore/DeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF9F4FDB1AAD8070004C9500"
BuildableName = "SNESDeltaCore.framework"
BlueprintName = "SNESDeltaCore"
ReferencedContainer = "container:Cores/SNESDeltaCore/SNESDeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFE8E9C91D010AF7009D623D"
BuildableName = "GBADeltaCore.framework"
BlueprintName = "GBADeltaCore"
ReferencedContainer = "container:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
<BuildActionEntry <BuildActionEntry
@ -71,7 +29,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1" BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
BuildableName = "Delta.app" BuildableName = "Retro Game Emulator.app"
BlueprintName = "Delta" BlueprintName = "Delta"
ReferencedContainer = "container:Delta.xcodeproj"> ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference> </BuildableReference>
@ -83,24 +41,23 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1" BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
BuildableName = "Delta.app" BuildableName = "Retro Game Emulator.app"
BlueprintName = "Delta" BlueprintName = "Delta"
ReferencedContainer = "container:Delta.xcodeproj"> ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<AdditionalOptions> <Testables>
</AdditionalOptions> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
@ -112,12 +69,20 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1" BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
BuildableName = "Delta.app" BuildableName = "Retro Game Emulator.app"
BlueprintName = "Delta" BlueprintName = "Delta"
ReferencedContainer = "container:Delta.xcodeproj"> ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments> <CommandLineArguments>
<CommandLineArgument
argument = "-com.apple.CoreData.MigrationDebug 1"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-com.rileytestut.Harmony.Debug 1"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "-com.apple.CoreData.ConcurrencyDebug 1" argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES"> isEnabled = "YES">
@ -127,11 +92,9 @@
<EnvironmentVariable <EnvironmentVariable
key = "OS_ACTIVITY_MODE" key = "OS_ACTIVITY_MODE"
value = "disable" value = "disable"
isEnabled = "YES"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@ -144,7 +107,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1" BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
BuildableName = "Delta.app" BuildableName = "Retro Game Emulator.app"
BlueprintName = "Delta" BlueprintName = "Delta"
ReferencedContainer = "container:Delta.xcodeproj"> ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference> </BuildableReference>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D5D7C1F029E60DFF00663793"
BuildableName = "libDeltaFeatures.a"
BlueprintName = "DeltaFeatures"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D5D7C1F029E60DFF00663793"
BuildableName = "libDeltaFeatures.a"
BlueprintName = "DeltaFeatures"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D539102E29E88B6B0006B350"
BuildableName = "DeltaPreviews.framework"
BlueprintName = "DeltaPreviews"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D539102E29E88B6B0006B350"
BuildableName = "DeltaPreviews.framework"
BlueprintName = "DeltaPreviews"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF6E70B925D2187800E41CD1"
BuildableName = "Systems"
BlueprintName = "Systems"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF6E70B925D2187800E41CD1"
BuildableName = "Systems"
BlueprintName = "Systems"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF14D8941DE7A512002CA1BE"
BuildableName = "mogenerator"
BlueprintName = "mogenerator"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF14D8941DE7A512002CA1BE"
BuildableName = "mogenerator"
BlueprintName = "mogenerator"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -7,14 +7,32 @@
<FileRef <FileRef
location = "group:Cores/DeltaCore/DeltaCore.xcodeproj"> location = "group:Cores/DeltaCore/DeltaCore.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Cores/NESDeltaCore/NESDeltaCore.xcodeproj">
</FileRef>
<FileRef <FileRef
location = "group:Cores/SNESDeltaCore/SNESDeltaCore.xcodeproj"> location = "group:Cores/SNESDeltaCore/SNESDeltaCore.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Cores/GBCDeltaCore/GBCDeltaCore.xcodeproj">
</FileRef>
<FileRef <FileRef
location = "group:Cores/GBADeltaCore/GBADeltaCore.xcodeproj"> location = "group:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:Cores/GBCDeltaCore/GBCDeltaCore.xcodeproj"> location = "group:Cores/N64DeltaCore/N64DeltaCore.xcodeproj">
</FileRef>
<FileRef
location = "group:Cores/MelonDSDeltaCore/MelonDSDeltaCore.xcodeproj">
</FileRef>
<FileRef
location = "group:Cores/DSDeltaCore/DSDeltaCore.xcodeproj">
</FileRef>
<FileRef
location = "group:Cores/GPGXDeltaCore">
</FileRef>
<FileRef
location = "group:External/Harmony/Harmony.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:External/Roxas/Roxas.xcodeproj"> location = "group:External/Roxas/Roxas.xcodeproj">

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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -4,5 +4,7 @@
<dict> <dict>
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key> <key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<false/> <false/>
<key>PreviewsEnabled</key>
<false/>
</dict> </dict>
</plist> </plist>

View File

@ -0,0 +1,34 @@
{
"object": {
"pins": [
{
"package": "AltKit",
"repositoryURL": "https://github.com/rileytestut/AltKit.git",
"state": {
"branch": null,
"revision": "2fd376df1c79ec06a5c80cc8933da027f65b3148",
"version": null
}
},
{
"package": "DeltaCore",
"repositoryURL": "https://github.com/rileytestut/DeltaCore.git",
"state": {
"branch": "ios14",
"revision": "74d2a7a6e36035cb5730d0b0cf2456cbeb6faf0c",
"version": null
}
},
{
"package": "ZIPFoundation",
"repositoryURL": "https://github.com/weichsel/ZIPFoundation.git",
"state": {
"branch": null,
"revision": "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0",
"version": "0.9.19"
}
}
]
},
"version": 1
}

View File

@ -9,38 +9,57 @@
import UIKit import UIKit
import DeltaCore import DeltaCore
import Harmony
import AltKit
import Fabric private extension CFNotificationName
import Crashlytics {
static let altstoreRequestAppState: CFNotificationName = CFNotificationName("com.altstore.RequestAppState.com.rileytestut.Delta" as CFString)
static let altstoreAppIsRunning: CFNotificationName = CFNotificationName("com.altstore.AppState.Running.com.rileytestut.Delta" as CFString)
}
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
appDelegate.receivedApplicationStateRequest()
}
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate class AppDelegate: UIResponder, UIApplicationDelegate
{ {
var window: UIWindow? var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool private let deepLinkController = DeepLinkController()
{ private var appLaunchDeepLink: DeepLink?
Fabric.with([Crashlytics.self])
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
Settings.registerDefaults() Settings.registerDefaults()
System.supportedSystems.forEach { Delta.register($0.deltaCore) } self.registerCores()
self.configureAppearance() self.configureAppearance()
// Disable system gestures that delay touches on left edge of screen
for gestureRecognizer in self.window?.gestureRecognizers ?? [] where NSStringFromClass(type(of: gestureRecognizer)).contains("GateGesture")
{
gestureRecognizer.delaysTouchesBegan = false
}
// Database
DatabaseManager.shared.loadPersistentStores { (description, error) in
}
// Controllers // Controllers
ExternalControllerManager.shared.startMonitoringExternalControllers() ExternalGameControllerManager.shared.startMonitoring()
// JIT
ServerManager.shared.prepare()
// Notifications
let center = CFNotificationCenterGetDarwinNotifyCenter()
CFNotificationCenterAddObserver(center, nil, ReceivedApplicationState, CFNotificationName.altstoreRequestAppState.rawValue, nil, .deliverImmediately)
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.databaseManagerDidStart(_:)), name: DatabaseManager.didStartNotification, object: DatabaseManager.shared)
// Deep Links
if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem
{
self.appLaunchDeepLink = .shortcut(shortcut)
// false = we handled the deep link, so no need to call delegate method separately.
return false
}
return true return true
} }
@ -73,27 +92,75 @@ class AppDelegate: UIResponder, UIApplicationDelegate
} }
} }
@available(iOS 13, *)
extension AppDelegate extension AppDelegate
{ {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
if connectingSceneSession.role == .windowExternalDisplay
{
// External Display
return UISceneConfiguration(name: "External Display", sessionRole: connectingSceneSession.role)
}
else
{
// Default Scene
return UISceneConfiguration(name: "Main", sessionRole: connectingSceneSession.role)
}
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
{
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
private extension AppDelegate
{
func registerCores()
{
#if LITE
#if BETA
Delta.register(System.nes.deltaCore)
Delta.register(System.gbc.deltaCore)
#else
Delta.register(System.nes.deltaCore)
#endif
#else
#if BETA
System.allCases.forEach { Delta.register($0.deltaCore) }
#else
System.allCases.filter { $0 != .genesis }.forEach { Delta.register($0.deltaCore) }
#endif
#endif
}
func configureAppearance() func configureAppearance()
{ {
self.window?.tintColor = UIColor.deltaPurple self.window?.tintColor = UIColor.deltaPurple
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes[NSForegroundColorAttributeName] = UIColor.white
} }
} }
extension AppDelegate extension AppDelegate
{ {
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
{ {
return self.openURL(url) return self.openURL(url)
} }
@discardableResult fileprivate func openURL(_ url: URL) -> Bool @discardableResult private func openURL(_ url: URL) -> Bool
{
if url.isFileURL
{ {
guard url.isFileURL else { return false }
if GameType(fileExtension: url.pathExtension) != nil || url.pathExtension.lowercased() == "zip" if GameType(fileExtension: url.pathExtension) != nil || url.pathExtension.lowercased() == "zip"
{ {
return self.importGame(at: url) return self.importGame(at: url)
@ -102,6 +169,15 @@ extension AppDelegate
{ {
return self.importControllerSkin(at: url) return self.importControllerSkin(at: url)
} }
}
else if url.scheme?.hasPrefix("db-") == true
{
return DropboxService.shared.handleDropboxURL(url)
}
else if url.scheme?.lowercased() == "delta"
{
return self.deepLinkController.handle(.url(url))
}
return false return false
} }
@ -145,3 +221,30 @@ extension AppDelegate
} }
} }
extension AppDelegate
{
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void)
{
let result = self.deepLinkController.handle(.shortcut(shortcutItem))
completionHandler(result)
}
}
private extension AppDelegate
{
@objc func databaseManagerDidStart(_ notification: Notification)
{
guard let deepLink = self.appLaunchDeepLink else { return }
DispatchQueue.main.async {
self.deepLinkController.handle(deepLink)
}
}
func receivedApplicationStateRequest()
{
let center = CFNotificationCenterGetDarwinNotifyCenter()
CFNotificationCenterPostNotification(center!, CFNotificationName(CFNotificationName.altstoreAppIsRunning.rawValue), nil, nil, true)
}
}

View File

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU">
<device id="retina4_7" orientation="portrait"> <device id="retina4_7" orientation="portrait" appearance="light"/>
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@ -18,52 +15,6 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> <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> <connections>
<outlet property="dataSource" destination="SB6-jW-dhZ" id="2aq-ZA-84E"/> <outlet property="dataSource" destination="SB6-jW-dhZ" id="2aq-ZA-84E"/>
<outlet property="delegate" destination="SB6-jW-dhZ" id="WgY-cp-m7K"/> <outlet property="delegate" destination="SB6-jW-dhZ" id="WgY-cp-m7K"/>
@ -72,7 +23,7 @@
<navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR"> <navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff"> <barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff">
<connections> <connections>
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindFromGamesDatabaseBrowserWith:" id="zdg-Az-WwQ"/> <segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindToGameCollectionViewController:" id="nzI-4n-kDg"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
@ -101,7 +52,4 @@
<point key="canvasLocation" x="1854" y="1002"/> <point key="canvasLocation" x="1854" y="1002"/>
</scene> </scene>
</scenes> </scenes>
<resources>
<image name="BoxArt" width="100" height="100"/>
</resources>
</document> </document>

View File

@ -1,13 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?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="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="dkK-ii-Bx4">
<device id="retina4_7" orientation="portrait"> <device id="retina4_7" orientation="portrait" appearance="light"/>
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11755"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<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"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@ -23,21 +19,32 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Delta" translatesAutoresizingMaskIntoConstraints="NO" id="plh-tL-LY0"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" insetsLayoutMarginsFromSafeArea="NO" image="LaunchViewC" translatesAutoresizingMaskIntoConstraints="NO" id="5XD-I3-tLg">
<rect key="frame" x="94" y="250" width="187.5" height="167"/> <rect key="frame" x="0.0" y="50" width="375" height="378"/>
<constraints>
<constraint firstAttribute="width" secondItem="plh-tL-LY0" secondAttribute="height" multiplier="64:57" id="8qM-L2-ASa"/>
</constraints>
</imageView> </imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Retro Game Emulator" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vhb-Xd-o6a">
<rect key="frame" x="55.5" y="448" width="264" height="33.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="28"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" alpha="0.44999998807907104" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ENJOY CLASSIC MOMENT" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wWH-Lx-U9x">
<rect key="frame" x="90.5" y="513.5" width="194.5" height="19.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews> </subviews>
<color key="backgroundColor" white="0.14728124936421713" alpha="1" colorSpace="calibratedWhite"/> <color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstItem="plh-tL-LY0" firstAttribute="width" relation="lessThanOrEqual" secondItem="8Uu-wz-ps8" secondAttribute="width" multiplier="0.5" id="8j9-39-Y2s"/> <constraint firstItem="5XD-I3-tLg" firstAttribute="top" secondItem="qMb-3x-uIu" secondAttribute="bottom" constant="30" id="Aiv-ac-bYx"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="centerY" secondItem="8Uu-wz-ps8" secondAttribute="centerY" id="COW-Co-NFK"/> <constraint firstItem="wWH-Lx-U9x" firstAttribute="centerX" secondItem="vhb-Xd-o6a" secondAttribute="centerX" id="DEu-U7-qVq"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="height" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="0.5" priority="900" id="G3L-7B-xVc"/> <constraint firstAttribute="trailing" secondItem="5XD-I3-tLg" secondAttribute="trailing" id="Gc5-y0-Vsy"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="width" secondItem="8Uu-wz-ps8" secondAttribute="width" multiplier="0.5" priority="950" id="n3i-kS-7eQ"/> <constraint firstItem="vhb-Xd-o6a" firstAttribute="top" secondItem="5XD-I3-tLg" secondAttribute="bottom" constant="20" id="Ncn-Yh-ecr"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="centerX" secondItem="8Uu-wz-ps8" secondAttribute="centerX" id="sp5-Kf-N7G"/> <constraint firstItem="5XD-I3-tLg" firstAttribute="leading" secondItem="8Uu-wz-ps8" secondAttribute="leading" id="SSl-CS-XOC"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="height" relation="lessThanOrEqual" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="0.5" id="ubN-Qh-I5H"/> <constraint firstItem="5XD-I3-tLg" firstAttribute="height" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="1.7:3" id="WGx-z3-vXf"/>
<constraint firstItem="wWH-Lx-U9x" firstAttribute="top" secondItem="vhb-Xd-o6a" secondAttribute="bottom" constant="32" id="lVY-5z-fz7"/>
<constraint firstItem="vhb-Xd-o6a" firstAttribute="centerX" secondItem="8Uu-wz-ps8" secondAttribute="centerX" id="oCf-dA-bJX"/>
</constraints> </constraints>
</view> </view>
</viewController> </viewController>
@ -47,6 +54,6 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="Delta" width="1280" height="1140"/> <image name="LaunchViewC" width="375" height="472.5"/>
</resources> </resources>
</document> </document>

View File

@ -1,74 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" 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="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
<device id="retina4_7" orientation="portrait"> <device id="retina4_7" orientation="portrait" appearance="light"/>
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Games--> <!--Games View Controller-->
<scene sceneID="Cd2-Pf-cua"> <scene sceneID="Cd2-Pf-cua">
<objects> <objects>
<viewController id="jeE-WD-wXO" customClass="GamesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="jeE-WD-wXO" customClass="GamesViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides> <layoutGuides>
<viewControllerLayoutGuide type="top" id="WoX-O4-qy5"/> <viewControllerLayoutGuide type="top" id="WoX-O4-qy5"/>
<viewControllerLayoutGuide type="bottom" id="0om-QB-N5a"/> <viewControllerLayoutGuide type="bottom" id="0om-QB-N5a"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="3Bk-k3-7J9"> <view key="view" contentMode="scaleToFill" id="3Bk-k3-7J9">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tmn-gd-5UN"> <containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tmn-gd-5UN">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<connections> <connections>
<segue destination="tpK-ou-yEA" kind="embed" identifier="embedPageViewController" id="cjU-nW-cHY"/> <segue destination="tpK-ou-yEA" kind="embed" identifier="embedPageViewController" id="cjU-nW-cHY"/>
</connections> </connections>
</containerView> </containerView>
</subviews> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="J8K-ZI-4X1">
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <rect key="frame" x="0.0" y="56" width="375" height="591"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CxB-GP-B6S">
<rect key="frame" x="0.0" y="538" width="375" height="109"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="home_tab"/>
<connections>
<segue destination="xMK-Cs-fAS" kind="presentation" modalPresentationStyle="fullScreen" id="bxS-9X-3vh"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="n4H-Kw-HPj">
<rect key="frame" x="143.5" y="517" width="100" height="100"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="100" id="SLR-9h-t9y"/>
<constraint firstAttribute="width" constant="100" id="Tdv-6u-nlA"/>
</constraints>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="addtwo"/>
<connections>
<action selector="importfilesBtn:" destination="jeE-WD-wXO" eventType="touchUpInside" id="T2w-zr-j8V"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="CxB-GP-B6S" secondAttribute="trailing" id="5sO-AB-YB5"/>
<constraint firstAttribute="trailing" secondItem="J8K-ZI-4X1" secondAttribute="trailing" id="7MY-qA-ANn"/>
<constraint firstAttribute="trailing" secondItem="tmn-gd-5UN" secondAttribute="trailing" id="9Rq-HM-vqk"/> <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="J8K-ZI-4X1" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="9kW-o9-BMv"/>
<constraint firstItem="CxB-GP-B6S" firstAttribute="centerX" secondItem="3Bk-k3-7J9" secondAttribute="centerX" id="Dbp-v1-mUp"/>
<constraint firstItem="n4H-Kw-HPj" firstAttribute="centerX" secondItem="3Bk-k3-7J9" secondAttribute="centerX" constant="6" id="H9T-X7-RK8"/>
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="n4H-Kw-HPj" secondAttribute="bottom" constant="30" id="QHB-jA-R0s"/>
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="CxB-GP-B6S" secondAttribute="bottom" id="QzJ-Kr-VZk"/>
<constraint firstItem="CxB-GP-B6S" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="axc-ed-3zE"/>
<constraint firstItem="tmn-gd-5UN" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="f1f-sa-dBA"/> <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="J8K-ZI-4X1" firstAttribute="top" secondItem="WoX-O4-qy5" secondAttribute="bottom" id="jaI-AF-tpn"/>
<constraint firstItem="tmn-gd-5UN" firstAttribute="top" secondItem="3Bk-k3-7J9" secondAttribute="top" id="nhS-aC-rUR"/> <constraint firstItem="tmn-gd-5UN" firstAttribute="top" secondItem="3Bk-k3-7J9" secondAttribute="top" id="nhS-aC-rUR"/>
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="J8K-ZI-4X1" secondAttribute="bottom" id="tvh-Sd-zA1"/>
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" title="Games" id="pFk-as-3k4"> <navigationItem key="navigationItem" id="pFk-as-3k4">
<barButtonItem key="leftBarButtonItem" image="Settings_Button" id="2gg-lC-FhX"> <barButtonItem key="leftBarButtonItem" image="home" id="2gg-lC-FhX"/>
<connections> <barButtonItem key="rightBarButtonItem" style="plain" id="FeA-O5-xd2">
<segue destination="xMK-Cs-fAS" kind="presentation" id="uN5-PN-7FK"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" systemItem="add" id="FeA-O5-xd2">
<connections> <connections>
<action selector="importFiles" destination="jeE-WD-wXO" id="A1s-kE-NkM"/> <action selector="importFiles" destination="jeE-WD-wXO" id="A1s-kE-NkM"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
<connections>
<outlet property="importButton" destination="FeA-O5-xd2" id="A44-3S-Okz"/>
</connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1036" y="1002"/> <point key="canvasLocation" x="1036" y="1001.649175412294"/>
</scene> </scene>
<!--Game Collection View Controller--> <!--Game Collection View Controller-->
<scene sceneID="qNA-NP-TiF"> <scene sceneID="qNA-NP-TiF">
<objects> <objects>
<collectionViewController storyboardIdentifier="gameCollectionViewController" clearsSelectionOnViewWillAppear="NO" id="kqu-75-owz" customClass="GameCollectionViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <collectionViewController storyboardIdentifier="gameCollectionViewController" clearsSelectionOnViewWillAppear="NO" id="kqu-75-owz" customClass="GameCollectionViewController" customModule="Retro_Game_Emulator" 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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <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"/> <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="NKN-dd-bTh" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target"> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="10" id="NKN-dd-bTh" customClass="GridCollectionViewLayout" customModule="Retro_Game_Emulator" customModuleProvider="target">
<size key="itemSize" width="100" height="100"/> <size key="itemSize" width="100" height="100"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/> <size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/> <size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="20" maxX="0.0" maxY="20"/> <inset key="sectionInset" minX="0.0" minY="20" maxX="0.0" maxY="20"/>
</collectionViewFlowLayout> </collectionViewFlowLayout>
<cells> <cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ioT-sh-j8y" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target"> <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ioT-sh-j8y" customClass="GridCollectionViewCell" customModule="Retro_Game_Emulator" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="100" height="100"/> <rect key="frame" x="0.0" y="20" width="100" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
@ -84,7 +114,9 @@
</collectionView> </collectionView>
<connections> <connections>
<segue destination="X2o-q6-XD5" kind="unwind" identifier="unwindFromGames" unwindAction="unwindFromGamesViewControllerWith:" id="k8C-Xn-maU"/> <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="MPk-bF-nkj" kind="presentation" identifier="saveStates" customClass="SaveStatesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="1Xp-2J-0cq"/>
<segue destination="qdE-gb-V2e" kind="presentation" identifier="preferredControllerSkins" id="i6y-cP-3WM"/>
<segue destination="V2x-v0-jWm" kind="presentation" identifier="showDSSettings" id="kuV-tY-Y0B"/>
</connections> </connections>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bW1-t8-idm" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="bW1-t8-idm" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -95,7 +127,7 @@
<!--Launch View Controller--> <!--Launch View Controller-->
<scene sceneID="p7y-IT-nlb"> <scene sceneID="p7y-IT-nlb">
<objects> <objects>
<viewController id="SPq-Bk-fQl" customClass="LaunchViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="SPq-Bk-fQl" customClass="LaunchViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides> <layoutGuides>
<viewControllerLayoutGuide type="top" id="Qap-U8-zpQ"/> <viewControllerLayoutGuide type="top" id="Qap-U8-zpQ"/>
<viewControllerLayoutGuide type="bottom" id="dca-QO-wba"/> <viewControllerLayoutGuide type="bottom" id="dca-QO-wba"/>
@ -114,12 +146,38 @@
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vf5-Iy-lAb" userLabel="Launch Screen"> <containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vf5-Iy-lAb" userLabel="Launch Screen">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections> <connections>
<segue destination="ibA-aC-X3M" kind="embed" id="fsv-uf-AOE"/> <segue destination="ibA-aC-X3M" kind="embed" id="fsv-uf-AOE"/>
</connections> </connections>
</containerView> </containerView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Mgn-IF-ax7">
<rect key="frame" x="187.5" y="448" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="28"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" alpha="0.44999998807907104" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wnW-MB-udh">
<rect key="frame" x="187.5" y="480" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="50O-wR-1hq">
<rect key="frame" x="0.0" y="50" width="375" height="378"/>
</imageView>
</subviews> </subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> <color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="50O-wR-1hq" secondAttribute="trailing" id="2Xh-ZZ-Dbc"/>
<constraint firstItem="50O-wR-1hq" firstAttribute="leading" secondItem="8jv-0a-ItC" secondAttribute="leading" id="6rJ-Cz-C3Z"/>
<constraint firstItem="Mgn-IF-ax7" firstAttribute="centerX" secondItem="8jv-0a-ItC" secondAttribute="centerX" id="8kV-gZ-4TW"/>
<constraint firstItem="50O-wR-1hq" firstAttribute="height" secondItem="8jv-0a-ItC" secondAttribute="height" multiplier="1.7:3" id="FTt-Ru-HhA"/>
<constraint firstItem="wnW-MB-udh" firstAttribute="top" secondItem="Mgn-IF-ax7" secondAttribute="bottom" constant="32" id="IAR-im-kU7"/>
<constraint firstItem="50O-wR-1hq" firstAttribute="top" secondItem="Qap-U8-zpQ" secondAttribute="bottom" constant="30" id="bo1-Am-BdP"/>
<constraint firstItem="wnW-MB-udh" firstAttribute="centerX" secondItem="8jv-0a-ItC" secondAttribute="centerX" id="djF-KX-NgM"/>
<constraint firstItem="Mgn-IF-ax7" firstAttribute="top" secondItem="50O-wR-1hq" secondAttribute="bottom" constant="20" id="igk-jf-lgM"/>
</constraints>
</view> </view>
<connections> <connections>
<outlet property="gameViewContainerView" destination="oBZ-xU-jeC" id="jMI-iF-JlU"/> <outlet property="gameViewContainerView" destination="oBZ-xU-jeC" id="jMI-iF-JlU"/>
@ -140,7 +198,7 @@
<!--Game View Controller--> <!--Game View Controller-->
<scene sceneID="ASV-Uk-0aP"> <scene sceneID="ASV-Uk-0aP">
<objects> <objects>
<viewController id="yhz-fF-D91" customClass="GameViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="yhz-fF-D91" customClass="GameViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides> <layoutGuides>
<viewControllerLayoutGuide type="top" id="ItC-Bu-WRI"/> <viewControllerLayoutGuide type="top" id="ItC-Bu-WRI"/>
<viewControllerLayoutGuide type="bottom" id="g58-HO-6L5"/> <viewControllerLayoutGuide type="bottom" id="g58-HO-6L5"/>
@ -148,12 +206,23 @@
<view key="view" contentMode="scaleToFill" id="skW-1S-YD4"> <view key="view" contentMode="scaleToFill" id="skW-1S-YD4">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="0b6-zE-QS0">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.02214059979" green="0.092763282360000004" blue="0.18196243049999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="0b6-zE-QS0" secondAttribute="trailing" id="5n6-uA-WLa"/>
<constraint firstItem="0b6-zE-QS0" firstAttribute="top" secondItem="ItC-Bu-WRI" secondAttribute="bottom" id="DTE-cg-9BW"/>
<constraint firstItem="0b6-zE-QS0" firstAttribute="leading" secondItem="skW-1S-YD4" secondAttribute="leading" id="Ej3-6D-wcR"/>
<constraint firstItem="g58-HO-6L5" firstAttribute="top" secondItem="0b6-zE-QS0" secondAttribute="bottom" id="ozO-Ni-qpr"/>
</constraints>
</view> </view>
<connections> <connections>
<segue destination="Yrw-9v-Pcr" kind="presentation" identifier="pause" customClass="PauseStoryboardSegue" customModule="Delta" customModuleProvider="target" id="FLq-Zt-HDv"/> <segue destination="Yrw-9v-Pcr" kind="presentation" identifier="pause" customClass="PauseStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="FLq-Zt-HDv"/>
<segue destination="wKV-3d-NIY" kind="presentation" identifier="showGamesViewController" customClass="GamesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="Tey-6Z-UHp"/> <segue destination="wKV-3d-NIY" kind="presentation" identifier="showGamesViewController" customClass="GamesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="Tey-6Z-UHp"/>
<segue destination="wKV-3d-NIY" kind="presentation" identifier="showInitialGamesViewController" customClass="InitialGamesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="eR2-0c-0Rv"/> <segue destination="wKV-3d-NIY" kind="presentation" identifier="showInitialGamesViewController" customClass="InitialGamesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="eR2-0c-0Rv"/>
</connections> </connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="gxI-00-NlJ" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="gxI-00-NlJ" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -171,10 +240,12 @@
<!--Settings--> <!--Settings-->
<scene sceneID="L3X-MM-hJL"> <scene sceneID="L3X-MM-hJL">
<objects> <objects>
<viewControllerPlaceholder storyboardName="Settings" id="xMK-Cs-fAS" sceneMemberID="viewController"/> <viewControllerPlaceholder storyboardName="Settings" id="xMK-Cs-fAS" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="7yR-QM-2bX"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="2N5-3k-beA" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="2N5-3k-beA" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1036" y="605"/> <point key="canvasLocation" x="1578" y="774"/>
</scene> </scene>
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="zJI-sC-1sm"> <scene sceneID="zJI-sC-1sm">
@ -183,12 +254,14 @@
<toolbarItems/> <toolbarItems/>
<nil key="simulatedBottomBarMetrics"/> <nil key="simulatedBottomBarMetrics"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="wj9-1e-eev"> <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="0.0" width="375" height="56"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" red="0.032843004910000001" green="0.076728828250000006" blue="0.13726195690000001" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="M4r-sO-G4H"> <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"/> <rect key="frame" x="0.0" y="-20" width="0.0" height="0.0"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</toolbar> </toolbar>
<connections> <connections>
@ -210,7 +283,7 @@
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="nR0-Va-AI1"> <scene sceneID="nR0-Va-AI1">
<objects> <objects>
<navigationController storyboardIdentifier="saveStatesNavigationController" automaticallyAdjustsScrollViewInsets="NO" id="MPk-bF-nkj" sceneMemberID="viewController"> <navigationController storyboardIdentifier="saveStatesNavigationController" automaticallyAdjustsScrollViewInsets="NO" modalPresentationStyle="fullScreen" id="MPk-bF-nkj" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="twH-3X-6DV"> <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="0.0" width="375" height="44"/>
@ -219,7 +292,7 @@
<nil name="viewControllers"/> <nil name="viewControllers"/>
<connections> <connections>
<segue destination="Eae-Qk-9MI" kind="relationship" relationship="rootViewController" id="1Jh-Zf-ntp"/> <segue destination="Eae-Qk-9MI" kind="relationship" relationship="rootViewController" id="1Jh-Zf-ntp"/>
<segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Delta" customModuleProvider="target" unwindAction="unwindFromSaveStatesViewControllerWith:" id="dwO-iv-XDr"/> <segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" unwindAction="unwindToGameCollectionViewController:" id="dwO-iv-XDr"/>
</connections> </connections>
</navigationController> </navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="htj-tq-2KP" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="htj-tq-2KP" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -227,6 +300,16 @@
</objects> </objects>
<point key="canvasLocation" x="2652" y="1718"/> <point key="canvasLocation" x="2652" y="1718"/>
</scene> </scene>
<!--preferredControllerSkins-->
<scene sceneID="aKY-Ld-et6">
<objects>
<viewControllerPlaceholder storyboardName="Settings" referencedIdentifier="preferredControllerSkins" id="dbc-pQ-iun" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="xth-MV-SHp"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="za6-AO-ZFe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3405" y="2394"/>
</scene>
<!--saveStatesViewController--> <!--saveStatesViewController-->
<scene sceneID="f1R-Kb-FOU"> <scene sceneID="f1R-Kb-FOU">
<objects> <objects>
@ -237,11 +320,46 @@
</objects> </objects>
<point key="canvasLocation" x="3409" y="1716"/> <point key="canvasLocation" x="3409" y="1716"/>
</scene> </scene>
<!--Navigation Controller-->
<scene sceneID="eMh-8N-ZGA">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qdE-gb-V2e" sceneMemberID="viewController">
<toolbarItems/>
<navigationItem key="navigationItem" id="Dg6-He-v5H"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="35T-4Q-Mmp">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="dbc-pQ-iun" kind="relationship" relationship="rootViewController" id="oRb-B6-c0J"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="XmB-QY-yA3" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2652" y="2394"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="OW2-zT-pbF">
<objects>
<navigationController id="V2x-v0-jWm" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="pjb-4I-yar">
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Jo9-gl-p5p" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2652" y="3085"/>
</scene>
</scenes> </scenes>
<resources>
<image name="Settings_Button" width="22" height="22"/>
</resources>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="Tey-6Z-UHp"/> <segue reference="Tey-6Z-UHp"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
<resources>
<image name="addtwo" width="519" height="519"/>
<image name="bg" width="375" height="812"/>
<image name="home" width="92" height="37"/>
<image name="home_tab" width="384.5" height="109"/>
</resources>
</document> </document>

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<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"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11151.4"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@ -20,16 +21,19 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p2M-dE-BJs" userLabel="Blur View"> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p2M-dE-BJs" userLabel="Blur View">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="eyD-0d-RHe" userLabel="Blur Content View"> <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="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rqN-NB-jbb"> <containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rqN-NB-jbb">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<connections> <connections>
<segue destination="sWv-Ky-VGs" kind="embed" identifier="embedNavigationController" id="1Ja-XW-uoT"/> <segue destination="sWv-Ky-VGs" kind="embed" identifier="embedNavigationController" id="1Ja-XW-uoT"/>
</connections> </connections>
</containerView> </containerView>
</subviews> </subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="calibratedRGB"/>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="rqN-NB-jbb" secondAttribute="bottom" id="3XJ-2M-uVD"/> <constraint firstAttribute="bottom" secondItem="rqN-NB-jbb" secondAttribute="bottom" id="3XJ-2M-uVD"/>
<constraint firstAttribute="trailing" secondItem="rqN-NB-jbb" secondAttribute="trailing" id="NQ7-cS-8T5"/> <constraint firstAttribute="trailing" secondItem="rqN-NB-jbb" secondAttribute="trailing" id="NQ7-cS-8T5"/>
@ -41,10 +45,10 @@
</visualEffectView> </visualEffectView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="p2M-dE-BJs" secondAttribute="bottom" id="70W-aM-NX0"/>
<constraint firstItem="p2M-dE-BJs" firstAttribute="top" secondItem="oOH-ea-jcb" secondAttribute="top" id="8tp-qg-fgz"/> <constraint firstItem="p2M-dE-BJs" firstAttribute="top" secondItem="oOH-ea-jcb" secondAttribute="top" id="8tp-qg-fgz"/>
<constraint firstAttribute="trailing" secondItem="p2M-dE-BJs" secondAttribute="trailing" id="Idx-Ok-WhM"/> <constraint firstAttribute="trailing" secondItem="p2M-dE-BJs" secondAttribute="trailing" id="Idx-Ok-WhM"/>
<constraint firstItem="p2M-dE-BJs" firstAttribute="leading" secondItem="oOH-ea-jcb" secondAttribute="leading" id="Ppi-05-jHX"/> <constraint firstItem="p2M-dE-BJs" firstAttribute="leading" secondItem="oOH-ea-jcb" secondAttribute="leading" id="Ppi-05-jHX"/>
<constraint firstItem="gF0-0U-kR7" firstAttribute="top" secondItem="p2M-dE-BJs" secondAttribute="bottom" id="eFj-ha-zJQ"/>
</constraints> </constraints>
</view> </view>
<connections> <connections>
@ -63,8 +67,8 @@
<objects> <objects>
<navigationController id="sWv-Ky-VGs" sceneMemberID="viewController"> <navigationController id="sWv-Ky-VGs" sceneMemberID="viewController">
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<connections> <connections>
@ -78,7 +82,7 @@
<!--Paused--> <!--Paused-->
<scene sceneID="1md-hu-g0J"> <scene sceneID="1md-hu-g0J">
<objects> <objects>
<collectionViewController id="0jA-NY-mvB" customClass="PauseMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <collectionViewController id="0jA-NY-mvB" customClass="GridMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" delaysContentTouches="NO" dataMode="prototypes" id="scc-uc-vaJ"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -91,7 +95,7 @@
</collectionViewFlowLayout> </collectionViewFlowLayout>
<cells> <cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="6XS-Ne-nGZ" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="60" height="80"/> <rect key="frame" x="0.0" y="0.0" width="60" height="80"/>
@ -128,7 +132,7 @@
<objects> <objects>
<collectionViewController storyboardIdentifier="saveStatesViewController" id="OOk-k7-INg" customClass="SaveStatesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <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"> <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="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <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"/> <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"> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="tvW-q1-PD8" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
@ -139,7 +143,7 @@
</collectionViewFlowLayout> </collectionViewFlowLayout>
<cells> <cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="c3N-1A-ryV" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/> <rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
@ -148,7 +152,7 @@
</collectionViewCell> </collectionViewCell>
</cells> </cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Header" id="YeY-W9-CC6" customClass="SaveStatesCollectionHeaderView" customModule="Delta" customModuleProvider="target"> <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="375" height="50"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</collectionReusableView> </collectionReusableView>
<connections> <connections>
@ -157,13 +161,28 @@
</connections> </connections>
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="Save State" id="BoG-k2-aR2"> <navigationItem key="navigationItem" title="Save State" id="BoG-k2-aR2">
<barButtonItem key="rightBarButtonItem" systemItem="add" id="lKg-Ks-hWN"> <rightBarButtonItems>
<barButtonItem systemItem="add" id="lKg-Ks-hWN">
<connections> <connections>
<action selector="addSaveState" destination="OOk-k7-INg" id="xY2-94-EOr"/> <action selector="addSaveState" destination="OOk-k7-INg" id="xY2-94-EOr"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
<barButtonItem style="plain" id="has-I3-HDZ">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="y2a-9f-EFz">
<rect key="frame" x="288.5" y="13" width="30" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="▼"/>
<connections>
<action selector="changeSortOrder:" destination="OOk-k7-INg" eventType="primaryActionTriggered" id="qQn-uw-SN1"/>
</connections>
</button>
</barButtonItem>
</rightBarButtonItems>
</navigationItem> </navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<connections>
<outlet property="sortButton" destination="y2a-9f-EFz" id="Zbo-Q0-bVL"/>
</connections>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cL5-DH-K3b" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="cL5-DH-K3b" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
@ -174,22 +193,22 @@
<objects> <objects>
<tableViewController id="wb8-5o-1jE" customClass="CheatsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <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"> <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="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <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"/> <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="e8g-ZW-5lQ" customClass="CheatTableViewCell" customModule="Delta" customModuleProvider="target"> <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="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="e8g-ZW-5lQ" id="AHE-Jk-ULE"> <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="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="emc-gw-KkJ" userLabel="Selected Background View"> <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="375" height="45"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" id="9bA-Tg-Bko"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="9bA-Tg-Bko">
<frame key="frameInset"/> <rect key="frame" x="0.0" y="0.0" width="375" height="45"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view> </view>
@ -198,8 +217,9 @@
</vibrancyEffect> </vibrancyEffect>
</visualEffectView> </visualEffectView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="R4A-9d-DGb"> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="R4A-9d-DGb">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="EdX-fU-x54"> <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="375" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view> </view>
<vibrancyEffect> <vibrancyEffect>
@ -247,18 +267,19 @@
<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"> <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="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <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"/> <color key="backgroundColor" red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<sections> <sections>
<tableViewSection headerTitle="Name" id="QT6-DZ-g70"> <tableViewSection headerTitle="Name" id="QT6-DZ-g70">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ZeC-rg-QFa"> <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="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="ZeC-rg-QFa" id="UIF-fK-ApW"> <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="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <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"> <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="16" y="0.0" width="343" height="44"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" returnKeyType="done"/> <textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" returnKeyType="done"/>
<connections> <connections>
@ -280,13 +301,14 @@
<tableViewSection headerTitle="Type" footerTitle="Description" id="rvn-VK-2uH"> <tableViewSection headerTitle="Type" footerTitle="Description" id="rvn-VK-2uH">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Tst-zn-e04"> <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="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Tst-zn-e04" id="gwV-zS-RWQ"> <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="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xrD-ue-96Q"> <segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xrD-ue-96Q">
<rect key="frame" x="16" y="6.5" width="343" height="32"/>
<segments> <segments>
<segment title="First"/> <segment title="First"/>
<segment title="Second"/> <segment title="Second"/>
@ -308,14 +330,15 @@
<tableViewSection headerTitle="Code" footerTitle="Description" id="rHC-nA-ga0"> <tableViewSection headerTitle="Code" footerTitle="Description" id="rHC-nA-ga0">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="210" id="xxc-cz-sb7"> <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="375" height="210"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="xxc-cz-sb7" id="agU-SE-fNa"> <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="375" height="210"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target">
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <rect key="frame" x="0.0" y="0.0" width="375" height="210"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/> <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/>
<textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/> <textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/>
<connections> <connections>
@ -376,4 +399,9 @@
<point key="canvasLocation" x="2385" y="1377"/> <point key="canvasLocation" x="2385" y="1377"/>
</scene> </scene>
</scenes> </scenes>
<resources>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document> </document>

View File

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES"> <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> <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> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PausePresentationController" customModule="Delta" customModuleProvider="target"> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PausePresentationController" customModule="Delta" customModuleProvider="target">
@ -13,14 +18,14 @@
</placeholder> </placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB"> <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"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="qFz-hB-77X"> <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> <subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="500" image="Pause" translatesAutoresizingMaskIntoConstraints="NO" id="cWa-Ht-i5m"> <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> <constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" priority="900" constant="44" id="xki-9P-sHi"> <constraint firstAttribute="height" relation="greaterThanOrEqual" priority="900" constant="44" id="xki-9P-sHi">
<variation key="heightClass=compact" constant="25"/> <variation key="heightClass=compact" constant="25"/>
@ -28,9 +33,9 @@
</constraints> </constraints>
</imageView> </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"> <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"/> <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"/> <nil key="highlightedColor"/>
</label> </label>
</subviews> </subviews>

View File

@ -0,0 +1,846 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Settings-->
<scene sceneID="N30-Sa-dHo">
<objects>
<tableViewController title="Settings" id="eHi-aO-uGS" customClass="SettingsViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="hRw-wV-lch">
<rect key="frame" x="0.0" y="0.0" width="375" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="separatorColor" systemColor="separatorColor"/>
<sections>
<tableViewSection headerTitle="Controllers" id="c6K-sJ-0vW">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="oDc-U1-cH3" detailTextLabel="czc-3c-hwp" style="IBUITableViewCellStyleValue1" id="jvV-ZB-Rq1">
<rect key="frame" x="16" y="38" width="343" 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="316.5" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Player 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="oDc-U1-cH3">
<rect key="frame" x="16" y="13" width="56" height="19.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="czc-3c-hwp">
<rect key="frame" x="266.5" y="13" width="42" height="19.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" systemColor="lightTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableViewCellContentView>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="e3u-x9-IEC" detailTextLabel="2OP-A1-VYo" style="IBUITableViewCellStyleValue1" id="1Fv-H5-0oH">
<rect key="frame" x="16" y="82" width="343" 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="316.5" height="44"/>
<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="16" y="13" width="58" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="SteelSeries Stratus" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2OP-A1-VYo">
<rect key="frame" x="170" y="13" width="138.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" systemColor="lightTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.074800990519999999" green="0.081782229240000004" blue="0.1058854684" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="Cdn-11-xZe" detailTextLabel="wWc-NY-Bsd" style="IBUITableViewCellStyleValue1" id="EcC-Be-jV5">
<rect key="frame" x="16" y="126" width="343" 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="316.5" height="44"/>
<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="16" y="13" width="58.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="MOGA Gamepad" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wWc-NY-Bsd">
<rect key="frame" x="186.5" y="13" width="122" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" systemColor="lightTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.074800990519999999" green="0.081782229240000004" blue="0.1058854684" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="Hls-3b-EaS" detailTextLabel="hNf-uc-PLR" style="IBUITableViewCellStyleValue1" id="hO9-Ov-vsA">
<rect key="frame" x="16" y="170" width="343" 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="316.5" height="44"/>
<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="16" y="13" width="59" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Jayce's iPhone" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hNf-uc-PLR">
<rect key="frame" x="200" y="13" width="108.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" systemColor="lightTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.070923872289999995" green="0.081780366600000001" blue="0.10588551309999999" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Controller Skins" id="Nch-k1-6pR">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" id="ICf-ug-NwS">
<rect key="frame" x="16" y="270" width="343" 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="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="game" translatesAutoresizingMaskIntoConstraints="NO" id="pZJ-Xw-wbK">
<rect key="frame" x="20" y="12" width="20" height="20"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Game Boy Color" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="L97-UB-99v">
<rect key="frame" x="48" y="13.5" width="103" height="17"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="baockgo" translatesAutoresizingMaskIntoConstraints="NO" id="f28-XT-Xqv">
<rect key="frame" x="303" y="12" width="20" height="20"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="pZJ-Xw-wbK" firstAttribute="leading" secondItem="7se-sE-x9e" secondAttribute="leading" constant="20" id="8Ju-vf-sg8"/>
<constraint firstItem="pZJ-Xw-wbK" firstAttribute="centerY" secondItem="7se-sE-x9e" secondAttribute="centerY" id="Buh-qm-B9i"/>
<constraint firstItem="L97-UB-99v" firstAttribute="leading" secondItem="pZJ-Xw-wbK" secondAttribute="trailing" constant="8" id="T6f-Mp-Etv"/>
<constraint firstAttribute="trailing" secondItem="f28-XT-Xqv" secondAttribute="trailing" constant="20" id="Y6B-0R-HYa"/>
<constraint firstItem="L97-UB-99v" firstAttribute="centerY" secondItem="7se-sE-x9e" secondAttribute="centerY" id="aRg-pc-aaY"/>
<constraint firstItem="f28-XT-Xqv" firstAttribute="centerY" secondItem="7se-sE-x9e" secondAttribute="centerY" id="i7Y-Js-8KD"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="Dxs-Me-IVU" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="Hqy-yc-Jef">
<rect key="frame" x="16" y="314" width="343" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<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="316.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<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="292.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="o9x-Kn-6bC" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="jFa-Qk-1cj">
<rect key="frame" x="16" y="314.00000011920929" width="343" height="1.1920928955078125e-07"/>
<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="316.5" height="1.1920928955078125e-07"/>
<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="292.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="XVO-TO-ncw" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="vIu-iy-kRM">
<rect key="frame" x="16" y="314.00000023841858" width="343" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="vIu-iy-kRM" id="FIZ-uw-fR7">
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="XVO-TO-ncw">
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="vV9-Fk-zd5" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="Dfy-MJ-39n">
<rect key="frame" x="16" y="314.00000035762787" width="343" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Dfy-MJ-39n" id="dgi-73-brN">
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="vV9-Fk-zd5">
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="Etp-g5-W9R" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="p69-Xz-VoS">
<rect key="frame" x="16" y="314.00000047683716" width="343" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="p69-Xz-VoS" id="pwB-9y-EUf">
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Etp-g5-W9R">
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="sF6-5e-HL2" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="WVd-aL-SWy">
<rect key="frame" x="16" y="314.00000059604645" width="343" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="WVd-aL-SWy" id="uD0-3G-npJ">
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="sF6-5e-HL2">
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="other" id="SwK-m9-8gt">
<cells>
<tableViewCell contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="44" id="Xxk-vo-eu4">
<rect key="frame" x="16" y="370.00000071525574" width="343" 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="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="about" translatesAutoresizingMaskIntoConstraints="NO" id="Bvn-hc-pwv">
<rect key="frame" x="20" y="12" width="20" height="20"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="About" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JQ1-q7-Gxs">
<rect key="frame" x="48" y="13.5" width="39" height="17"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="baockgo" translatesAutoresizingMaskIntoConstraints="NO" id="deP-m5-YEL">
<rect key="frame" x="303" y="12" width="20" height="20"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hwG-bd-Nvz">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<connections>
<action selector="aboutbtn:" destination="eHi-aO-uGS" eventType="touchUpInside" id="3Zx-do-hZW"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="hwG-bd-Nvz" firstAttribute="top" secondItem="vxt-Ex-b4b" secondAttribute="top" id="0er-Nl-2S2"/>
<constraint firstAttribute="bottom" secondItem="hwG-bd-Nvz" secondAttribute="bottom" id="3d6-cf-80e"/>
<constraint firstItem="Bvn-hc-pwv" firstAttribute="leading" secondItem="vxt-Ex-b4b" secondAttribute="leading" constant="20" id="7Ot-0B-bZJ"/>
<constraint firstItem="JQ1-q7-Gxs" firstAttribute="centerY" secondItem="Bvn-hc-pwv" secondAttribute="centerY" id="Ggp-5n-s6x"/>
<constraint firstItem="deP-m5-YEL" firstAttribute="centerY" secondItem="vxt-Ex-b4b" secondAttribute="centerY" id="SMi-0Z-bS0"/>
<constraint firstItem="JQ1-q7-Gxs" firstAttribute="leading" secondItem="Bvn-hc-pwv" secondAttribute="trailing" constant="8" id="TGU-7v-ZAl"/>
<constraint firstAttribute="trailing" secondItem="deP-m5-YEL" secondAttribute="trailing" constant="20" id="g70-Mf-HKo"/>
<constraint firstAttribute="trailing" secondItem="hwG-bd-Nvz" secondAttribute="trailing" id="hma-YZ-euX"/>
<constraint firstItem="hwG-bd-Nvz" firstAttribute="leading" secondItem="vxt-Ex-b4b" secondAttribute="leading" id="ieP-nx-1SH"/>
<constraint firstItem="Bvn-hc-pwv" firstAttribute="centerY" secondItem="vxt-Ex-b4b" secondAttribute="centerY" id="oRX-Bk-F9q"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="44" id="acf-m6-fmX">
<rect key="frame" x="16" y="414.00000071525574" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="acf-m6-fmX" id="X3f-9y-xTV">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="privacy" translatesAutoresizingMaskIntoConstraints="NO" id="siI-Gj-0gv">
<rect key="frame" x="20" y="12" width="20" height="20"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Privacy Policy" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="L8X-tT-vJW">
<rect key="frame" x="48" y="13.5" width="88" height="17"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="baockgo" translatesAutoresizingMaskIntoConstraints="NO" id="NoK-Re-DcB">
<rect key="frame" x="303" y="12" width="20" height="20"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1xF-JV-zaQ">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<connections>
<action selector="privacybtn:" destination="eHi-aO-uGS" eventType="touchUpInside" id="sDx-xb-tmz"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="1xF-JV-zaQ" secondAttribute="bottom" id="6lW-J7-pcp"/>
<constraint firstItem="L8X-tT-vJW" firstAttribute="centerY" secondItem="X3f-9y-xTV" secondAttribute="centerY" id="EcD-Mb-AY8"/>
<constraint firstAttribute="trailing" secondItem="1xF-JV-zaQ" secondAttribute="trailing" id="M6D-4J-8BW"/>
<constraint firstItem="1xF-JV-zaQ" firstAttribute="leading" secondItem="X3f-9y-xTV" secondAttribute="leading" id="NyC-vL-YuI"/>
<constraint firstItem="L8X-tT-vJW" firstAttribute="leading" secondItem="siI-Gj-0gv" secondAttribute="trailing" constant="8" id="ZOi-xq-zNB"/>
<constraint firstAttribute="trailing" secondItem="NoK-Re-DcB" secondAttribute="trailing" constant="20" id="aXe-iE-zuL"/>
<constraint firstItem="siI-Gj-0gv" firstAttribute="leading" secondItem="X3f-9y-xTV" secondAttribute="leading" constant="20" id="arU-V7-rjZ"/>
<constraint firstItem="NoK-Re-DcB" firstAttribute="centerY" secondItem="X3f-9y-xTV" secondAttribute="centerY" id="jk6-yF-Z5S"/>
<constraint firstItem="1xF-JV-zaQ" firstAttribute="top" secondItem="X3f-9y-xTV" secondAttribute="top" id="kNk-0u-VE0"/>
<constraint firstItem="siI-Gj-0gv" firstAttribute="centerY" secondItem="X3f-9y-xTV" secondAttribute="centerY" id="rpD-ru-yjt"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="44" id="MrG-s8-yXA">
<rect key="frame" x="16" y="458.00000071525574" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="MrG-s8-yXA" id="6T0-pz-Vma">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="useragreement" translatesAutoresizingMaskIntoConstraints="NO" id="CSn-hu-wMa">
<rect key="frame" x="20" y="12" width="20" height="20"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="User Agreement" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OmR-EO-UWK">
<rect key="frame" x="48" y="13.5" width="104" height="17"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="baockgo" translatesAutoresizingMaskIntoConstraints="NO" id="O9E-CI-aBo">
<rect key="frame" x="303" y="12" width="20" height="20"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TgQ-rC-R47">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<connections>
<action selector="useragreemBtn:" destination="eHi-aO-uGS" eventType="touchUpInside" id="Rxb-QU-uBw"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="CSn-hu-wMa" firstAttribute="leading" secondItem="6T0-pz-Vma" secondAttribute="leading" constant="20" id="8aS-xw-48c"/>
<constraint firstItem="TgQ-rC-R47" firstAttribute="top" secondItem="6T0-pz-Vma" secondAttribute="top" id="9rp-bv-CGE"/>
<constraint firstItem="CSn-hu-wMa" firstAttribute="centerY" secondItem="6T0-pz-Vma" secondAttribute="centerY" id="FTm-wm-JMk"/>
<constraint firstItem="O9E-CI-aBo" firstAttribute="centerY" secondItem="6T0-pz-Vma" secondAttribute="centerY" id="TIz-3R-YbC"/>
<constraint firstAttribute="trailing" secondItem="O9E-CI-aBo" secondAttribute="trailing" constant="20" id="TyR-DE-Un5"/>
<constraint firstItem="TgQ-rC-R47" firstAttribute="leading" secondItem="6T0-pz-Vma" secondAttribute="leading" id="YFV-PF-z69"/>
<constraint firstAttribute="bottom" secondItem="TgQ-rC-R47" secondAttribute="bottom" id="dze-O5-QfT"/>
<constraint firstItem="OmR-EO-UWK" firstAttribute="centerY" secondItem="6T0-pz-Vma" secondAttribute="centerY" id="iYL-9B-bcl"/>
<constraint firstAttribute="trailing" secondItem="TgQ-rC-R47" secondAttribute="trailing" id="isn-Gv-lJy"/>
<constraint firstItem="OmR-EO-UWK" firstAttribute="leading" secondItem="CSn-hu-wMa" secondAttribute="trailing" constant="8" id="yu7-ce-uSe"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="aJ4-xT-aZL">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="0.0" id="bza-Nl-H39">
<rect key="frame" x="16" y="538.00000071525574" width="343" height="1.1920928955078125e-07"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="bza-Nl-H39" id="g7q-9D-XYR">
<rect key="frame" x="0.0" y="0.0" width="343" height="0.0"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="QWm-yw-YNv">
<rect key="frame" x="0.0" y="-17" width="343" height="34"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<connections>
<action selector="didmisiigame:" destination="eHi-aO-uGS" eventType="touchUpInside" id="4yk-aq-NQN"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="QWm-yw-YNv" firstAttribute="leading" secondItem="g7q-9D-XYR" secondAttribute="leading" id="XjX-7g-4Kw"/>
<constraint firstItem="QWm-yw-YNv" firstAttribute="centerY" secondItem="g7q-9D-XYR" secondAttribute="centerY" id="Zgp-AP-dw3"/>
<constraint firstAttribute="trailing" secondItem="QWm-yw-YNv" secondAttribute="trailing" id="gPG-We-VJt"/>
<constraint firstItem="QWm-yw-YNv" firstAttribute="centerX" secondItem="g7q-9D-XYR" secondAttribute="centerX" id="gzN-im-JAL"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="eHi-aO-uGS" id="rsh-0y-8Ex"/>
<outlet property="delegate" destination="eHi-aO-uGS" id="7cP-Sn-ONN"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Settings" largeTitleDisplayMode="always" id="DYU-tO-Lmw">
<barButtonItem key="rightBarButtonItem" title="back" style="done" id="UdW-1j-fSz">
<connections>
<segue destination="slQ-xv-zI1" kind="unwind" identifier="unwindSettingsSegue" unwindAction="unwindFromSettingsViewController:" id="7gb-Bj-XU3"/>
</connections>
</barButtonItem>
</navigationItem>
<size key="freeformSize" width="375" height="600"/>
<connections>
<segue destination="uBz-mm-mXr" kind="show" identifier="controllersSegue" id="MLY-hF-UB8"/>
<segue destination="56e-ul-z6v" kind="show" identifier="controllerSkinsSegue" id="GNM-Gt-YFf"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="19F-9T-esM" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="slQ-xv-zI1" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="1498" y="794"/>
</scene>
<!--Controls-->
<scene sceneID="Gi9-m1-y9x">
<objects>
<viewController title="Controls" id="x1g-pH-DnF" customClass="ControllerInputsViewController" customModule="Retro_Game_Emulator" 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="0.0" width="375" height="667"/>
<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 firstAttribute="bottom" 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="cPg-qa-ERT" secondAttribute="top" 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="unwindFromControllerInputsViewController:" id="8m5-qE-zxs"/>
</connections>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem style="done" systemItem="save" id="WHh-7W-jpl">
<connections>
<segue destination="8l5-7I-Z7e" kind="unwind" identifier="saveControllerInputs" unwindAction="unwindFromControllerInputsViewController:" id="b6G-wn-Tim"/>
</connections>
</barButtonItem>
<barButtonItem title="Reset" style="done" id="4jy-hy-YS3">
<color key="tintColor" systemColor="systemRedColor"/>
<connections>
<action selector="resetInputMapping:" destination="x1g-pH-DnF" id="flE-B2-TMu"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</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="4102" y="-244"/>
</scene>
<!--Controllers-->
<scene sceneID="swa-DT-VKS">
<objects>
<tableViewController id="uBz-mm-mXr" customClass="ControllersSettingsViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="hLd-Z5-I3b">
<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.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="VBO-V1-Wfu" detailTextLabel="tqn-1q-p53" style="IBUITableViewCellStyleValue1" id="lzU-uS-el2">
<rect key="frame" x="16" y="55.5" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="lzU-uS-el2" id="o56-OW-cxE">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<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="16" y="13" width="118.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<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="271" y="13" width="56" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="NoControllers" textLabel="OeA-dF-xlk" style="IBUITableViewCellStyleDefault" id="rCu-Pd-J3y">
<rect key="frame" x="16" y="99.5" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="rCu-Pd-J3y" id="JTP-bU-BBn">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="No Connected Controllers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="OeA-dF-xlk">
<rect key="frame" x="16" y="0.0" width="311" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="uBz-mm-mXr" id="XB4-jw-1fr"/>
<outlet property="delegate" destination="uBz-mm-mXr" id="tJX-mX-6j8"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Controllers" largeTitleDisplayMode="never" id="QK7-oi-2jJ"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<segue destination="0QR-U9-gtx" kind="presentation" identifier="controllerInputsSegue" modalPresentationStyle="fullScreen" id="E3Y-yV-zT5"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="owG-Kh-rfn" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2513" y="-244"/>
</scene>
<!--Game Boy Advance-->
<scene sceneID="pkL-Te-puh">
<objects>
<tableViewController storyboardIdentifier="preferredControllerSkins" id="56e-ul-z6v" customClass="PreferredControllerSkinsViewController" customModule="Retro_Game_Emulator" 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"/>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<sections>
<tableViewSection headerTitle="Portrait" id="jGW-i7-nK1">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="8Vp-a7-RvI">
<rect key="frame" x="0.0" y="38" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="8Vp-a7-RvI" id="KCG-fx-fax">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5r7-OQ-i7w">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="5r7-OQ-i7w" firstAttribute="top" secondItem="KCG-fx-fax" secondAttribute="top" id="2uo-BN-g4M"/>
<constraint firstAttribute="trailing" secondItem="5r7-OQ-i7w" secondAttribute="trailing" id="AWh-tS-ECE"/>
<constraint firstItem="5r7-OQ-i7w" firstAttribute="leading" secondItem="KCG-fx-fax" secondAttribute="leading" id="WLG-Wh-Pq2"/>
<constraint firstAttribute="bottom" secondItem="5r7-OQ-i7w" secondAttribute="bottom" id="X0H-WI-dQX"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Landscape" id="PqP-JS-vGE">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="HaE-e5-fux">
<rect key="frame" x="0.0" y="138" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="HaE-e5-fux" id="XwS-Kw-Fe6">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="XY1-es-oZe">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="XY1-es-oZe" firstAttribute="top" secondItem="XwS-Kw-Fe6" secondAttribute="top" id="BLb-gp-vUV"/>
<constraint firstAttribute="bottom" secondItem="XY1-es-oZe" secondAttribute="bottom" id="EmB-8V-hFE"/>
<constraint firstItem="XY1-es-oZe" firstAttribute="leading" secondItem="XwS-Kw-Fe6" secondAttribute="leading" id="ONa-4a-dnw"/>
<constraint firstAttribute="trailing" secondItem="XY1-es-oZe" secondAttribute="trailing" id="avH-RJ-zDf"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="56e-ul-z6v" id="oBb-23-5bM"/>
<outlet property="delegate" destination="56e-ul-z6v" id="08A-GG-8bs"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Game Boy Advance" id="2GJ-l7-DgY">
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="3iE-lg-bkA">
<connections>
<segue destination="eMT-K0-PiM" kind="unwind" unwindAction="unwindToGameCollectionViewController:" id="fFx-zM-kfa"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="landscapeImageView" destination="XY1-es-oZe" id="d6S-ez-jh2"/>
<outlet property="portraitImageView" destination="5r7-OQ-i7w" id="C5r-uX-SlN"/>
<segue destination="pG9-hI-tRE" kind="show" identifier="showControllerSkins" id="kiq-86-5B7"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Nx1-Ly-oRu" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="eMT-K0-PiM" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="2513" y="466"/>
</scene>
<!--Controller Skins-->
<scene sceneID="IN0-an-SWm">
<objects>
<tableViewController title="Controller Skins" id="pG9-hI-tRE" customClass="ControllerSkinsViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="150" sectionHeaderHeight="18" sectionFooterHeight="18" id="WiB-mC-9xS">
<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.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="kK3-bl-qxv" customClass="ControllerSkinTableViewCell" customModule="Retro_Game_Emulator" customModuleProvider="target">
<rect key="frame" x="0.0" y="55.5" width="375" height="150"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="kK3-bl-qxv" id="7Vt-Nl-Sfx">
<rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="o9T-qo-UoF">
<rect key="frame" x="177.5" y="65" width="20" height="20"/>
</activityIndicatorView>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="zbw-fi-eau">
<rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="zbw-fi-eau" firstAttribute="top" secondItem="7Vt-Nl-Sfx" secondAttribute="top" id="1KW-et-nu2"/>
<constraint firstAttribute="trailing" secondItem="zbw-fi-eau" secondAttribute="trailing" id="51I-HE-J4C"/>
<constraint firstAttribute="bottom" secondItem="zbw-fi-eau" secondAttribute="bottom" id="ZpW-sc-XRY"/>
<constraint firstItem="o9T-qo-UoF" firstAttribute="centerX" secondItem="7Vt-Nl-Sfx" secondAttribute="centerX" id="rmp-c3-xsc"/>
<constraint firstItem="o9T-qo-UoF" firstAttribute="centerY" secondItem="7Vt-Nl-Sfx" secondAttribute="centerY" id="y0I-Bf-s60"/>
<constraint firstItem="zbw-fi-eau" firstAttribute="leading" secondItem="7Vt-Nl-Sfx" secondAttribute="leading" id="zQU-50-6Od"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="activityIndicatorView" destination="o9T-qo-UoF" id="dZl-54-Q22"/>
<outlet property="controllerSkinImageView" destination="zbw-fi-eau" id="VXC-1B-ZLE"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="pG9-hI-tRE" id="FkO-on-Ylv"/>
<outlet property="delegate" destination="pG9-hI-tRE" id="8NO-ha-J5E"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Controller Skins" largeTitleDisplayMode="never" id="Ful-dk-I0G">
<rightBarButtonItems>
<barButtonItem style="plain" systemItem="add" id="FI4-LI-ozM">
<connections>
<action selector="importControllerSkin" destination="pG9-hI-tRE" id="tYx-5l-Yrn"/>
</connections>
</barButtonItem>
<barButtonItem title="Reset" style="done" id="XU0-TI-SPi">
<color key="tintColor" systemColor="systemRedColor"/>
<connections>
<action selector="resetControllerSkin:" destination="pG9-hI-tRE" id="EOa-FJ-zOp"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="importControllerSkinButton" destination="FI4-LI-ozM" id="t6D-mk-Tc7"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="54U-JB-wBG" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3270" y="466"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="8qd-VB-Uy5">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="ssH-mM-uG6" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="Ckw-ES-lkE">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<textAttributes key="titleTextAttributes">
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textAttributes>
<textAttributes key="largeTitleTextAttributes">
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</textAttributes>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="eHi-aO-uGS" kind="relationship" relationship="rootViewController" id="wkb-nX-9mw"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HMI-Ep-MdI" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="622" y="1182"/>
</scene>
<!--Grid Menu View Controller-->
<scene sceneID="Lgi-Ii-M1W">
<objects>
<collectionViewController id="Jpj-e9-6XW" customClass="GridMenuViewController" customModule="Retro_Game_Emulator" 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="Retro_Game_Emulator" 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="Retro_Game_Emulator" 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="4858" y="-931"/>
</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="667"/>
<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="4858" y="-244"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="bwW-s2-fcE">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="0QR-U9-gtx" customClass="RSTNavigationController" sceneMemberID="viewController">
<toolbarItems/>
<value key="contentSizeForViewInPopover" type="size" width="375" height="667"/>
<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="0.0" 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="3270" y="-244"/>
</scene>
</scenes>
<resources>
<image name="about" width="20" height="20"/>
<image name="baockgo" width="20" height="20"/>
<image name="game" width="20" height="20"/>
<image name="privacy" width="20" height="20"/>
<image name="useragreement" width="20" height="20"/>
<systemColor name="lightTextColor">
<color white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="secondaryLabelColor">
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="separatorColor">
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemRedColor">
<color red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@ -17,8 +17,7 @@ extension Action
case destructive case destructive
case selected case selected
var alertActionStyle: UIAlertActionStyle var alertActionStyle: UIAlertAction.Style {
{
switch self switch self
{ {
case .default, .selected: return .default case .default, .selected: return .default
@ -27,8 +26,7 @@ extension Action
} }
} }
var previewActionStyle: UIPreviewActionStyle? var previewActionStyle: UIPreviewAction.Style? {
{
switch self switch self
{ {
case .default: return .default case .default: return .default
@ -40,11 +38,40 @@ extension Action
} }
} }
@available(iOS 13, *)
extension Action.Style
{
var menuAttributes: UIMenuElement.Attributes {
switch self
{
case .default, .cancel, .selected: return []
case .destructive: return .destructive
}
}
var menuState: UIMenuElement.State {
switch self
{
case .default, .cancel, .destructive: return .off
case .selected: return .on
}
}
}
struct Action struct Action
{ {
let title: String var title: String
let style: Style var style: Style
let action: ((Action) -> Void)? var image: UIImage? = nil
var action: ((Action) -> Void)?
init(title: String, style: Style = .default, image: UIImage? = nil, action: ((Action) -> Void)? = nil)
{
self.title = title
self.style = style
self.image = image
self.action = action
}
} }
extension UIAlertAction extension UIAlertAction
@ -82,6 +109,19 @@ extension UIAlertController
} }
} }
@available(iOS 13.0, *)
extension UIAction
{
convenience init?(_ action: Action)
{
guard action.style != .cancel else { return nil }
self.init(title: action.title, image: action.image, attributes: action.style.menuAttributes, state: action.style.menuState) { _ in
action.action?(action)
}
}
}
extension RangeReplaceableCollection where Iterator.Element == Action extension RangeReplaceableCollection where Iterator.Element == Action
{ {
var alertActions: [UIAlertAction] { var alertActions: [UIAlertAction] {
@ -90,7 +130,13 @@ extension RangeReplaceableCollection where Iterator.Element == Action
} }
var previewActions: [UIPreviewAction] { var previewActions: [UIPreviewAction] {
let actions = self.flatMap { UIPreviewAction($0) } let actions = self.compactMap { UIPreviewAction($0) }
return actions
}
@available(iOS 13.0, *)
var menuActions: [UIAction] {
let actions = self.compactMap { UIAction($0) }
return actions return actions
} }
} }

View File

@ -0,0 +1,19 @@
//
// Box.swift
// Delta
//
// Created by Riley Testut on 11/28/18.
// Copyright © 2018 Riley Testut. All rights reserved.
//
import Foundation
class Box<T>
{
let value: T
init(_ value: T)
{
self.value = value
}
}

View File

@ -39,21 +39,21 @@ class GridCollectionViewCell: UICollectionViewCell
} }
} }
var maximumImageSize: CGSize = CGSize(width: 100, height: 100) { var maximumImageSize: CGSize = CGSize(width: 150, height: 120) {
didSet { didSet {
self.updateMaximumImageSize() self.updateMaximumImageSize()
} }
} }
fileprivate var vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark))) private var vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
fileprivate var imageViewWidthConstraint: NSLayoutConstraint! private var imageViewWidthConstraint: NSLayoutConstraint!
fileprivate var imageViewHeightConstraint: NSLayoutConstraint! private var imageViewHeightConstraint: NSLayoutConstraint!
fileprivate var textLabelBottomAnchorConstraint: NSLayoutConstraint! private var textLabelBottomAnchorConstraint: NSLayoutConstraint!
fileprivate var textLabelVerticalSpacingConstraint: NSLayoutConstraint! private var textLabelVerticalSpacingConstraint: NSLayoutConstraint!
fileprivate var textLabelFocusedVerticalSpacingConstraint: NSLayoutConstraint? private var textLabelFocusedVerticalSpacingConstraint: NSLayoutConstraint?
override init(frame: CGRect) override init(frame: CGRect)
{ {
@ -109,13 +109,15 @@ class GridCollectionViewCell: UICollectionViewCell
// Image View // Image View
self.imageView.translatesAutoresizingMaskIntoConstraints = false self.imageView.translatesAutoresizingMaskIntoConstraints = false
self.imageView.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true self.imageView.topAnchor.constraint(equalTo: self.vibrancyView.topAnchor).isActive = true
self.imageView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true self.imageView.centerXAnchor.constraint(equalTo: self.vibrancyView.centerXAnchor).isActive = true
self.imageViewWidthConstraint = self.imageView.widthAnchor.constraint(equalToConstant: self.maximumImageSize.width) self.imageViewWidthConstraint = self.imageView.widthAnchor.constraint(equalToConstant: 150)
self.imageViewWidthConstraint.isActive = true self.imageViewWidthConstraint.isActive = true
self.imageViewHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: self.maximumImageSize.height) self.imageViewHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: 120)
self.imageViewHeightConstraint.priority = UILayoutPriority(999) // Fixes "Unable to simultaneously satisfy constraints" runtime error when inserting new grid row.
self.imageViewHeightConstraint.isActive = true self.imageViewHeightConstraint.isActive = true
@ -181,10 +183,15 @@ private extension GridCollectionViewCell
{ {
func updateMaximumImageSize() func updateMaximumImageSize()
{ {
self.imageViewWidthConstraint.constant = self.maximumImageSize.width self.imageViewWidthConstraint.constant = 150
self.imageViewHeightConstraint.constant = self.maximumImageSize.height self.imageViewHeightConstraint.constant = 120
self.textLabelVerticalSpacingConstraint.constant = 8 self.textLabelVerticalSpacingConstraint.constant = 8
self.textLabelFocusedVerticalSpacingConstraint?.constant = self.maximumImageSize.height / 10.0 self.textLabelFocusedVerticalSpacingConstraint?.constant = self.maximumImageSize.height / 10.0
// textLabel 30
// let textLabelFixedHeight: CGFloat = 30
//
// self.textLabelVerticalSpacingConstraint.constant = textLabelFixedHeight
// self.textLabelFocusedVerticalSpacingConstraint?.constant = textLabelFixedHeight / 50
} }
} }

View File

@ -23,22 +23,63 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
// If only one row, distribute the items equally horizontally // If only one row, distribute the items equally horizontally
var usesEqualHorizontalSpacingDistributionForSingleRow = false var usesEqualHorizontalSpacingDistributionForSingleRow = false
private var contentInset: UIEdgeInsets {
guard let collectionView = self.collectionView else { return .zero }
var contentInset = collectionView.contentInset
contentInset.left += collectionView.safeAreaInsets.left
contentInset.right += collectionView.safeAreaInsets.right
return contentInset
}
private var contentWidth: CGFloat {
guard let collectionView = self.collectionView else { return 0.0 }
let contentWidth = collectionView.bounds.width - (self.contentInset.left + self.contentInset.right)
return contentWidth
}
private var maximumItemsPerRow: Int {
let maximumItemsPerRow = Int(floor((self.contentWidth - self.minimumInteritemSpacing) / (self.itemWidth + self.minimumInteritemSpacing)))
return maximumItemsPerRow
}
private var interitemSpacing: CGFloat {
let interitemSpacing = (self.contentWidth - CGFloat(self.maximumItemsPerRow) * self.itemWidth) / CGFloat(self.maximumItemsPerRow + 1)
return interitemSpacing
}
private var cachedCellLayoutAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
override var estimatedItemSize: CGSize { override var estimatedItemSize: CGSize {
didSet { didSet {
fatalError("GridCollectionViewLayout does not support self-sizing cells.") fatalError("GridCollectionViewLayout does not support self-sizing cells.")
} }
} }
override func prepare()
{
super.prepare()
self.sectionInset.left = self.interitemSpacing + self.contentInset.left
self.sectionInset.right = self.interitemSpacing + self.contentInset.right
}
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext)
{
super.invalidateLayout(with: context)
if let context = context as? UICollectionViewFlowLayoutInvalidationContext,
context.invalidateFlowLayoutAttributes || context.invalidateFlowLayoutDelegateMetrics || context.invalidateEverything
{
// Clear layout cache to prevent crashing due to returning outdated layout attributes.
self.cachedCellLayoutAttributes = [:]
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{ {
guard let collectionView = self.collectionView else { return nil }
let maximumItemsPerRow = floor((collectionView.bounds.width - self.minimumInteritemSpacing) / (self.itemWidth + self.minimumInteritemSpacing))
let interitemSpacing = (collectionView.bounds.width - maximumItemsPerRow * self.itemWidth) / (maximumItemsPerRow + 1)
self.sectionInset.left = interitemSpacing
self.sectionInset.right = interitemSpacing
let layoutAttributes = super.layoutAttributesForElements(in: rect)?.map({ $0.copy() }) as! [UICollectionViewLayoutAttributes] let layoutAttributes = super.layoutAttributesForElements(in: rect)?.map({ $0.copy() }) as! [UICollectionViewLayoutAttributes]
var minimumY: CGFloat? = nil var minimumY: CGFloat? = nil
@ -58,7 +99,7 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
if abs(attributes.frame.minX - self.sectionInset.left) > 1 if abs(attributes.frame.minX - self.sectionInset.left) > 1
{ {
attributes.frame.origin.x = previousLayoutAttributes.frame.maxX + interitemSpacing attributes.frame.origin.x = previousLayoutAttributes.frame.maxX + self.interitemSpacing
} }
} }
@ -70,7 +111,7 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
{ {
isSingleRow = false isSingleRow = false
self.alignLayoutAttributes(tempLayoutAttributes, toMinimumY: minY) self.align(tempLayoutAttributes, toMinimumY: minY)
// Reset tempLayoutAttributes // Reset tempLayoutAttributes
tempLayoutAttributes.removeAll() tempLayoutAttributes.removeAll()
@ -97,27 +138,42 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
// Handle the remaining tempLayoutAttributes // Handle the remaining tempLayoutAttributes
if let minimumY = minimumY if let minimumY = minimumY
{ {
self.alignLayoutAttributes(tempLayoutAttributes, toMinimumY: minimumY) self.align(tempLayoutAttributes, toMinimumY: minimumY)
if isSingleRow && self.usesEqualHorizontalSpacingDistributionForSingleRow if isSingleRow && self.usesEqualHorizontalSpacingDistributionForSingleRow
{ {
let spacing = (collectionView.bounds.width - (self.itemWidth * CGFloat(tempLayoutAttributes.count))) / (CGFloat(tempLayoutAttributes.count) + 1.0) let spacing = (self.contentWidth - (self.itemWidth * CGFloat(tempLayoutAttributes.count))) / (CGFloat(tempLayoutAttributes.count) + 1.0)
for (index, layoutAttributes) in tempLayoutAttributes.enumerated() for (index, layoutAttributes) in tempLayoutAttributes.enumerated()
{ {
layoutAttributes.frame.origin.x = spacing + (spacing + self.itemWidth) * CGFloat(index) layoutAttributes.frame.origin.x = spacing + (spacing + self.itemWidth) * CGFloat(index) + self.contentInset.left
} }
} }
} }
for attributes in layoutAttributes where attributes.representedElementCategory == .cell
{
// Update cached attributes for layoutAttributesForItem(at:)
self.cachedCellLayoutAttributes[attributes.indexPath] = attributes
}
return layoutAttributes return layoutAttributes
} }
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
{
if let cachedAttributes = self.cachedCellLayoutAttributes[indexPath]
{
return cachedAttributes
}
return super.layoutAttributesForItem(at: indexPath)
}
} }
private extension GridCollectionViewLayout private extension GridCollectionViewLayout
{ {
func alignLayoutAttributes(_ layoutAttributes: [UICollectionViewLayoutAttributes], toMinimumY minimumY: CGFloat) func align(_ layoutAttributes: [UICollectionViewLayoutAttributes], toMinimumY minimumY: CGFloat)
{ {
for attributes in layoutAttributes for attributes in layoutAttributes
{ {

View File

@ -65,12 +65,12 @@ class LoadControllerSkinImageOperation: RSTLoadOperation<UIImage, ControllerSkin
override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void) override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void)
{ {
guard self.controllerSkin.supports(self.traits) else { guard let traits = self.controllerSkin.supportedTraits(for: self.traits) else {
completion(nil, Error.unsupportedTraits) completion(nil, Error.unsupportedTraits)
return return
} }
guard let image = self.controllerSkin.image(for: self.traits, preferredSize: self.size) else { guard let image = self.controllerSkin.image(for: traits, preferredSize: self.size) else {
completion(nil, Error.doesNotExist) completion(nil, Error.doesNotExist)
return return
} }

View File

@ -45,6 +45,11 @@ class LoadImageURLOperation: RSTLoadOperation<UIImage, NSURL>
super.cancel() super.cancel()
self.downloadOperation?.cancel() self.downloadOperation?.cancel()
if self.isAsynchronous
{
self.finish()
}
} }
override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void) override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void)

View File

@ -0,0 +1,65 @@
//
// 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 {
// Don't include navigation bar height in calculation (as of iOS 13).
let navigationBarHeight = 0.0 // 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,160 @@
//
// 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: [NSAttributedString.Key: Any]? {
if let textAttributes = self._defaultTitleTextAttributes
{
return textAttributes
}
// Make "copy" of self.
let navigationBar = UINavigationBar(frame: self.bounds) // Use self.bounds to avoid "Unable to simultaneously satisfy constraints" runtime error.
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: [NSAttributedString.Key: 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") })
else { return nil }
let containerView: UIView
//TODO: Recursively search all subviews for title UILabel instead of hardcoded OS version-specific hierarchy traversals...
if #available(iOS 16, *)
{
guard let titleControl = contentView.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("Title") }) else { return nil }
if #available(iOS 17, *)
{
guard let view = titleControl.subviews.first else { return nil }
containerView = view
}
else
{
containerView = titleControl
}
}
else
{
containerView = contentView
}
guard let titleLabel = containerView.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 _didLayoutSubviews = false
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(UIView.layoutFittingCompressedSize)
}
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(UIView.layoutFittingCompressedSize)
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()
}
override func layoutSubviews()
{
super.layoutSubviews()
if !_didLayoutSubviews
{
_didLayoutSubviews = true
// didMoveToSuperview() can be too early to accurately
// update text attributes, so ensure we also update
// during first layoutSubviews() call.
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

@ -0,0 +1,73 @@
//
// BadgedTableViewCell.swift
// Delta
//
// Created by Riley Testut on 11/20/18.
// Copyright © 2018 Riley Testut. All rights reserved.
//
import UIKit
class BadgedTableViewCell: UITableViewCell
{
let badgeLabel = UILabel()
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initialize()
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)
{
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.initialize()
}
private func initialize()
{
self.badgeLabel.clipsToBounds = true
self.badgeLabel.textAlignment = .center
self.badgeLabel.backgroundColor = .red
self.badgeLabel.font = UIFont.boldSystemFont(ofSize: 14)
self.badgeLabel.textColor = .white
self.contentView.addSubview(self.badgeLabel)
}
override func layoutSubviews()
{
super.layoutSubviews()
guard let textLabel = self.textLabel else { return }
let spacing = 8 as CGFloat
var contentSize = self.badgeLabel.intrinsicContentSize
contentSize.width += 10
contentSize.height += 10
contentSize.width = max(contentSize.width, contentSize.height)
var frame = CGRect(x: self.contentView.bounds.maxX - contentSize.width,
y: self.contentView.bounds.midY - contentSize.height / 2,
width: contentSize.width,
height: contentSize.height)
if self.accessoryType == .none
{
frame.origin.x -= spacing
}
self.badgeLabel.frame = frame
self.badgeLabel.layer.cornerRadius = frame.height / 2
self.badgeLabel.backgroundColor = .red
let overlap = textLabel.frame.maxX - (frame.minX - spacing)
if overlap > 0 && !self.badgeLabel.isHidden
{
textLabel.frame.size.width -= overlap
}
}
}

View File

@ -1,5 +1,5 @@
// //
// GameMetadataTableViewCell.swift // GameTableViewCell.swift
// Delta // Delta
// //
// Created by Riley Testut on 3/27/17. // Created by Riley Testut on 3/27/17.
@ -8,7 +8,7 @@
import UIKit import UIKit
class GameMetadataTableViewCell: UITableViewCell class GameTableViewCell: UITableViewCell
{ {
@IBOutlet private(set) var nameLabel: UILabel! @IBOutlet private(set) var nameLabel: UILabel!
@IBOutlet private(set) var artworkImageView: UIImageView! @IBOutlet private(set) var artworkImageView: UIImageView!

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="FL0-zT-qa3" customClass="GameTableViewCell" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="97"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="FL0-zT-qa3" id="zSi-4a-DaH">
<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="5dT-zd-huQ" userLabel="Selected Background View">
<rect key="frame" x="0.0" y="0.0" width="375" height="96.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="68X-vf-MNx">
<rect key="frame" x="15" y="8" width="80" height="80"/>
<constraints>
<constraint firstAttribute="width" secondItem="68X-vf-MNx" secondAttribute="height" multiplier="1:1" id="P6f-Lc-8B3"/>
</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="hKz-BX-p8h">
<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 firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="hKz-BX-p8h" secondAttribute="bottom" constant="1" id="1JB-It-w1s"/>
<constraint firstItem="68X-vf-MNx" firstAttribute="leading" secondItem="zSi-4a-DaH" secondAttribute="leading" constant="15" id="CD9-Qz-0UL"/>
<constraint firstAttribute="trailing" secondItem="hKz-BX-p8h" secondAttribute="trailing" constant="15" id="RxT-jB-cvp"/>
<constraint firstItem="68X-vf-MNx" firstAttribute="top" secondItem="zSi-4a-DaH" secondAttribute="top" constant="8" id="T5j-O5-aTX"/>
<constraint firstItem="hKz-BX-p8h" firstAttribute="leading" secondItem="68X-vf-MNx" secondAttribute="trailing" constant="15" id="jks-s2-5ZX"/>
<constraint firstItem="hKz-BX-p8h" firstAttribute="top" relation="greaterThanOrEqual" secondItem="zSi-4a-DaH" secondAttribute="top" constant="1" id="swc-Ib-2wh"/>
<constraint firstItem="hKz-BX-p8h" firstAttribute="centerY" secondItem="zSi-4a-DaH" secondAttribute="centerY" id="uyb-vA-Qtb"/>
<constraint firstAttribute="bottom" secondItem="68X-vf-MNx" secondAttribute="bottom" constant="8" id="wGX-lV-ACr"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="artworkImageView" destination="68X-vf-MNx" id="Sda-Tl-WEd"/>
<outlet property="artworkImageViewLeadingConstraint" destination="CD9-Qz-0UL" id="jhw-i7-9ak"/>
<outlet property="artworkImageViewTrailingConstraint" destination="jks-s2-5ZX" id="vrM-OV-rsa"/>
<outlet property="nameLabel" destination="hKz-BX-p8h" id="gdI-v9-dj3"/>
<outlet property="selectedBackgroundView" destination="5dT-zd-huQ" id="jOr-DK-8bp"/>
</connections>
</tableViewCell>
</objects>
<resources>
<image name="BoxArt" width="100" height="100"/>
</resources>
</document>

View File

@ -0,0 +1,143 @@
//
// CheatBase.swift
// Delta
//
// Created by Riley Testut on 1/17/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
import SQLite
import Roxas
private extension UserDefaults
{
@NSManaged var previousCheatBaseVersion: Int
}
extension ExpressionType
{
static var cheatID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("cheatID")
}
static var cheatName: SQLite.Expression<String> {
return SQLite.Expression<String>("cheatName")
}
static var cheatDescription: SQLite.Expression<String?> {
return SQLite.Expression<String?>("cheatDescription")
}
static var cheatCode: SQLite.Expression<String> {
return SQLite.Expression<String>("cheatCode")
}
static var cheatDeviceID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("cheatDeviceID")
}
static var cheatActivation: SQLite.Expression<String?> {
return SQLite.Expression<String?>("cheatActivation")
}
static var cheatCategoryID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("cheatCategoryID")
}
static var cheatCategoryName: SQLite.Expression<String> {
return SQLite.Expression<String>("cheatCategory")
}
static var cheatCategoryDescription: SQLite.Expression<String> {
return SQLite.Expression<String>("cheatCategoryDescription")
}
}
extension Table
{
static var cheats: Table {
return Table("CHEATS")
}
static var cheatCategories: Table {
return Table("CHEAT_CATEGORIES")
}
}
@available(iOS 14, *)
class CheatBase: GamesDatabase
{
static let cheatsVersion = 1
static var previousCheatsVersion: Int? {
return UserDefaults.standard.previousCheatBaseVersion
}
private let connection: Connection
override init() throws
{
let fileURL = DatabaseManager.cheatBaseURL
guard FileManager.default.fileExists(atPath: fileURL.path) else { throw GamesDatabase.Error.doesNotExist }
self.connection = try Connection(fileURL.path)
try super.init()
UserDefaults.standard.previousCheatBaseVersion = CheatBase.cheatsVersion
}
func cheats(for game: Game) async throws -> [CheatMetadata]?
{
let metadata = await withCheckedContinuation { continuation in
if let context = game.managedObjectContext
{
context.perform {
let metadata = self.metadata(for: game)
continuation.resume(returning: metadata)
}
}
else
{
let metadata = self.metadata(for: game)
continuation.resume(returning: metadata)
}
}
guard let romIDValue = metadata?.romID else { return nil }
let cheatID = Expression<Any>.cheatID
let cheatName = Expression<Any>.cheatName
let cheatCode = Expression<Any>.cheatCode
let cheatDescription = Expression<Any>.cheatDescription
let cheatActivation = Expression<Any>.cheatActivation
let cheatDeviceID = Expression<Any>.cheatDeviceID
let categoryID = Expression<Any>.cheatCategoryID
let categoryName = Expression<Any>.cheatCategoryName
let categoryDescription = Expression<Any>.cheatCategoryDescription
let romID = Expression<Any>.romID
let query = Table.cheats.select(cheatID, cheatName, cheatCode, cheatDescription, cheatActivation, cheatDeviceID, Table.cheats[categoryID], categoryName, categoryDescription)
.filter(romID == romIDValue)
.join(Table.cheatCategories, on: Table.cheats[categoryID] == Table.cheatCategories[categoryID])
.order(cheatName)
let rows = try self.connection.prepare(query)
let results = rows.compactMap { (row) -> CheatMetadata? in
guard case let deviceID = Int16(row[cheatDeviceID]), let device = CheatDevice(rawValue: deviceID) else { return nil }
let id = row[Table.cheats[categoryID]]
let category = CheatCategory(id: id, name: row[categoryName], categoryDescription: row[categoryDescription])
let metadata = CheatMetadata(id: row[cheatID], name: row[cheatName], code: row[cheatCode], description: row[cheatDescription], activationHint: row[cheatActivation], device: device, category: category)
return metadata
}
return results
}
}

View File

@ -0,0 +1,273 @@
//
// CheatBaseView.swift
// Delta
//
// Created by Riley Testut on 1/17/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import enum SQLite.Result
@available(iOS 14, *)
extension CheatBaseView
{
private class ViewModel: ObservableObject
{
@Published
var allCheats: [CheatMetadata]? {
didSet {
guard let cheats = allCheats else {
self.cheatsByCategory = nil
return
}
let cheatsByCategory = Dictionary(grouping: cheats, by: { $0.category }).sorted { $0.key.id < $1.key.id }
self.cheatsByCategory = cheatsByCategory
}
}
@Published
private(set) var cheatsByCategory: [(CheatCategory, [CheatMetadata])]?
@Published
private(set) var error: Error?
@Published
var searchText: String = "" {
didSet {
self.searchCheats()
}
}
@Published
private(set) var filteredCheats: [CheatMetadata]?
@MainActor
func fetchCheats(for game: Game) async
{
guard self.allCheats == nil else { return }
do
{
let database = try CheatBase()
let cheats = try await database.cheats(for: game) ?? []
self.allCheats = cheats
}
catch
{
self.error = error
}
}
private func searchCheats()
{
if let cheats = self.allCheats, !self.searchText.isEmpty
{
let predicate = NSPredicate(forSearchingForText: self.searchText, inValuesForKeyPaths: [#keyPath(CheatMetadata.name), #keyPath(CheatMetadata.cheatDescription)])
let filteredCheats = cheats.filter { predicate.evaluate(with: $0) }
self.filteredCheats = filteredCheats
}
else
{
self.filteredCheats = nil
}
}
}
}
@available(iOS 14, *)
struct CheatBaseView: View
{
let game: Game?
var cancellationHandler: (() -> Void)?
var selectionHandler: ((CheatMetadata) -> Void)?
@StateObject
private var viewModel = ViewModel()
@State
private var activationHintCheat: CheatMetadata?
var body: some View {
NavigationView {
ZStack {
if let cheats = viewModel.allCheats, !cheats.isEmpty
{
// Only show List if there is at least one cheat for this game.
cheatList()
}
// Place above List
placeholderView()
}
.alert(item: $activationHintCheat) { cheat in
Alert(title: Text("How to Activate"),
message: Text(cheat.activationHint ?? ""),
dismissButton: .default(Text("OK")))
}
.navigationTitle(Text(game?.name ?? "CheatBase"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
cancellationHandler?()
}
}
}
}
.onAppear {
Task {
guard let game = self.game else { return }
await viewModel.fetchCheats(for: game)
}
}
}
private func cheatList() -> some View
{
VStack {
if #unavailable(iOS 15)
{
LegacySearchBar(text: $viewModel.searchText)
}
let listView = List {
if let filteredCheats = viewModel.filteredCheats
{
ForEach(filteredCheats) { cheat in
cell(for: cheat)
}
}
else if let cheats = viewModel.cheatsByCategory
{
ForEach(cheats, id: \.0.id) { (category, cheats) in
Section {
DisclosureGroup {
ForEach(cheats) { cheat in
cell(for: cheat)
}
} label: {
Text(category.name)
}
} footer: {
Text(category.categoryDescription)
}
}
}
}
if #available(iOS 15, *)
{
listView.searchable(text: $viewModel.searchText)
}
else
{
listView
}
}
.listStyle(.insetGrouped)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
}
private func cell(for cheat: CheatMetadata) -> some View
{
ZStack(alignment: .leading) {
Button(action: { choose(cheat) }) {}
HStack {
// Name + Description
VStack(alignment: .leading, spacing: 4) {
Text(cheat.name)
if let description = cheat.cheatDescription
{
Text(description)
.font(.caption)
}
}
// Activation Hint
if cheat.activationHint != nil
{
Spacer()
Button(action: { activationHintCheat = cheat }) {
Image(systemName: "info.circle")
}
.buttonStyle(.borderless)
}
}
.multilineTextAlignment(.leading)
}
}
private func placeholderView() -> some View
{
VStack(spacing: 8) {
if let error = viewModel.error
{
Text("Unable to Load Cheats")
.font(.title)
if let error = error as? SQLite.Result
{
// SQLite.Result implements CustomStringConvertible.description, but not localizedDescription.
Text(String(describing: error))
.font(.callout)
}
else
{
Text(error.localizedDescription)
.font(.callout)
}
}
else if let filteredCheats = viewModel.filteredCheats, filteredCheats.isEmpty
{
Text("Cheat Not Found")
.font(.title)
Text("Please make sure the name is correct, or try searching for another cheat.")
.font(.callout)
}
else if let cheats = viewModel.allCheats, cheats.isEmpty
{
Text("No Cheats")
.font(.title)
Text("There are no cheats for this game in Delta's CheatBase. Please try a different game.")
.font(.callout)
}
else if viewModel.allCheats == nil
{
ProgressView()
.progressViewStyle(.circular)
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.foregroundColor(.gray)
.padding()
}
init(game: Game, cheats: [CheatMetadata]? = nil)
{
self.game = game
let viewModel = ViewModel()
viewModel.allCheats = cheats
self._viewModel = StateObject(wrappedValue: viewModel)
}
}
@available(iOS 14, *)
private extension CheatBaseView
{
func choose(_ cheatMetadata: CheatMetadata)
{
self.selectionHandler?(cheatMetadata)
}
}

View File

@ -0,0 +1,111 @@
//
// CheatDevice.swift
// Delta
//
// Created by Riley Testut on 1/30/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
import DeltaCore
import NESDeltaCore
@objc
enum CheatDevice: Int16
{
case famicomGameGenie = 1
case famicomRaw = 2
case famicomRawCompare = 3
case gbGameGenie = 4
case gbaActionReplayMax = 5
case gbaCodeBreaker = 6
case gbaGameShark = 7
case gbcGameShark = 8
case n64GameShark = 9
case dsActionReplay = 10
case dsCodeBreaker = 11
case nesGameGenie = 12
case nesRaw = 13
case nesRawCompare = 14
case snesActionReplay = 15
case snesGameGenie = 16
case gameGearActionReplay = 17
case gameGearGameGenie = 18
case masterSystemActionReplay = 19
case masterSystemGameGenie = 20
case cdActionReplay10 = 21
case cdActionReplay8 = 22
case genesisActionReplay10 = 23
case genesisActionReplay8 = 24
}
extension CheatDevice
{
var cheatType: CheatType? {
switch self
{
case .snesActionReplay, .gbaActionReplayMax, .dsActionReplay, .gameGearActionReplay, .masterSystemActionReplay, .genesisActionReplay8, .genesisActionReplay10, .cdActionReplay8, .cdActionReplay10:
return .actionReplay
case .n64GameShark, .gbcGameShark, .gbaGameShark:
return .gameShark
case .famicomGameGenie, .snesGameGenie, .gbGameGenie, .gameGearGameGenie, .masterSystemGameGenie:
return .gameGenie
case .nesGameGenie:
return CheatType(rawValue: DeltaCore.CheatType.gameGenie8.rawValue)
case .gbaCodeBreaker, .dsCodeBreaker:
return .codeBreaker
case .famicomRaw, .famicomRawCompare:
return nil
case .nesRaw, .nesRawCompare:
return nil
}
}
var gameType: GameType? {
switch self
{
case .famicomGameGenie, .famicomRaw, .famicomRawCompare: return .nes
case .nesGameGenie, .nesRaw, .nesRawCompare: return .nes
case .snesActionReplay, .snesGameGenie: return .snes
case .n64GameShark: return .n64
case .gbGameGenie, .gbcGameShark: return .gbc
case .gbaActionReplayMax, .gbaGameShark, .gbaCodeBreaker: return .gba
case .dsActionReplay, .dsCodeBreaker: return .ds
case .genesisActionReplay8, .genesisActionReplay10: return .genesis
case .cdActionReplay8, .cdActionReplay10: return .genesis
// Not yet supported
case .gameGearActionReplay, .gameGearGameGenie: return nil
case .masterSystemActionReplay, .masterSystemGameGenie: return nil
}
}
var cheatFormat: CheatFormat? {
guard
let cheatType = self.cheatType,
let gameType = self.gameType,
let deltaCore = Delta.core(for: gameType)
else { return nil }
let cheatFormat = deltaCore.supportedCheatFormats.first { $0.type == cheatType }
return cheatFormat
}
}

View File

@ -0,0 +1,45 @@
//
// CheatMetadata.swift
// Delta
//
// Created by Riley Testut on 1/17/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import DeltaCore
struct CheatCategory: Identifiable, Hashable
{
var id: Int
var name: String
var categoryDescription: String
}
@objcMembers // @objcMembers required for NSPredicate-based filtering.
final class CheatMetadata: NSObject, Identifiable
{
let id: Int
let name: String
let code: String
let cheatDescription: String?
let activationHint: String?
let device: CheatDevice
let category: CheatCategory
init(id: Int, name: String, code: String, description: String?, activationHint: String?, device: CheatDevice, category: CheatCategory)
{
self.id = id
self.name = name
self.code = code
self.cheatDescription = description
self.activationHint = activationHint
self.device = device
self.category = category
}
}

View File

@ -0,0 +1,51 @@
//
// LegacySearchBar.swift
// Delta
//
// Created by Riley Testut on 1/25/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import SwiftUI
@available(iOS 13, *)
struct LegacySearchBar: UIViewRepresentable
{
class Coordinator: NSObject, UISearchBarDelegate
{
@Binding
var text: String
init(text: Binding<String>)
{
self._text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
{
self.text = searchText
}
}
@Binding
var text: String
func makeUIView(context: Context) -> UISearchBar
{
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = NSLocalizedString("Search", comment: "")
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: Context)
{
uiView.text = self.text
}
func makeCoordinator() -> Coordinator
{
return Coordinator(text: $text)
}
}

View File

@ -11,14 +11,19 @@ import CoreData
// Workspace // Workspace
import DeltaCore import DeltaCore
import ZipZap import Harmony
import Roxas
// Pods import ZIPFoundation
import FileMD5Hash import MelonDSDeltaCore
extension DatabaseManager extension DatabaseManager
{ {
enum ImportError: Error, Hashable static let didStartNotification = Notification.Name("databaseManagerDidStartNotification")
}
extension DatabaseManager
{
enum ImportError: LocalizedError, Hashable, Equatable
{ {
case doesNotExist(URL) case doesNotExist(URL)
case invalid(URL) case invalid(URL)
@ -26,91 +31,217 @@ extension DatabaseManager
case unknown(URL, NSError) case unknown(URL, NSError)
case saveFailed(Set<URL>, NSError) case saveFailed(Set<URL>, NSError)
var hashValue: Int { var errorDescription: String? {
switch self switch self
{ {
case .doesNotExist: return 0 case .doesNotExist: return NSLocalizedString("The file does not exist.", comment: "")
case .invalid: return 1 case .invalid: return NSLocalizedString("The file is invalid.", comment: "")
case .unsupported: return 2 case .unsupported: return NSLocalizedString("This file is not supported.", comment: "")
case .unknown: return 3 case .unknown(_, let error): return error.localizedDescription
case .saveFailed: return 4 case .saveFailed(_, let error): return error.localizedDescription
}
}
static func ==(lhs: ImportError, rhs: ImportError) -> Bool
{
switch (lhs, rhs)
{
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
} }
} }
} }
} }
final class DatabaseManager: NSPersistentContainer final class DatabaseManager: RSTPersistentContainer
{ {
static let shared = DatabaseManager() static let shared = DatabaseManager()
fileprivate var gamesDatabase: GamesDatabase? = nil private(set) var isStarted = false
private var gamesDatabase: GamesDatabase? = nil
private var validationManagedObjectContext: NSManagedObjectContext?
private let importController = ImportController(documentTypes: [])
private init() private init()
{ {
guard 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) let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL),
let harmonyModel = NSManagedObjectModel.harmonyModel(byMergingWith: [managedObjectModel])
else { fatalError("Core Data model cannot be found. Aborting.") } else { fatalError("Core Data model cannot be found. Aborting.") }
super.init(name: "Delta", managedObjectModel: harmonyModel)
super.init(name: "Delta", managedObjectModel: managedObjectModel) self.shouldAddStoresAsynchronously = true
self.viewContext.automaticallyMergesChangesFromParent = true
} }
} }
extension DatabaseManager extension DatabaseManager
{ {
override func newBackgroundContext() -> NSManagedObjectContext func start(completionHandler: @escaping (Error?) -> Void)
{ {
let context = super.newBackgroundContext() guard !self.isStarted else { return }
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context for description in self.persistentStoreDescriptions
{
// Set configuration so RSTPersistentContainer can determine how to migrate this and Harmony's database independently.
description.configuration = NSManagedObjectModel.Configuration.external.rawValue
} }
override func loadPersistentStores(completionHandler block: @escaping (NSPersistentStoreDescription, Error?) -> Void) self.loadPersistentStores { (description, error) in
{ guard error == nil else { return completionHandler(error) }
super.loadPersistentStores { (description, error) in
self.prepareDatabase { self.prepareDatabase {
block(description, error) self.isStarted = true
NotificationCenter.default.post(name: DatabaseManager.didStartNotification, object: self)
completionHandler(nil)
} }
} }
} }
func prepare(_ core: DeltaCoreProtocol, in context: NSManagedObjectContext)
{
guard let system = System(gameType: core.gameType) else { return }
if let skin = ControllerSkin(system: system, context: context)
{
print("Updated default skin (\(skin.identifier)) for system:", system)
}
else
{
print("Failed to update default skin for system:", system)
}
switch system
{
case .ds where core == MelonDS.core:
// Returns nil if game already exists.
func makeBIOS(name: String, identifier: String) -> Game?
{
let predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), identifier)
if let _ = Game.instancesWithPredicate(predicate, inManagedObjectContext: context, type: Game.self).first
{
// BIOS already exists, so don't do anything.
return nil
}
let filename: String
switch identifier
{
case Game.melonDSBIOSIdentifier:
guard
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios7URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios9URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.firmwareURL.path)
else { return nil }
filename = "nds.bios"
case Game.melonDSDSiBIOSIdentifier:
#if BETA
guard
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS7URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS9URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiFirmwareURL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiNANDURL.path)
else { return nil }
filename = "dsi.bios"
#else
return nil
#endif
default: filename = "system.bios"
}
let bios = Game(context: context)
bios.name = name
bios.identifier = identifier
bios.type = .ds
bios.filename = filename
if let artwork = UIImage(named: "DS Home Screen"), let artworkData = artwork.pngData()
{
do
{
let destinationURL = DatabaseManager.artworkURL(for: bios)
try artworkData.write(to: destinationURL, options: .atomic)
bios.artworkURL = destinationURL
}
catch
{
print("Failed to copy default DS home screen artwork.", error)
}
}
return bios
}
let insertedGames = [
(name: NSLocalizedString("Home Screen", comment: ""), identifier: Game.melonDSBIOSIdentifier),
(name: NSLocalizedString("Home Screen (DSi)", comment: ""), identifier: Game.melonDSDSiBIOSIdentifier)
].compactMap(makeBIOS)
// Break if we didn't create any new Games.
guard !insertedGames.isEmpty else { break }
let gameCollection = GameCollection(context: context)
gameCollection.identifier = GameType.ds.rawValue
gameCollection.index = Int16(System.ds.year)
gameCollection.games.formUnion(insertedGames)
case .ds:
let predicate = NSPredicate(format: "%K IN %@", #keyPath(Game.identifier), [Game.melonDSBIOSIdentifier, Game.melonDSDSiBIOSIdentifier])
let games = Game.instancesWithPredicate(predicate, inManagedObjectContext: context, type: Game.self)
for game in games
{
context.delete(game)
}
default: break
}
}
}
//MARK: - Update -
private extension DatabaseManager
{
func updateRecentGameShortcuts()
{
guard let managedObjectContext = self.validationManagedObjectContext else { return }
guard Settings.gameShortcutsMode == .recent else { return }
let fetchRequest = Game.recentlyPlayedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
do
{
let games = try managedObjectContext.fetch(fetchRequest)
Settings.gameShortcuts = games
}
catch
{
print(error)
}
}
} }
//MARK: - Preparation - //MARK: - Preparation -
private extension DatabaseManager private extension DatabaseManager
{ {
func prepareDatabase(completion: @escaping (Void) -> Void) func prepareDatabase(completion: @escaping () -> Void)
{ {
self.validationManagedObjectContext = self.newBackgroundContext()
NotificationCenter.default.addObserver(self, selector: #selector(DatabaseManager.validateManagedObjectContextSave(with:)), name: .NSManagedObjectContextDidSave, object: nil)
self.performBackgroundTask { (context) in self.performBackgroundTask { (context) in
for system in System.supportedSystems for system in System.allCases
{ {
guard let deltaControllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: system.gameType) else { continue } self.prepare(system.deltaCore, in: context)
let controllerSkin = ControllerSkin(context: context)
controllerSkin.isStandard = true
controllerSkin.filename = deltaControllerSkin.fileURL.lastPathComponent
controllerSkin.configure(with: deltaControllerSkin)
} }
do do
@ -124,10 +255,27 @@ private extension DatabaseManager
do do
{ {
if !FileManager.default.fileExists(atPath: DatabaseManager.gamesDatabaseURL.path) if !FileManager.default.fileExists(atPath: DatabaseManager.gamesDatabaseURL.path) || GamesDatabase.version != GamesDatabase.previousVersion
{ {
guard let bundleURL = Bundle.main.url(forResource: "openvgdb", withExtension: "sqlite") else { throw GamesDatabase.Error.doesNotExist } guard let bundleURL = Bundle.main.url(forResource: "openvgdb", withExtension: "sqlite") else { throw GamesDatabase.Error.doesNotExist }
try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL) try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL, shouldReplace: true)
}
if #available(iOS 14, *), !FileManager.default.fileExists(atPath: DatabaseManager.cheatBaseURL.path) || CheatBase.cheatsVersion != CheatBase.previousCheatsVersion
{
guard let archiveURL = Bundle.main.url(forResource: "cheatbase", withExtension: "zip") else { throw GamesDatabase.Error.doesNotExist }
let temporaryDirectoryURL = FileManager.default.uniqueTemporaryURL()
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true)
defer {
try? FileManager.default.removeItem(at: temporaryDirectoryURL)
}
// Unzip to temporaryDirectoryURL first to ensure we don't accidentally unzip other items into DatabaseManager.cheatBaseURL directory (e.g. __MACOSX directory).
try FileManager.default.unzipItem(at: archiveURL, to: temporaryDirectoryURL, skipCRC32: true) // skipCRC32 to avoid ~10 second extraction.
let extractedDatabaseURL = temporaryDirectoryURL.appendingPathComponent("cheatbase.sqlite")
try FileManager.default.copyItem(at: extractedDatabaseURL, to: DatabaseManager.cheatBaseURL, shouldReplace: true)
} }
self.gamesDatabase = try GamesDatabase() self.gamesDatabase = try GamesDatabase()
@ -138,7 +286,6 @@ private extension DatabaseManager
} }
completion() completion()
} }
} }
} }
@ -149,7 +296,20 @@ extension DatabaseManager
{ {
func importGames(at urls: Set<URL>, completion: ((Set<Game>, Set<ImportError>) -> Void)?) func importGames(at urls: Set<URL>, completion: ((Set<Game>, Set<ImportError>) -> Void)?)
{ {
var errors = Set<ImportError>() let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) }
guard externalFileURLs.isEmpty else {
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
availableFileURLs.formUnion(importedURLs)
self.importGames(at: Set(availableFileURLs)) { (importedGames, importErrors) in
let allErrors = importErrors.union(externalImportErrors)
completion?(importedGames, allErrors)
}
}
return
}
let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" } let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" }
if zipFileURLs.count > 0 if zipFileURLs.count > 0
@ -167,6 +327,7 @@ extension DatabaseManager
self.performBackgroundTask { (context) in self.performBackgroundTask { (context) in
var errors = Set<ImportError>()
var identifiers = Set<String>() var identifiers = Set<String>()
for url in urls for url in urls
@ -181,7 +342,23 @@ extension DatabaseManager
continue continue
} }
let identifier = FileHash.sha1HashOfFile(atPath: url.path) as String guard System.registeredSystems.contains(system) else {
errors.insert(.unsupported(url))
continue
}
let identifier: String
do
{
identifier = try RSTHasher.sha1HashOfFile(at: url)
}
catch let error as NSError
{
errors.insert(.unknown(url, error))
continue
}
let filename = identifier + "." + url.pathExtension let filename = identifier + "." + url.pathExtension
let game = Game(context: context) let game = Game(context: context)
@ -246,10 +423,24 @@ extension DatabaseManager
func importControllerSkins(at urls: Set<URL>, completion: ((Set<ControllerSkin>, Set<ImportError>) -> Void)?) func importControllerSkins(at urls: Set<URL>, completion: ((Set<ControllerSkin>, Set<ImportError>) -> Void)?)
{ {
var errors = Set<ImportError>() let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) }
guard externalFileURLs.isEmpty else {
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
availableFileURLs.formUnion(importedURLs)
self.importControllerSkins(at: Set(availableFileURLs)) { (importedSkins, importErrors) in
let allErrors = importErrors.union(externalImportErrors)
completion?(importedSkins, allErrors)
}
}
return
}
self.performBackgroundTask { (context) in self.performBackgroundTask { (context) in
var errors = Set<ImportError>()
var identifiers = Set<String>() var identifiers = Set<String>()
for url in urls for url in urls
@ -318,7 +509,6 @@ extension DatabaseManager
{ {
DispatchQueue.global().async { DispatchQueue.global().async {
var semaphores = Set<DispatchSemaphore>()
var outputURLs = Set<URL>() var outputURLs = Set<URL>()
var errors = Set<ImportError>() var errors = Set<ImportError>()
@ -326,16 +516,19 @@ extension DatabaseManager
{ {
var archiveContainsValidGameFile = false var archiveContainsValidGameFile = false
guard let archive = Archive(url: url, accessMode: .read) else {
errors.insert(.invalid(url))
continue
}
for entry in archive
{
do do
{
let archive = try ZZArchive(url: url)
for entry in archive.entries
{ {
// Ensure entry is not in a subdirectory // Ensure entry is not in a subdirectory
guard !entry.fileName.contains("/") else { continue } guard !entry.path.contains("/") else { continue }
let fileExtension = (entry.fileName as NSString).pathExtension let fileExtension = (entry.path as NSString).pathExtension
guard GameType(fileExtension: fileExtension) != nil else { continue } guard GameType(fileExtension: fileExtension) != nil else { continue }
@ -344,55 +537,23 @@ extension DatabaseManager
// However, if this game file does turn out to be invalid when extracting, we'll return an ImportError.invalid error specific to this game file // However, if this game file does turn out to be invalid when extracting, we'll return an ImportError.invalid error specific to this game file
archiveContainsValidGameFile = true archiveContainsValidGameFile = true
// ROMs may potentially be very large, so we extract using file streams and not raw Data
let inputStream = try entry.newStream()
// Must use temporary directory, and not the directory containing zip file, since the latter might be read-only (such as when importing from Safari) // Must use temporary directory, and not the directory containing zip file, since the latter might be read-only (such as when importing from Safari)
let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent(entry.fileName) let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent(entry.path)
if FileManager.default.fileExists(atPath: outputURL.path) if FileManager.default.fileExists(atPath: outputURL.path)
{ {
try FileManager.default.removeItem(at: outputURL) try FileManager.default.removeItem(at: outputURL)
} }
guard let outputStream = OutputStream(url: outputURL, append: false) else { continue } _ = try archive.extract(entry, to: outputURL, skipCRC32: true)
let semaphore = DispatchSemaphore(value: 0)
semaphores.insert(semaphore)
let outputWriter = InputStreamOutputWriter(inputStream: inputStream, outputStream: outputStream)
outputWriter.start { (error) in
if let error = error
{
if FileManager.default.fileExists(atPath: outputURL.path)
{
do
{
try FileManager.default.removeItem(at: outputURL)
}
catch
{
print(error)
}
}
print(error)
errors.insert(.invalid(outputURL))
}
else
{
outputURLs.insert(outputURL) outputURLs.insert(outputURL)
} }
semaphore.signal()
}
}
}
catch catch
{ {
print(error) print(error)
} }
}
if !archiveContainsValidGameFile if !archiveContainsValidGameFile
{ {
@ -400,11 +561,6 @@ extension DatabaseManager
} }
} }
for semaphore in semaphores
{
semaphore.wait()
}
for url in urls for url in urls
{ {
if FileManager.default.fileExists(atPath: url.path) if FileManager.default.fileExists(atPath: url.path)
@ -423,6 +579,32 @@ extension DatabaseManager
completion(outputURLs, errors) completion(outputURLs, errors)
} }
} }
private func importExternalFiles(at urls: Set<URL>, completion: @escaping ((Set<URL>, Set<ImportError>) -> Void))
{
var outputURLs = Set<URL>()
var errors = Set<ImportError>()
let dispatchGroup = DispatchGroup()
for url in urls
{
dispatchGroup.enter()
self.importController.importExternalFile(at: url) { (result) in
switch result
{
case .failure(let error): errors.insert(.unknown(url, error as NSError))
case .success(let fileURL): outputURLs.insert(fileURL)
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
completion(outputURLs, errors)
}
}
} }
//MARK: - File URLs - //MARK: - File URLs -
@ -454,6 +636,12 @@ extension DatabaseManager
return gamesDatabaseURL return gamesDatabaseURL
} }
class var cheatBaseURL: URL
{
let gamesDatabaseURL = self.defaultDirectoryURL().appendingPathComponent("cheatbase.sqlite")
return gamesDatabaseURL
}
class var gamesDirectoryURL: URL class var gamesDirectoryURL: URL
{ {
let gamesDirectoryURL = DatabaseManager.defaultDirectoryURL().appendingPathComponent("Games") let gamesDirectoryURL = DatabaseManager.defaultDirectoryURL().appendingPathComponent("Games")
@ -498,11 +686,33 @@ extension DatabaseManager
{ {
let gameURL = game.fileURL let gameURL = game.fileURL
let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("jpg") let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("png")
return artworkURL return artworkURL
} }
} }
//MARK: - Notifications -
private extension DatabaseManager
{
@objc func validateManagedObjectContextSave(with notification: Notification)
{
guard (notification.object as? NSManagedObjectContext) != self.validationManagedObjectContext else { return }
let insertedObjects = (notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? []
let updatedObjects = (notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? []
let deletedObjects = (notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? []
let allObjects = insertedObjects.union(updatedObjects).union(deletedObjects)
if allObjects.contains(where: { $0 is Game })
{
self.validationManagedObjectContext?.perform {
self.updateRecentGameShortcuts()
}
}
}
}
//MARK: - Private - //MARK: - Private -
private extension DatabaseManager private extension DatabaseManager
{ {

View File

@ -1,148 +0,0 @@
//
// InputStreamOutputWriter.swift
// Delta
//
// Created by Riley Testut on 12/25/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import Foundation
private let MaximumBufferLength = 4 * 1024 // 4 KB
class InputStreamOutputWriter: NSObject
{
let inputStream: InputStream
let outputStream: OutputStream
fileprivate var completion: ((Error?) -> Void)?
fileprivate var dataBuffer = Data(capacity: MaximumBufferLength * 2)
init(inputStream: InputStream, outputStream: OutputStream)
{
self.inputStream = inputStream
self.outputStream = outputStream
super.init()
self.inputStream.delegate = self
self.outputStream.delegate = self
}
func start(with completion: @escaping ((Error?) -> Void))
{
guard self.completion == nil else { return }
self.completion = completion
let writingQueue = DispatchQueue(label: "com.rileytestut.InputStreamOutputWriter.writingQueue", qos: .userInitiated)
writingQueue.async {
self.inputStream.schedule(in: .current, forMode: .defaultRunLoopMode)
self.outputStream.schedule(in: .current, forMode: .defaultRunLoopMode)
self.outputStream.open()
self.inputStream.open()
RunLoop.current.run()
}
}
}
private extension InputStreamOutputWriter
{
func writeDataBuffer()
{
while self.outputStream.hasSpaceAvailable && self.dataBuffer.count > 0
{
self.dataBuffer.withUnsafeMutableBytes { (buffer: UnsafeMutablePointer<UInt8>) -> Void in
let writtenBytesCount = self.outputStream.write(buffer, maxLength: self.dataBuffer.count)
if writtenBytesCount >= 0
{
self.dataBuffer.removeSubrange(0 ..< writtenBytesCount)
}
}
}
}
func finishWriting()
{
self.inputStream.close()
self.outputStream.close()
self.inputStream.remove(from: .current, forMode: .commonModes)
self.outputStream.remove(from: .current, forMode: .commonModes)
self.completion?(self.inputStream.streamError ?? self.outputStream.streamError)
CFRunLoopStop(CFRunLoopGetCurrent())
}
}
extension InputStreamOutputWriter: StreamDelegate
{
func stream(_ aStream: Stream, handle eventCode: Stream.Event)
{
if let inputStream = aStream as? InputStream
{
self.inputStream(inputStream, handle: eventCode)
}
else if let outputStream = aStream as? OutputStream
{
self.outputStream(outputStream, handle: eventCode)
}
}
private func inputStream(_ inputStream: InputStream, handle eventCode: Stream.Event)
{
switch eventCode
{
case Stream.Event.hasBytesAvailable:
guard inputStream.streamError == nil else { return }
while inputStream.hasBytesAvailable
{
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: MaximumBufferLength)
let readBytesCount = inputStream.read(buffer, maxLength: MaximumBufferLength)
guard readBytesCount >= 0 else { break }
self.dataBuffer.append(buffer, count: readBytesCount)
buffer.deallocate(capacity: MaximumBufferLength)
self.writeDataBuffer()
}
case Stream.Event.endEncountered:
if self.dataBuffer.count == 0
{
self.finishWriting()
}
case Stream.Event.errorOccurred: self.finishWriting()
default: break
}
}
private func outputStream(_ outputStream: OutputStream, handle eventCode: Stream.Event)
{
switch eventCode
{
case Stream.Event.hasSpaceAvailable:
self.writeDataBuffer()
if self.inputStream.streamStatus == .atEnd
{
self.finishWriting()
}
case Stream.Event.errorOccurred: self.finishWriting()
default: break
}
}
}

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 7.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="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"/>
<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

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13772" systemVersion="16G1114" 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="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" 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="2" 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="195"/>
<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

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14492.1" systemVersion="18G95" 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="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" 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" valueTransformerName="NSSecureUnarchiveFromData" 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" valueTransformerName="NSSecureUnarchiveFromData" 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="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" 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="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" 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="gameCollection" 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="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</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="2" 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="210"/>
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
</elements>
</model>

View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" 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="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" 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" valueTransformerName="NSSecureUnarchiveFromData" 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>
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
<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" valueTransformerName="NSSecureUnarchiveFromData" 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="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" 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="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" 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="gameCollection" 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="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</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="2" 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="163"/>
<element name="Game" positionX="-378" positionY="-54" width="128" height="238"/>
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
</elements>
</model>

View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E287" 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="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" 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" valueTransformerName="NSSecureUnarchiveFromData" 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>
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
<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" valueTransformerName="NSSecureUnarchiveFromData" 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="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" 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="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" 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="gameCollection" 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="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
<attribute name="coreIdentifier" optional="YES" attributeType="String" 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="2" 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="163"/>
<element name="Game" positionX="-378" positionY="-54" width="128" height="238"/>
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
<element name="SaveState" positionX="-198" positionY="113" width="128" height="178"/>
</elements>
</model>

View File

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22158.8" systemVersion="22F66" 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="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" 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" valueTransformerName="NSSecureUnarchiveFromData" 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>
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
<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" valueTransformerName="NSSecureUnarchiveFromData" 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="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" 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="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" 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="gameCollection" 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="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="sha1" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
<attribute name="coreIdentifier" optional="YES" attributeType="String" 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="2" 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>
</model>

View File

@ -9,6 +9,7 @@
import Foundation import Foundation
import DeltaCore import DeltaCore
import Harmony
@objc(Cheat) @objc(Cheat)
public class Cheat: _Cheat, CheatProtocol public class Cheat: _Cheat, CheatProtocol
@ -29,3 +30,32 @@ public class Cheat: _Cheat, CheatProtocol
self.primitiveModifiedDate = date self.primitiveModifiedDate = date
} }
} }
extension Cheat: Syncable
{
public static var syncablePrimaryKey: AnyKeyPath {
return \Cheat.identifier
}
public var syncableKeys: Set<AnyKeyPath> {
return [\Cheat.code, \Cheat.creationDate, \Cheat.modifiedDate, \Cheat.name, \Cheat.type]
}
public var syncableRelationships: Set<AnyKeyPath> {
return [\Cheat.game as AnyKeyPath]
}
public var syncableMetadata: [HarmonyMetadataKey : String] {
guard let game = self.game else { return [:] }
return [.gameID: game.identifier, .gameName: game.name]
}
public var syncableLocalizedName: String? {
return self.name
}
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
{
return .newest
}
}

View File

@ -9,42 +9,31 @@
import Foundation import Foundation
import DeltaCore import DeltaCore
import Harmony
extension ControllerSkinConfigurations extension ControllerSkinConfigurations
{ {
init(traits: DeltaCore.ControllerSkin.Traits) init?(traits: DeltaCore.ControllerSkin.Traits)
{ {
switch traits.deviceType switch (traits.device, traits.displayType, traits.orientation)
{ {
case .iphone: case (.iphone, .standard, .portrait): self = .iphoneStandardPortrait
case (.iphone, .standard, .landscape): self = .iphoneStandardLandscape
case (.iphone, .edgeToEdge, .portrait): self = .iphoneEdgeToEdgePortrait
case (.iphone, .edgeToEdge, .landscape): self = .iphoneEdgeToEdgeLandscape
case (.iphone, .splitView, _): return nil
switch traits.orientation case (.ipad, .standard, .portrait): self = .ipadStandardPortrait
{ case (.ipad, .standard, .landscape): self = .ipadStandardLandscape
case .portrait: self = .fullScreenPortrait case (.ipad, .edgeToEdge, .portrait): self = .ipadEdgeToEdgePortrait
case .landscape: self = .fullScreenLandscape case (.ipad, .edgeToEdge, .landscape): self = .ipadEdgeToEdgeLandscape
} case (.ipad, .splitView, .portrait): self = .ipadSplitViewPortrait
case (.ipad, .splitView, .landscape): self = .ipadSplitViewLandscape
case .ipad:
switch traits.displayMode
{
case .fullScreen:
switch traits.orientation
{
case .portrait: self = .fullScreenPortrait
case .landscape: self = .fullScreenLandscape
}
case .splitView:
switch traits.orientation
{
case .portrait: self = .splitViewPortrait
case .landscape: self = .splitViewLandscape
}
}
case (.tv, .standard, .portrait): self = .tvStandardPortrait
case (.tv, .standard, .landscape): self = .tvStandardLandscape
case (.tv, .edgeToEdge, _): return nil
case (.tv, .splitView, _): return nil
} }
} }
} }
@ -61,7 +50,7 @@ public class ControllerSkin: _ControllerSkin
return self.controllerSkin?.isDebugModeEnabled ?? false 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) let controllerSkin = self.isStandard ? DeltaCore.ControllerSkin.standardControllerSkin(for: self.gameType) : DeltaCore.ControllerSkin(fileURL: self.fileURL)
return controllerSkin return controllerSkin
}() }()
@ -88,9 +77,9 @@ extension ControllerSkin: ControllerSkinProtocol
return self.controllerSkin?.image(for: traits, preferredSize: preferredSize) return self.controllerSkin?.image(for: traits, preferredSize: preferredSize)
} }
public func inputs(for traits: DeltaCore.ControllerSkin.Traits, point: CGPoint) -> [Input]? public func thumbstick(for item: DeltaCore.ControllerSkin.Item, traits: DeltaCore.ControllerSkin.Traits, preferredSize: DeltaCore.ControllerSkin.Size) -> (UIImage, CGSize)?
{ {
return self.controllerSkin?.inputs(for: traits, point: point) return self.controllerSkin?.thumbstick(for: item, traits: traits, preferredSize: preferredSize)
} }
public func items(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Item]? public func items(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Item]?
@ -103,13 +92,46 @@ extension ControllerSkin: ControllerSkinProtocol
return self.controllerSkin?.isTranslucent(for: traits) return self.controllerSkin?.isTranslucent(for: traits)
} }
public func gameScreenFrame(for traits: DeltaCore.ControllerSkin.Traits) -> CGRect? public func screens(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Screen]?
{ {
return self.controllerSkin?.gameScreenFrame(for: traits) return self.controllerSkin?.screens(for: traits)
} }
public func aspectRatio(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize? public func aspectRatio(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize?
{ {
return self.controllerSkin?.aspectRatio(for: traits) return self.controllerSkin?.aspectRatio(for: traits)
} }
public func contentSize(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize?
{
return self.controllerSkin?.contentSize(for: traits)
}
}
extension ControllerSkin: Syncable
{
public static var syncablePrimaryKey: AnyKeyPath {
return \ControllerSkin.identifier
}
public var syncableKeys: Set<AnyKeyPath> {
return [\ControllerSkin.filename, \ControllerSkin.gameType, \ControllerSkin.name, \ControllerSkin.supportedConfigurations]
}
public var syncableFiles: Set<File> {
return [File(identifier: "skin", fileURL: self.fileURL)]
}
public var isSyncingEnabled: Bool {
return !self.isStandard
}
public var syncableLocalizedName: String? {
return self.name
}
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
{
return .newest
}
} }

View File

@ -9,13 +9,22 @@
import Foundation import Foundation
import DeltaCore import DeltaCore
import MelonDSDeltaCore
import Harmony
public extension Game
{
static let melonDSBIOSIdentifier = "com.rileytestut.MelonDSDeltaCore.BIOS"
static let melonDSDSiBIOSIdentifier = "com.rileytestut.MelonDSDeltaCore.DSiBIOS"
}
@objc(Game) @objc(Game)
public class Game: _Game, GameProtocol public class Game: _Game, GameProtocol
{ {
public var fileURL: URL { public var fileURL: URL {
var fileURL: URL! var fileURL: URL!
// self URL
self.managedObjectContext?.performAndWait { self.managedObjectContext?.performAndWait {
fileURL = DatabaseManager.gamesDirectoryURL.appendingPathComponent(self.filename) fileURL = DatabaseManager.gamesDirectoryURL.appendingPathComponent(self.filename)
} }
@ -29,11 +38,29 @@ public class Game: _Game, GameProtocol
var artworkURL = self.primitiveValue(forKey: #keyPath(Game.artworkURL)) as? URL var artworkURL = self.primitiveValue(forKey: #keyPath(Game.artworkURL)) as? URL
self.didAccessValue(forKey: #keyPath(Game.artworkURL)) self.didAccessValue(forKey: #keyPath(Game.artworkURL))
if let unwrappedArtworkURL = artworkURL, unwrappedArtworkURL.isFileURL if let unwrappedArtworkURL = artworkURL
{
if unwrappedArtworkURL.isFileURL
{ {
// Recreate the stored URL relative to current sandbox location. // Recreate the stored URL relative to current sandbox location.
artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL) artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL)
} }
else if let host = unwrappedArtworkURL.host?.lowercased(), host == "img.gamefaqs.net" || host == "gamefaqs1.cbsistatic.com",
var components = URLComponents(url: unwrappedArtworkURL, resolvingAgainstBaseURL: false)
{
// Quick fix for broken album artwork URLs due to host change.
components.host = "gamefaqs.gamespot.com"
components.scheme = "https"
let updatedPath = "/a" + components.path
components.path = updatedPath
if let url = components.url
{
artworkURL = url
}
}
}
return artworkURL return artworkURL
} }
@ -55,6 +82,18 @@ public class Game: _Game, GameProtocol
} }
} }
extension Game
{
class var recentlyPlayedFetchRequest: NSFetchRequest<Game> {
let fetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K != nil", #keyPath(Game.playedDate))
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Game.playedDate, ascending: false), NSSortDescriptor(keyPath: \Game.name, ascending: true)]
fetchRequest.fetchLimit = 4
return fetchRequest
}
}
extension Game extension Game
{ {
override public func prepareForDeletion() override public func prepareForDeletion()
@ -63,11 +102,17 @@ extension Game
guard let managedObjectContext = self.managedObjectContext else { return } guard let managedObjectContext = self.managedObjectContext else { return }
// If filename == empty string (e.g. during merge), ignore this deletion.
// Otherwise, we may accidentally delete the entire Games directory!
guard !self.filename.isEmpty else { return }
// If a game with the same identifier is also currently being inserted, Core Data is more than likely resolving a conflict by deleting the previous instance // If a game with the same identifier is also currently being inserted, Core Data is more than likely resolving a conflict by deleting the previous instance
// In this case, we make sure we DON'T delete the game file + misc other Core Data relationships, or else we'll just lose all that data // In this case, we make sure we DON'T delete the game file + misc other Core Data relationships, or else we'll just lose all that data
guard !managedObjectContext.insertedObjects.contains(where: { ($0 as? Game)?.identifier == self.identifier }) else { return } guard !managedObjectContext.insertedObjects.contains(where: { ($0 as? Game)?.identifier == self.identifier }) else { return }
guard FileManager.default.fileExists(atPath: self.fileURL.path) else { return } // Double-check fileURL is NOT actually a directory, which we should never delete.
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: self.fileURL.path, isDirectory: &isDirectory), !isDirectory.boolValue else { return }
do do
{ {
@ -78,7 +123,7 @@ extension Game
print(error) print(error)
} }
for collection in self.gameCollections where collection.games.count == 1 if let collection = self.gameCollection, collection.games.count == 1
{ {
// Once this game is deleted, collection will have 0 games, so we should delete it // Once this game is deleted, collection will have 0 games, so we should delete it
managedObjectContext.delete(collection) managedObjectContext.delete(collection)
@ -97,3 +142,71 @@ extension Game
} }
} }
} }
extension Game: Syncable
{
public static var syncablePrimaryKey: AnyKeyPath {
return \Game.identifier
}
public var syncableKeys: Set<AnyKeyPath> {
return [\Game.artworkURL, \Game.filename, \Game.name, \Game.type]
}
public var syncableFiles: Set<File> {
let artworkURL: URL
if let fileURL = self.artworkURL, fileURL.isFileURL
{
artworkURL = fileURL
}
else
{
artworkURL = DatabaseManager.artworkURL(for: self)
}
let artworkFile = File(identifier: "artwork", fileURL: artworkURL)
switch self.identifier
{
case Game.melonDSBIOSIdentifier:
let bios7File = File(identifier: "bios7", fileURL: MelonDSEmulatorBridge.shared.bios7URL)
let bios9File = File(identifier: "bios9", fileURL: MelonDSEmulatorBridge.shared.bios9URL)
let firmwareFile = File(identifier: "firmware", fileURL: MelonDSEmulatorBridge.shared.firmwareURL)
return [artworkFile, bios7File, bios9File, firmwareFile]
case Game.melonDSDSiBIOSIdentifier:
let bios7File = File(identifier: "bios7", fileURL: MelonDSEmulatorBridge.shared.dsiBIOS7URL)
let bios9File = File(identifier: "bios9", fileURL: MelonDSEmulatorBridge.shared.dsiBIOS9URL)
let firmwareFile = File(identifier: "firmware", fileURL: MelonDSEmulatorBridge.shared.dsiFirmwareURL)
// DSi NAND is ~240MB, so don't sync for now until Harmony can selectively download files.
// let nandFile = File(identifier: "nand", fileURL: MelonDSEmulatorBridge.shared.dsiNANDURL)
return [artworkFile, bios7File, bios9File, firmwareFile]
default:
let gameFile = File(identifier: "game", fileURL: self.fileURL)
return [artworkFile, gameFile]
}
}
public var syncableRelationships: Set<AnyKeyPath> {
return [\Game.gameCollection]
}
public var syncableLocalizedName: String? {
return self.name
}
public func awakeFromSync(_ record: AnyRecord) throws
{
guard let gameCollection = self.gameCollection else { throw SyncValidationError.incorrectGameCollection(nil) }
if gameCollection.identifier != self.type.rawValue
{
throw SyncValidationError.incorrectGameCollection(gameCollection.name)
}
}
}

View File

@ -9,15 +9,16 @@
import CoreData import CoreData
import DeltaCore import DeltaCore
import Harmony
@objc(GameCollection) @objc(GameCollection)
public class GameCollection: _GameCollection public class GameCollection: _GameCollection
{ {
var name: String { @objc var name: String {
return self.system?.localizedName ?? NSLocalizedString("Unknown", comment: "") return self.system?.localizedName ?? NSLocalizedString("Unknown", comment: "")
} }
var shortName: String { @objc var shortName: String {
return self.system?.localizedShortName ?? NSLocalizedString("Unknown", comment: "") return self.system?.localizedShortName ?? NSLocalizedString("Unknown", comment: "")
} }
@ -28,3 +29,23 @@ public class GameCollection: _GameCollection
return system return system
} }
} }
extension GameCollection: Syncable
{
public static var syncablePrimaryKey: AnyKeyPath {
return \GameCollection.identifier
}
public var syncableKeys: Set<AnyKeyPath> {
return [\GameCollection.index as AnyKeyPath]
}
public var syncableLocalizedName: String? {
return self.name
}
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
{
return .newest
}
}

View File

@ -0,0 +1,108 @@
//
// GameControllerInputMapping.swift
// Delta
//
// Created by Riley Testut on 8/30/16.
// Copyright (c) 2016 Riley Testut. All rights reserved.
//
import Foundation
import DeltaCore
import Harmony
@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
}
public override func awakeFromInsert()
{
super.awakeFromInsert()
self.identifier = UUID().uuidString
}
}
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(where: { !$0.isDeleted })
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)
}
}
extension GameControllerInputMapping: Syncable
{
public static var syncablePrimaryKey: AnyKeyPath {
return \GameControllerInputMapping.identifier
}
public var syncableKeys: Set<AnyKeyPath> {
return [\GameControllerInputMapping.deltaCoreInputMapping,
\GameControllerInputMapping.gameControllerInputType,
\GameControllerInputMapping.gameType,
\GameControllerInputMapping.playerIndex]
}
public var syncableLocalizedName: String? {
return self.name
}
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
{
return .newest
}
}

View File

@ -0,0 +1,116 @@
//
// GameSave.swift
// Delta
//
// Created by Riley Testut on 8/30/16.
// Copyright (c) 2016 Riley Testut. All rights reserved.
//
import Foundation
import GBCDeltaCore
import Harmony
@objc(GameSave)
public class GameSave: _GameSave
{
public override func awakeFromInsert()
{
super.awakeFromInsert()
self.modifiedDate = Date()
}
}
extension GameSave: Syncable
{
public static var syncablePrimaryKey: AnyKeyPath {
return \GameSave.identifier
}
public var syncableKeys: Set<AnyKeyPath> {
return [\GameSave.modifiedDate, \GameSave.sha1]
}
public var syncableRelationships: Set<AnyKeyPath> {
return [\GameSave.game]
}
public var syncableFiles: Set<File> {
guard let game = self.game else { return [] }
var files: Set<File> = [File(identifier: "gameSave", fileURL: game.gameSaveURL)]
if game.type == .gbc
{
let gameTimeSaveURL = game.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
files.insert(File(identifier: "gameTimeSave", fileURL: gameTimeSaveURL))
}
return files
}
public var syncableMetadata: [HarmonyMetadataKey : String] {
guard let game = self.game else { return [:] }
// Use self.identifier to always link with exact matching game.
return [.gameID: self.identifier, .gameName: game.name]
}
public var syncableLocalizedName: String? {
return self.game?.name
}
public var isSyncingEnabled: Bool {
// self.game may be nil if being downloaded, so don't enforce it.
// guard let identifier = self.game?.identifier else { return false }
return self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier
}
public func awakeFromSync(_ record: AnyRecord) throws
{
do
{
guard let game = self.game else { throw SyncValidationError.incorrectGame(nil) }
if game.identifier != self.identifier
{
let fetchRequest = GameSave.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(GameSave.identifier), game.identifier)
if let misplacedGameSave = try self.managedObjectContext?.fetch(fetchRequest).first, misplacedGameSave.game == nil
{
// Relink game with its correct gameSave, in case we accidentally misplaced it.
// Otherwise, corrupted records might displace already-downloaded GameSaves
// due to automatic Core Data relationship propagation, despite us throwing error.
game.gameSave = misplacedGameSave
}
else
{
// Either there is no misplacedGameSave, or there is but it's linked to another game somehow.
game.gameSave = nil
}
throw SyncValidationError.incorrectGame(game.name)
}
}
catch let error as SyncValidationError
{
guard SyncManager.shared.ignoredCorruptedRecordIDs.contains(record.recordID) else { throw error }
let fetchRequest = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), self.identifier)
if let correctGame = try self.managedObjectContext?.fetch(fetchRequest).first
{
self.game = correctGame
}
else
{
throw ValidationError.nilRelationshipObjects(keys: [#keyPath(GameSave.game)])
}
}
}
}

View File

@ -9,10 +9,14 @@
import Foundation import Foundation
import DeltaCore import DeltaCore
import Harmony
import struct DSDeltaCore.DS
@objc public enum SaveStateType: Int16 @objc public enum SaveStateType: Int16
{ {
case auto case auto
case quick
case general case general
case locked case locked
} }
@ -20,6 +24,13 @@ import DeltaCore
@objc(SaveState) @objc(SaveState)
public class SaveState: _SaveState, SaveStateProtocol public class SaveState: _SaveState, SaveStateProtocol
{ {
public static let localizedDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .short
dateFormatter.dateStyle = .short
return dateFormatter
}()
public var fileURL: URL { public var fileURL: URL {
let fileURL = DatabaseManager.saveStatesDirectoryURL(for: self.game!).appendingPathComponent(self.filename) let fileURL = DatabaseManager.saveStatesDirectoryURL(for: self.game!).appendingPathComponent(self.filename)
return fileURL return fileURL
@ -35,6 +46,11 @@ public class SaveState: _SaveState, SaveStateProtocol
return self.game!.type return self.game!.type
} }
public var localizedName: String {
let localizedName = self.name ?? SaveState.localizedDateFormatter.string(from: self.modifiedDate)
return localizedName
}
@NSManaged private var primitiveFilename: String @NSManaged private var primitiveFilename: String
@NSManaged private var primitiveIdentifier: String @NSManaged private var primitiveIdentifier: String
@NSManaged private var primitiveCreationDate: Date @NSManaged private var primitiveCreationDate: Date
@ -78,4 +94,101 @@ public class SaveState: _SaveState, SaveStateProtocol
print(error) 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
}
}
extension SaveState: Syncable
{
public static var syncablePrimaryKey: AnyKeyPath {
return \SaveState.identifier
}
public var syncableKeys: Set<AnyKeyPath> {
return [\SaveState.creationDate, \SaveState.filename, \SaveState.modifiedDate, \SaveState.name, \SaveState.type, \SaveState.coreIdentifier]
}
public var syncableFiles: Set<File> {
return [File(identifier: "saveState", fileURL: self.fileURL), File(identifier: "thumbnail", fileURL: self.imageFileURL)]
}
public var syncableRelationships: Set<AnyKeyPath> {
return [\SaveState.game]
}
public var isSyncingEnabled: Bool {
// self.game may be nil if being downloaded, so don't enforce it.
// guard let identifier = self.game?.identifier else { return false }
let isSyncingEnabled = (self.type != .auto && self.type != .quick) && (self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier)
return isSyncingEnabled
}
public var syncableMetadata: [HarmonyMetadataKey : String] {
guard let game = self.game else { return [:] }
return [.gameID: game.identifier, .gameName: game.name, .coreID: self.coreIdentifier, .verifiedGameID: game.identifier].compactMapValues { $0 }
}
public var syncableLocalizedName: String? {
return self.localizedName
}
public func awakeFromSync(_ record: AnyRecord) throws
{
let verifiedGameID = record.remoteMetadata?[.verifiedGameID]
do
{
guard let game = self.game else { return }
if let system = System(gameType: game.type), self.coreIdentifier == nil
{
if let coreIdentifier = record.remoteMetadata?[.coreID]
{
// SaveState was synced to older version of Delta and lost its coreIdentifier,
// but it remains in the remote metadata so we can reassign it.
self.coreIdentifier = coreIdentifier
}
else
{
switch system
{
case .ds: self.coreIdentifier = DS.core.identifier // Assume DS save state with nil coreIdentifier is from DeSmuME core.
default: self.coreIdentifier = system.deltaCore.identifier
}
}
}
if let verifiedGameID, verifiedGameID != game.identifier
{
// Game does not match verified game ID, which most likely means
// this SaveState was reviewed + fixed on another device, but not uploaded.
throw SyncValidationError.incorrectGame(game.name)
}
}
catch let error as SyncValidationError
{
guard let verifiedGameID, SyncManager.shared.ignoredCorruptedRecordIDs.contains(record.recordID) else { throw error }
let fetchRequest = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), verifiedGameID)
if let correctGame = try self.managedObjectContext?.fetch(fetchRequest).first
{
self.game = correctGame
}
else
{
throw ValidationError.nilRelationshipObjects(keys: [#keyPath(GameSave.game)])
}
}
}
} }

View File

@ -24,7 +24,7 @@ public class _Cheat: NSManagedObject
@NSManaged public var modifiedDate: Date @NSManaged public var modifiedDate: Date
@NSManaged public var name: String? @NSManaged public var name: String
@NSManaged public var type: CheatType @NSManaged public var type: CheatType

View File

@ -28,5 +28,9 @@ public class _ControllerSkin: NSManagedObject
// MARK: - Relationships // MARK: - Relationships
@NSManaged public var preferredLandscapeSkinByGames: Set<Game>
@NSManaged public var preferredPortraitSkinByGames: Set<Game>
} }

View File

@ -22,13 +22,21 @@ public class _Game: NSManagedObject
@NSManaged public var name: String @NSManaged public var name: String
@NSManaged public var playedDate: Date?
@NSManaged public var type: GameType @NSManaged public var type: GameType
// MARK: - Relationships // MARK: - Relationships
@NSManaged public var cheats: Set<Cheat> @NSManaged public var cheats: Set<Cheat>
@NSManaged public var gameCollections: Set<GameCollection> @NSManaged public var gameCollection: GameCollection?
@NSManaged public var gameSave: GameSave?
@NSManaged public var preferredLandscapeSkin: ControllerSkin?
@NSManaged public var preferredPortraitSkin: ControllerSkin?
@NSManaged public var previewSaveState: SaveState? @NSManaged public var previewSaveState: SaveState?

View File

@ -0,0 +1,30 @@
// 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 identifier: String
@NSManaged public var playerIndex: Int16
// MARK: - Relationships
}

View File

@ -0,0 +1,28 @@
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
// Make changes to GameSave.swift instead.
import Foundation
import CoreData
import DeltaCore
public class _GameSave: NSManagedObject
{
@nonobjc public class func fetchRequest() -> NSFetchRequest<GameSave> {
return NSFetchRequest<GameSave>(entityName: "GameSave")
}
// MARK: - Properties
@NSManaged public var identifier: String
@NSManaged public var modifiedDate: Date
@NSManaged public var sha1: String?
// MARK: - Relationships
@NSManaged public var game: Game?
}

View File

@ -14,6 +14,8 @@ public class _SaveState: NSManagedObject
// MARK: - Properties // MARK: - Properties
@NSManaged public var coreIdentifier: String?
@NSManaged public var creationDate: Date @NSManaged public var creationDate: Date
@NSManaged public var filename: String @NSManaged public var filename: String

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,19 @@
//
// GameControllerInputMappingMigrationPolicy.swift
// Delta
//
// Created by Riley Testut on 1/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
@objc(GameControllerInputMappingMigrationPolicy)
class GameControllerInputMappingMigrationPolicy: NSEntityMigrationPolicy
{
@objc(migrateIdentifier)
func migrateIdentifier() -> String
{
return UUID().uuidString
}
}

View File

@ -0,0 +1,45 @@
//
// SaveStateMigrationPolicy.swift
// Delta
//
// Created by Riley Testut on 9/28/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import DeltaCore
import struct DSDeltaCore.DS
@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
}
}
}
// Delta5 to Delta6
extension SaveStateToSaveStateMigrationPolicy
{
@objc(defaultCoreIdentifierForGameType:)
func defaultCoreIdentifier(for gameType: GameType) -> String?
{
guard let system = System(gameType: gameType) else { return nil }
switch system
{
case .ds: return DS.core.identifier // Assume any existing save state is from DeSmuME.
default: return system.deltaCore.identifier
}
}
}

View File

@ -9,13 +9,35 @@
#ifndef ControllerSkinConfigurations_h #ifndef ControllerSkinConfigurations_h
#define ControllerSkinConfigurations_h #define ControllerSkinConfigurations_h
// Every possible (supported) combination of traits.
typedef NS_OPTIONS(int16_t, ControllerSkinConfigurations) typedef NS_OPTIONS(int16_t, ControllerSkinConfigurations)
{ {
ControllerSkinConfigurationFullScreenPortrait = 1 << 0, /* iPhone */
ControllerSkinConfigurationFullScreenLandscape = 1 << 1, ControllerSkinConfigurationiPhoneStandardPortrait NS_SWIFT_NAME(iphoneStandardPortrait) = 1 << 0,
ControllerSkinConfigurationiPhoneStandardLandscape NS_SWIFT_NAME(iphoneStandardLandscape) = 1 << 1,
ControllerSkinConfigurationSplitViewPortrait = 1 << 2, // iPhone doesn't support Split View
ControllerSkinConfigurationSplitViewLandscape = 1 << 3, // ControllerSkinConfigurationiPhoneSplitViewPortrait = 1 << 2,
// ControllerSkinConfigurationiPhoneSplitViewLandscape = 1 << 3,
ControllerSkinConfigurationiPhoneEdgeToEdgePortrait NS_SWIFT_NAME(iphoneEdgeToEdgePortrait) = 1 << 4,
ControllerSkinConfigurationiPhoneEdgeToEdgeLandscape NS_SWIFT_NAME(iphoneEdgeToEdgeLandscape) = 1 << 5,
/* iPad */
ControllerSkinConfigurationiPadStandardPortrait NS_SWIFT_NAME(ipadStandardPortrait) = 1 << 6,
ControllerSkinConfigurationiPadStandardLandscape NS_SWIFT_NAME(ipadStandardLandscape) = 1 << 7,
ControllerSkinConfigurationiPadSplitViewPortrait NS_SWIFT_NAME(ipadSplitViewPortrait) = 1 << 2, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewPortrait
ControllerSkinConfigurationiPadSplitViewLandscape NS_SWIFT_NAME(ipadSplitViewLandscape) = 1 << 3, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewLandscape
ControllerSkinConfigurationiPadEdgeToEdgePortrait NS_SWIFT_NAME(ipadEdgeToEdgePortrait) = 1 << 8,
ControllerSkinConfigurationiPadEdgeToEdgeLandscape NS_SWIFT_NAME(ipadEdgeToEdgeLandscape) = 1 << 9,
/* TV */
ControllerSkinConfigurationTVStandardPortrait = 1 << 10,
ControllerSkinConfigurationTVStandardLandscape = 1 << 11,
}; };
#endif /* ControllerSkinConfigurations_h */ #endif /* ControllerSkinConfigurations_h */

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.78" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="LocalRecord" representedClassName="LocalRecord" syncable="YES">
<attribute name="modificationDate" attributeType="Date" syncable="YES"/>
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectURI" attributeType="URI" syncable="YES"/>
<attribute name="status" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="versionDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="versionIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="managedRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedRecord" inverseName="localRecord" inverseEntity="ManagedRecord" syncable="YES"/>
<relationship name="remoteFiles" toMany="YES" deletionRule="Cascade" destinationEntity="RemoteFile" inverseName="localRecord" inverseEntity="RemoteFile" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="recordedObjectType"/>
<constraint value="recordedObjectIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="ManagedRecord" representedClassName="ManagedRecord" syncable="YES">
<attribute name="isConflicted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="isSyncingEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
<relationship name="localRecord" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="LocalRecord" inverseName="managedRecord" inverseEntity="LocalRecord" syncable="YES"/>
<relationship name="remoteRecord" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="RemoteRecord" inverseName="managedRecord" inverseEntity="RemoteRecord" syncable="YES"/>
<fetchIndex name="byRecordedObject">
<fetchIndexElement property="recordedObjectType" type="Binary" order="ascending"/>
<fetchIndexElement property="recordedObjectIdentifier" type="Binary" order="ascending"/>
</fetchIndex>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="recordedObjectType"/>
<constraint value="recordedObjectIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RemoteFile" representedClassName="RemoteFile" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="remoteIdentifier" attributeType="String" syncable="YES"/>
<attribute name="sha1Hash" attributeType="String" syncable="YES"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
<attribute name="versionIdentifier" attributeType="String" syncable="YES"/>
<relationship name="localRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LocalRecord" inverseName="remoteFiles" inverseEntity="LocalRecord" syncable="YES"/>
</entity>
<entity name="RemoteRecord" representedClassName="RemoteRecord" syncable="YES">
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isLocked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="localizedName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="metadata" optional="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="previousVersionDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="previousVersionIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
<attribute name="status" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="versionIdentifier" attributeType="String" syncable="YES"/>
<relationship name="managedRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedRecord" inverseName="remoteRecord" inverseEntity="ManagedRecord" syncable="YES"/>
<fetchIndex name="byIdentifier">
<fetchIndexElement property="identifier" type="Binary" order="ascending"/>
</fetchIndex>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="recordedObjectType"/>
<constraint value="recordedObjectIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<configuration name="External"/>
<configuration name="Harmony">
<memberEntity name="ManagedRecord"/>
<memberEntity name="LocalRecord"/>
<memberEntity name="RemoteRecord"/>
<memberEntity name="RemoteFile"/>
</configuration>
<elements>
<element name="LocalRecord" positionX="93.01953125" positionY="273.0234375" width="128" height="180"/>
<element name="ManagedRecord" positionX="298.4375" positionY="225.49609375" width="128" height="133"/>
<element name="RemoteFile" positionX="288" positionY="378" width="128" height="135"/>
<element name="RemoteRecord" positionX="488.2578125" positionY="272.56640625" width="128" height="240"/>
</elements>
</model>

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,34 @@
import Foundation import Foundation
// Must be a class (not struct) so it can be used with Objective-C generics // Must be an NSObject subclass so it can be used with RSTCellContentDataSource.
class GameMetadata class GameMetadata: NSObject
{ {
var name: String? let releaseID: Int
var artworkURL: URL? let romID: Int
let name: String?
let artworkURL: URL?
init(releaseID: Int, romID: Int, name: String?, artworkURL: URL?)
{
self.releaseID = releaseID
self.romID = romID
self.name = name
self.artworkURL = artworkURL
}
}
extension GameMetadata
{
override var hash: Int {
return self.releaseID.hashValue ^ self.romID.hashValue
}
override func isEqual(_ object: Any?) -> Bool
{
guard let metadata = object as? GameMetadata else { return false }
return self.releaseID == metadata.releaseID && self.romID == metadata.romID
}
} }

View File

@ -9,6 +9,11 @@
import Foundation import Foundation
import SQLite import SQLite
private extension UserDefaults
{
@NSManaged var previousGamesDatabaseVersion: Int
}
extension ExpressionType extension ExpressionType
{ {
static var name: SQLite.Expression<String?> { static var name: SQLite.Expression<String?> {
@ -19,13 +24,17 @@ extension ExpressionType
return SQLite.Expression<String?>("releaseCoverFront") return SQLite.Expression<String?>("releaseCoverFront")
} }
static var hash: SQLite.Expression<String> { static var sha1Hash: SQLite.Expression<String> {
return SQLite.Expression<String>("romHashSHA1") return SQLite.Expression<String>("romHashSHA1")
} }
static var romID: SQLite.Expression<Int> { static var romID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("romID") return SQLite.Expression<Int>("romID")
} }
static var releaseID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("releaseID")
}
} }
extension Table extension Table
@ -48,16 +57,28 @@ extension VirtualTable
extension GamesDatabase extension GamesDatabase
{ {
enum Error: Swift.Error enum Error: LocalizedError
{ {
case doesNotExist case doesNotExist
case connection(Swift.Error)
var errorDescription: String? {
switch self
{
case .doesNotExist:
return NSLocalizedString("The SQLite database could not be found.", comment: "")
}
}
} }
} }
class GamesDatabase class GamesDatabase
{ {
fileprivate let connection: Connection static let version = 3
static var previousVersion: Int? {
return UserDefaults.standard.previousGamesDatabaseVersion
}
private let connection: Connection
init() throws init() throws
{ {
@ -69,30 +90,39 @@ class GamesDatabase
} }
catch catch
{ {
throw Error.connection(error) throw error
} }
self.invalidateVirtualTableIfNeeded()
} }
func metadataResults(forGameName gameName: String) -> [GameMetadata] func metadataResults(forGameName gameName: String) -> [GameMetadata]
{ {
let releaseID = Expression<Any>.releaseID
let romID = Expression<Any>.romID
let name = Expression<Any>.name let name = Expression<Any>.name
let artworkAddress = Expression<Any>.artworkAddress let artworkAddress = Expression<Any>.artworkAddress
let query = VirtualTable.search.select(name, artworkAddress).filter(name.match(gameName + "*")) let query = VirtualTable.search.select(releaseID, romID, name, artworkAddress).filter(name.match(gameName + "*"))
do do
{ {
let rows = try self.connection.prepare(query) let rows = try self.connection.prepare(query)
let results = rows.map { row -> GameMetadata in let results = rows.map { (row) -> GameMetadata in
let metadata = GameMetadata()
metadata.name = row[name]
let artworkURL: URL?
if let address = row[artworkAddress] if let address = row[artworkAddress]
{ {
metadata.artworkURL = URL(string: address) artworkURL = URL(string: address)
}
else
{
artworkURL = nil
} }
let metadata = GameMetadata(releaseID: row[releaseID], romID: row[romID], name: row[name], artworkURL: artworkURL)
return metadata return metadata
} }
@ -118,26 +148,31 @@ class GamesDatabase
func metadata(for game: Game) -> GameMetadata? func metadata(for game: Game) -> GameMetadata?
{ {
let releaseID = Expression<Any>.releaseID
let name = Expression<Any>.name let name = Expression<Any>.name
let artworkAddress = Expression<Any>.artworkAddress let artworkAddress = Expression<Any>.artworkAddress
let hash = Expression<Any>.hash
let sha1Hash = Expression<Any>.sha1Hash
let romID = Expression<Any>.romID let romID = Expression<Any>.romID
let gameHash = game.identifier.uppercased() 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, Table.roms[romID]).filter(sha1Hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID])
do do
{ {
if let row = try self.connection.pluck(query) if let row = try self.connection.pluck(query)
{ {
let metadata = GameMetadata() let artworkURL: URL?
metadata.name = row[name]
if let address = row[artworkAddress] if let address = row[artworkAddress]
{ {
metadata.artworkURL = URL(string: address) artworkURL = URL(string: address)
}
else
{
artworkURL = nil
} }
let metadata = GameMetadata(releaseID: row[releaseID], romID: row[Table.roms[romID]], name: row[name], artworkURL: artworkURL)
return metadata return metadata
} }
} }
@ -152,16 +187,34 @@ class GamesDatabase
private extension 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 func prepareFTS() -> Bool
{ {
let name = Expression<Any>.name let name = Expression<Any>.name
let artworkAddress = Expression<Any>.artworkAddress let artworkAddress = Expression<Any>.artworkAddress
let releaseID = Expression<Any>.releaseID
let romID = Expression<Any>.romID
do do
{ {
try self.connection.run(VirtualTable.search.create(.FTS4([name, artworkAddress], tokenize: .Unicode61()))) try self.connection.run(VirtualTable.search.create(.FTS4([releaseID, romID, name, artworkAddress], tokenize: .Unicode61())))
let update = VirtualTable.search.insert(Table.releases.select(name, artworkAddress)) let update = VirtualTable.search.insert(Table.releases.select(releaseID, romID, name, artworkAddress))
_ = try self.connection.run(update) _ = try self.connection.run(update)
} }
catch catch

View File

@ -15,13 +15,11 @@ class GamesDatabaseBrowserViewController: UITableViewController
{ {
var selectionHandler: ((GameMetadata) -> Void)? var selectionHandler: ((GameMetadata) -> Void)?
fileprivate let database: GamesDatabase? private let database: GamesDatabase?
fileprivate let dataSource: RSTArrayTableViewDataSource<GameMetadata>
fileprivate let operationQueue = RSTOperationQueue() private let dataSource: RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>
fileprivate let imageCache = NSCache<NSURL, UIImage>()
override init(style: UITableViewStyle) { override init(style: UITableView.Style) {
fatalError() fatalError()
} }
@ -37,39 +35,13 @@ class GamesDatabaseBrowserViewController: UITableViewController
print(error) print(error)
} }
self.dataSource = RSTArrayTableViewDataSource<GameMetadata>(items: []) self.dataSource = RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>(items: [])
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.textColor = UIColor.lightText
placeholderView.detailTextLabel.textColor = UIColor.lightText
self.dataSource.placeholderView = placeholderView
super.init(coder: aDecoder) 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.definesPresentationContext = true
self.prepareDataSource()
} }
override var preferredStatusBarStyle: UIStatusBarStyle { override var preferredStatusBarStyle: UIStatusBarStyle {
@ -82,26 +54,105 @@ class GamesDatabaseBrowserViewController: UITableViewController
self.view.backgroundColor = UIColor.deltaDarkGray self.view.backgroundColor = UIColor.deltaDarkGray
self.tableView.register(GameTableViewCell.nib!, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.tableView.dataSource = self.dataSource self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
self.tableView.indicatorStyle = .white self.tableView.indicatorStyle = .white
self.tableView.separatorColor = UIColor.gray self.tableView.separatorColor = UIColor.gray
self.dataSource.searchController.delegate = self self.dataSource.searchController.delegate = self
self.dataSource.searchController.searchBar.barStyle = .blackTranslucent self.dataSource.searchController.searchBar.barStyle = .black
self.tableView.tableHeaderView = self.dataSource.searchController.searchBar
self.navigationItem.searchController = self.dataSource.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.updatePlaceholderView() self.updatePlaceholderView()
} }
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
self.dataSource.searchController.isActive = true
}
override func didReceiveMemoryWarning() override func didReceiveMemoryWarning()
{ {
super.didReceiveMemoryWarning() super.didReceiveMemoryWarning()
} }
} }
extension GamesDatabaseBrowserViewController private extension GamesDatabaseBrowserViewController
{ {
func configure(cell: GameMetadataTableViewCell, with metadata: GameMetadata, for indexPath: IndexPath) 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! GameTableViewCell, 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! GameTableViewCell
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: GameTableViewCell, with metadata: GameMetadata, for indexPath: IndexPath)
{ {
cell.backgroundColor = UIColor.deltaDarkGray cell.backgroundColor = UIColor.deltaDarkGray
@ -112,30 +163,6 @@ extension GamesDatabaseBrowserViewController
cell.artworkImageViewTrailingConstraint.constant = 15 cell.artworkImageViewTrailingConstraint.constant = 15
cell.separatorInset.left = cell.nameLabel.frame.minX 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() func updatePlaceholderView()
@ -153,6 +180,12 @@ extension GamesDatabaseBrowserViewController
placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure the name is correct, or try searching for another game.", comment: "") 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.view.safeAreaInsets.top), animated: false)
}
} }
extension GamesDatabaseBrowserViewController extension GamesDatabaseBrowserViewController
@ -167,20 +200,15 @@ extension GamesDatabaseBrowserViewController
let metadata = self.dataSource.item(at: indexPath) let metadata = self.dataSource.item(at: indexPath)
self.selectionHandler?(metadata) 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 extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
{ {
func didPresentSearchController(_ searchController: UISearchController) func didPresentSearchController(_ searchController: UISearchController)
{ {
// Fix incorrect table view scroll indicator insets DispatchQueue.main.async {
self.tableView.scrollIndicatorInsets.top = self.navigationController!.navigationBar.bounds.height + UIApplication.shared.statusBarFrame.height searchController.searchBar.becomeFirstResponder()
}
} }
func willDismissSearchController(_ searchController: UISearchController) func willDismissSearchController(_ searchController: UISearchController)
@ -193,7 +221,6 @@ extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
func didDismissSearchController(_ searchController: UISearchController) func didDismissSearchController(_ searchController: UISearchController)
{ {
// Fix potentially incorrect offset if user dismisses searchController while scrolling // Fix potentially incorrect offset if user dismisses searchController while scrolling
self.tableView.setContentOffset(CGPoint.zero, animated: false) self.resetTableViewContentOffset()
self.tableView.setContentOffset(CGPoint(x: 0, y: -self.topLayoutGuide.length), animated: false)
} }
} }

View File

@ -0,0 +1,126 @@
//
// GamePickerViewController.swift
// Delta
//
// Created by Riley Testut on 8/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class GamePickerViewController: UITableViewController
{
private lazy var dataSource = self.makeDataSource()
var gameHandler: ((Game?) -> Void)?
init()
{
super.init(style: .insetGrouped)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.navigationController?.delegate = self
self.dataSource.proxy = self
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.navigationItem.title = NSLocalizedString("Choose Game", comment: "")
self.navigationItem.searchController = self.dataSource.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
}
}
private extension GamePickerViewController
{
func makeDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource<Game, UIImage>
{
let fetchRequest = Game.fetchRequest()
fetchRequest.propertiesToFetch = [#keyPath(Game.name), #keyPath(Game.identifier), #keyPath(Game.artworkURL)]
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Game.gameCollection?.index, ascending: true), NSSortDescriptor(keyPath: \Game.name, ascending: true)]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(Game.gameCollection.name), cacheName: nil)
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<Game, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.cellConfigurationHandler = { (cell, game, indexPath) in
var configuration = UIListContentConfiguration.valueCell()
configuration.prefersSideBySideTextAndSecondaryText = false
configuration.text = game.name
configuration.secondaryText = game.identifier
configuration.secondaryTextProperties.font = .preferredFont(forTextStyle: .caption1)
configuration.image = UIImage(named: "BoxArt")
configuration.imageProperties.maximumSize = CGSize(width: 48, height: 48)
configuration.imageProperties.reservedLayoutSize = CGSize(width: 48, height: 48)
configuration.imageProperties.cornerRadius = 4
cell.contentConfiguration = configuration
}
dataSource.prefetchHandler = { (game, indexPath, completionHandler) in
guard let artworkURL = game.artworkURL else {
completionHandler(nil, nil)
return nil
}
let imageOperation = LoadImageURLOperation(url: artworkURL)
imageOperation.resultHandler = { (image, error) in
completionHandler(image, error)
}
return imageOperation
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard let image = image, var config = cell.contentConfiguration as? UIListContentConfiguration else { return }
config.image = image
cell.contentConfiguration = config
}
dataSource.searchController.searchableKeyPaths = [#keyPath(Game.name), #keyPath(Game.identifier)]
return dataSource
}
}
extension GamePickerViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let game = self.dataSource.item(at: indexPath)
self.gameHandler?(game)
self.navigationController?.delegate = nil // Prevent calling navigationController(_:willShow:)
self.navigationController?.popViewController(animated: true)
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
guard let section = self.dataSource.fetchedResultsController.sections?[section], !section.name.isEmpty else {
return NSLocalizedString("Unknown System", comment: "")
}
return section.name
}
}
extension GamePickerViewController: UINavigationControllerDelegate
{
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)
{
guard viewController != self else { return }
self.gameHandler?(nil)
}
}

View File

@ -0,0 +1,479 @@
//
// RepairDatabaseViewController.swift
// Delta
//
// Created by Riley Testut on 8/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import OSLog
import DeltaCore
import Roxas
import Harmony
private extension String
{
func sanitizedFilePath() -> String
{
let sanitizedFilePath = self.components(separatedBy: .urlFilenameAllowed.inverted).joined()
return sanitizedFilePath
}
}
class RepairDatabaseViewController: UIViewController
{
var completionHandler: (() -> Void)?
private var _viewDidAppear = false
private lazy var managedObjectContext = DatabaseManager.shared.newBackgroundSavingViewContext()
private lazy var gameSavesContext = DatabaseManager.shared.newBackgroundContext(withParent: self.managedObjectContext)
private var gamesByID: [String: Game]?
private lazy var backupsDirectory = FileManager.default.documentsDirectory.appendingPathComponent("Backups")
private lazy var gameSavesDirectory = DatabaseManager.gamesDirectoryURL
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = .systemBackground
self.isModalInPresentation = true
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.text = NSLocalizedString("Verifying Database…", comment: "")
placeholderView.detailTextLabel.text = nil
placeholderView.activityIndicatorView.startAnimating()
placeholderView.stackView.spacing = 15
self.view.addSubview(placeholderView, pinningEdgesWith: .zero)
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
if !_viewDidAppear
{
self.repairDatabase()
}
_viewDidAppear = true
}
}
private extension RepairDatabaseViewController
{
func repairDatabase()
{
Logger.database.info("Begin repairing database...")
self.repairGames { result in
switch result
{
case .failure(let error):
DispatchQueue.main.async {
let alertController = UIAlertController(title: "Unable to Repair Games", error: error)
self.present(alertController, animated: true)
}
case .success:
self.repairGameSaves { result in
DispatchQueue.main.async {
switch result
{
case .failure(let error):
let alertController = UIAlertController(title: "Unable to Repair Save Files", error: error)
self.present(alertController, animated: true)
case .success:
self.showReviewViewController()
}
}
}
}
}
}
func repairGames(completion: @escaping (Result<Void, Error>) -> Void)
{
self.managedObjectContext.perform {
do
{
let fetchRequest = Game.fetchRequest()
fetchRequest.propertiesToFetch = [#keyPath(Game.type)]
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(Game.gameCollection)]
let allGames = try self.managedObjectContext.fetch(fetchRequest)
let affectedGames = allGames.filter { $0.type.rawValue != $0.gameCollection?.identifier }
let gameCollections = try self.managedObjectContext.fetch(GameCollection.fetchRequest())
let gameCollectionsByID = gameCollections.reduce(into: [:]) { $0[$1.identifier] = $1 }
for game in affectedGames
{
let gameCollection = gameCollectionsByID[game.type.rawValue]
game.gameCollection = gameCollection
Logger.database.notice("Re-associating “\(game.name, privacy: .public)” with GameCollection: \(gameCollection?.identifier ?? "nil", privacy: .public)")
}
try self.managedObjectContext.save()
completion(.success)
}
catch
{
completion(.failure(error))
}
}
}
func repairGameSaves(completion: @escaping (Result<Void, Error>) -> Void)
{
self.managedObjectContext.perform {
do
{
// Fetch GameSaves that don't have same identifier as their Game,
// OR GameSaves that have a non-nil SHA1 hash.
//
// This covers GameSaves connected to wrong games and GameSaves with nil Games,
// as well as any GameSaves modified since last beta (which we assume are corrupted).
let fetchRequest = GameSave.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "(%K == nil) OR (%K != %K) OR (%K != nil)",
#keyPath(GameSave.game),
#keyPath(GameSave.identifier), #keyPath(GameSave.game.identifier),
#keyPath(GameSave.sha1))
let gameSaves = try self.managedObjectContext.fetch(fetchRequest)
let gameSavesByID = gameSaves.reduce(into: [:]) { $0[$1.identifier] = $1 }
let gamesFetchRequest = Game.fetchRequest()
gamesFetchRequest.predicate = NSPredicate(format: "%K IN %@", #keyPath(Game.identifier), Set(gameSavesByID.keys))
let games = try self.managedObjectContext.fetch(gamesFetchRequest)
self.gamesByID = games.reduce(into: [:]) { $0[$1.identifier] = $1 }
let savesBackupsDirectory = self.backupsDirectory.appendingPathComponent("Saves")
try FileManager.default.createDirectory(at: savesBackupsDirectory, withIntermediateDirectories: true)
var conflictedGames = Set<Game>()
for gameSave in gameSaves
{
let expectedGame = self.repair(gameSave, backupsDirectory: savesBackupsDirectory)
// At this point, gameSave is only updated in gameSavesContext,
// so gameSave here still points to previous game,
if let game = gameSave.game
{
Logger.database.notice("The save file for “\(game.name, privacy: .public)” is potentially corrupted, writing to conflicts.txt")
conflictedGames.insert(game)
}
if let expectedGame
{
Logger.database.notice("The save file for “\(expectedGame.name, privacy: .public)” is potentially corrupted, writing to conflicts.txt")
conflictedGames.insert(expectedGame)
}
}
try self.gameSavesContext.performAndWait {
try self.gameSavesContext.save()
}
try self.managedObjectContext.save()
let outputURL = self.backupsDirectory.appendingPathComponent("conflicts.txt")
let conflictsLog = conflictedGames.map { $0.name + " (" + $0.identifier + ")" }.sorted().joined(separator: "\n")
try conflictsLog.write(to: outputURL, atomically: true, encoding: .utf8)
completion(.success)
}
catch
{
completion(.failure(error))
}
}
}
// Returns expectedGame, but in managedObjectContext (not gameSavesContext)
func repair(_ gameSave: GameSave, backupsDirectory: URL) -> Game?
{
Logger.database.notice("Repairing GameSave \(gameSave.identifier, privacy: .public)...")
guard let expectedGame = self.gamesByID?[gameSave.identifier] else {
// Game doesn't exist, so we'll back up save file and delete record.
Logger.database.warning("Orphaning GameSave \(gameSave.identifier, privacy: .public) due to no matching game.")
do
{
try self.backup(gameSave, for: nil, to: backupsDirectory)
}
catch
{
Logger.database.error("Failed to back up save file for orphaned GameSave \(gameSave.identifier, privacy: .public). \(error, privacy: .public)")
}
self.gameSavesContext.performAndWait {
let gameSave = self.gameSavesContext.object(with: gameSave.objectID) as! GameSave
gameSave.game = nil
}
return nil
}
let misplacedGameSave: GameSave?
if let otherGameSave = expectedGame.gameSave, otherGameSave != gameSave
{
misplacedGameSave = otherGameSave
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public) will misplace \(otherGameSave.identifier, privacy: .public)")
}
else
{
misplacedGameSave = nil
}
do
{
// Back up the save file gameSave (incorrectly) refers to, but name it after the _expected_ game.
try self.backup(gameSave, for: expectedGame, to: backupsDirectory)
}
catch
{
Logger.database.error("Failed to back up save file for GameSave \(gameSave.identifier, privacy: .public). Expected Game: \(expectedGame.identifier). \(error, privacy: .public)")
}
// Ignore error if we can't hash file, not that big a deal.
let hash = try? RSTHasher.sha1HashOfFile(at: expectedGame.gameSaveURL)
// Make changes on separate context so we don't change any relationships until we're finished.
// This allows us to refer to previous relationships.
self.gameSavesContext.performAndWait {
let gameSave = self.gameSavesContext.object(with: gameSave.objectID) as! GameSave
let expectedGame = self.gameSavesContext.object(with: expectedGame.objectID) as! Game
let misplacedGameSave: GameSave? = misplacedGameSave.map { self.gameSavesContext.object(with: $0.objectID) as! GameSave }
if hash == gameSave.sha1
{
// .sav has same hash as GameSave SHA1,
// so we can relink without changes.
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash matches .sav, relinking without changes.")
}
else if let misplacedGameSave
{
// GameSave data differs from actual .sav file,
// so copy metadata from misplacedGameSave.
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash does NOT match .sav, ignoring misplaced GameSave \(misplacedGameSave.identifier, privacy: .public).")
// Not worth potential conflicts.
// gameSave.sha1 = misplacedGameSave.sha1
// gameSave.modifiedDate = misplacedGameSave.modifiedDate
}
else
{
// GameSave data differs from actual .sav file,
// so copy metadata from disk.
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash does NOT match .sav, ignoring.")
// Not worth potential conflicts.
// let modifiedDate = try? FileManager.default.attributesOfItem(atPath: expectedGame.gameSaveURL.path)[.modificationDate] as? Date
// gameSave.sha1 = hash
// gameSave.modifiedDate = modifiedDate ?? Date()
}
gameSave.game = expectedGame
}
return expectedGame
}
func backup(_ gameSave: GameSave, for expectedGame: Game?, to backupsDirectory: URL) throws
{
Logger.database.notice("Backing up GameSave \(gameSave.identifier, privacy: .public). Expected Game: \(expectedGame?.name ?? "nil", privacy: .public)")
if let game = gameSave.game
{
// GameSave is linked with incorrect game.
// Prefer using expectedGame's saveFileExtension over game's.
let saveFileExtension: String
if let deltaCore = Delta.core(for: expectedGame?.type ?? game.type)
{
saveFileExtension = deltaCore.gameSaveFileExtension
}
else
{
saveFileExtension = "sav"
}
// 1. Backup existing file at `game`'s expected save file location
if FileManager.default.fileExists(atPath: game.gameSaveURL.path)
{
// Filename = expectedGame.name? + game.identifier
let filename: String
if let expectedGame
{
filename = expectedGame.name + "_" + game.identifier
}
else
{
filename = game.identifier
}
let sanitizedFilename = filename.sanitizedFilePath()
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileExtension)
try FileManager.default.copyItem(at: game.gameSaveURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up save file \(game.gameSaveURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
let rtcFileURL = game.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
if FileManager.default.fileExists(atPath: rtcFileURL.path)
{
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension("rtc")
try FileManager.default.copyItem(at: rtcFileURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up RTC save file \(rtcFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
}
}
// 2. Backup existing file at `expectedGame`'s save file location
if let expectedGame, FileManager.default.fileExists(atPath: expectedGame.gameSaveURL.path)
{
// Filename = expectedGame.name + (misplacedGameSave.identifier ?? expectedGame.identifier)
let filename = expectedGame.name + "_" + (expectedGame.gameSave?.identifier ?? expectedGame.identifier)
let sanitizedFilename = filename.sanitizedFilePath()
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileExtension)
try FileManager.default.copyItem(at: expectedGame.gameSaveURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up expected save file \(expectedGame.gameSaveURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
let rtcFileURL = expectedGame.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
if FileManager.default.fileExists(atPath: rtcFileURL.path)
{
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension("rtc")
try FileManager.default.copyItem(at: rtcFileURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up expected RTC save file \(rtcFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
}
}
}
else
{
@discardableResult
func backUp(_ saveFileURL: URL) throws -> Bool
{
guard FileManager.default.fileExists(atPath: saveFileURL.path) else { return false }
// Filename = expectedGame.name? + gameSave.identifier
let filename: String
if let expectedGame
{
filename = expectedGame.name + "_" + gameSave.identifier
}
else
{
filename = gameSave.identifier
}
let sanitizedFilename = filename.sanitizedFilePath()
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileURL.pathExtension)
try FileManager.default.copyItem(at: saveFileURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up discovered save file \(saveFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
return true
}
// GameSave is _not_ linked to a Game, so instead we iterate through all save files on disk to find match.
let savURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("sav")
let srmURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("srm")
let dsvURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("dsv")
let saveFileURLs = [savURL, srmURL, dsvURL]
for saveFileURL in saveFileURLs
{
if try backUp(saveFileURL)
{
break
}
}
// ALWAYS attempt to back up RTC file.
let rtcURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("rtc")
try backUp(rtcURL)
}
}
func showReviewViewController()
{
Logger.database.info("Finished repairing Games and GameSaves, reviewing recent SaveStates...")
let viewController = ReviewSaveStatesViewController()
viewController.filter = .sinceLastBeta
viewController.completionHandler = { [weak self] in
self?.finish()
}
self.navigationController?.pushViewController(viewController, animated: true)
}
func finish()
{
Logger.database.info("Finished repairing database!")
DispatchQueue.global(qos: .userInitiated).async {
if #available(iOS 15, *)
{
do
{
let store = try OSLogStore(scope: .currentProcessIdentifier)
// All logs since the app launched.
let position = store.position(timeIntervalSinceLatestBoot: 0)
let entries = try store.getEntries(at: position)
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == Logger.deltaSubsystem || $0.subsystem == Logger.harmonySubsystem }
.map { "[\($0.date.formatted())] [\($0.level.localizedName)] \($0.composedMessage)" }
let outputURL = self.backupsDirectory.appendingPathComponent("repair.log")
try FileManager.default.createDirectory(at: self.backupsDirectory, withIntermediateDirectories: true)
let outputText = entries.joined(separator: "\n")
try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
}
catch
{
print("Failed to export Harmony logs.", error)
}
}
DispatchQueue.main.async {
let alertController = UIAlertController(title: NSLocalizedString("Database Repaired", comment: ""),
message: NSLocalizedString("Some save files may still be corrupted and require you to restore an older version from the Delta Sync settings.\n\nA text file listing all affected games has been saved to “On My Device/Delta/Backups/conflicts.txt” in the Files app, alongside backups of any conflicted save files.", comment: ""),
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.ok.title, style: UIAlertAction.ok.style) { _ in
self.completionHandler?()
})
self.present(alertController, animated: true)
}
}
}
}

View File

@ -0,0 +1,377 @@
//
// ReviewSaveStatesViewController.swift
// Delta
//
// Created by Riley Testut on 8/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import OSLog
import Harmony
import Roxas
extension ReviewSaveStatesViewController
{
enum Filter
{
case recent
case all
case sinceLastBeta
}
}
extension RecordFlags
{
static let isGameRelationshipVerified = RecordFlags(rawValue: 1 << 0)
}
class ReviewSaveStatesViewController: UITableViewController
{
var filter: Filter = .recent {
didSet {
self.updateDataSource()
}
}
var completionHandler: (() -> Void)?
private lazy var managedObjectContext = DatabaseManager.shared.newBackgroundSavingViewContext()
private lazy var dataSource = self.makeDataSource()
private lazy var descriptionDataSource = self.makeDescriptionDataSource()
private lazy var saveStatesDataSource = self.makeSaveStatesDataSource()
private weak var _parentNavigationController: UINavigationController?
init()
{
super.init(style: .insetGrouped)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.dataSource.proxy = self
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(ReviewSaveStatesViewController.finish))
self.navigationItem.rightBarButtonItem = doneButton
self.navigationItem.title = NSLocalizedString("Review Save States", comment: "")
// Disable going back to RepairDatabaseViewController.
self.navigationItem.setHidesBackButton(true, animated: false)
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if let parent = self.parent, parent.navigationItem.title == nil
{
// Must change parent's navigationItem when we're contained in SwiftUI View.
parent.navigationItem.title = NSLocalizedString("Review Save States", comment: "")
parent.navigationItem.rightBarButtonItem = self.makeFilterButton()
}
}
override func viewWillDisappear(_ animated: Bool)
{
super.viewWillDisappear(animated)
_parentNavigationController = self.parent?.navigationController
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
switch self.filter
{
case .all, .recent:
if self.parent == nil || self.parent?.parent == nil
{
// Only finish if we're popped off navigation controller.
self.finish()
}
case .sinceLastBeta: break
}
}
}
private extension ReviewSaveStatesViewController
{
func makeDataSource() -> RSTCompositeTableViewPrefetchingDataSource<SaveState, UIImage>
{
let dataSource = RSTCompositeTableViewPrefetchingDataSource<SaveState, UIImage>(dataSources: [self.descriptionDataSource, self.saveStatesDataSource])
return dataSource
}
func makeDescriptionDataSource() -> RSTDynamicTableViewPrefetchingDataSource<SaveState, UIImage>
{
let dataSource = RSTDynamicTableViewPrefetchingDataSource<SaveState, UIImage>()
dataSource.numberOfSectionsHandler = { 1 }
dataSource.numberOfItemsHandler = { _ in 0 }
return dataSource
}
func makeSaveStatesDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource<SaveState, UIImage>
{
let fetchedResultsController = self.makeSaveStatesFetchedResultsController()
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<SaveState, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.cellConfigurationHandler = { (cell, saveState, indexPath) in
var configuration = UIListContentConfiguration.valueCell()
configuration.prefersSideBySideTextAndSecondaryText = false
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold) ?? .preferredFontDescriptor(withTextStyle: .body)
configuration.text = saveState.name ?? NSLocalizedString("Untitled", comment: "")
configuration.textProperties.font = UIFont(descriptor: fontDescriptor, size: 0)
configuration.secondaryText = SaveState.localizedDateFormatter.string(from: saveState.modifiedDate)
configuration.secondaryTextProperties.font = .preferredFont(forTextStyle: .caption1)
configuration.image = nil
configuration.imageProperties.maximumSize = CGSize(width: 80, height: 80)
configuration.imageProperties.reservedLayoutSize = CGSize(width: 80, height: 80)
configuration.imageProperties.cornerRadius = 6
cell.contentConfiguration = configuration
cell.accessoryType = .disclosureIndicator
}
dataSource.prefetchHandler = { (saveState, indexPath, completionHandler) in
guard saveState.game != nil else {
completionHandler(nil, nil)
return nil
}
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
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard let image = image, var config = cell.contentConfiguration as? UIListContentConfiguration else { return }
config.image = image
cell.contentConfiguration = config
}
return dataSource
}
func makeSaveStatesFetchedResultsController() -> NSFetchedResultsController<SaveState>
{
let fetchRequest = SaveState.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \SaveState.game?.name, ascending: true), NSSortDescriptor(keyPath: \SaveState.modifiedDate, ascending: false)]
let predicate = NSPredicate(format: "%K != %@", #keyPath(SaveState.type), SaveStateType.auto.rawValue as NSNumber)
switch self.filter
{
case .recent:
let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date().addingTimeInterval(-1 * 60 * 60 * 24 * 30)
let recentPredicate = NSPredicate(format: "%K > %@", #keyPath(SaveState.modifiedDate), oneMonthAgo as NSDate)
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, recentPredicate])
case .all:
fetchRequest.predicate = predicate
case .sinceLastBeta:
let dateComponents = DateComponents(year: 2023, month: 7, day: 18, hour: 0, minute: 0, second: 0)
let lastBetaDate = Calendar.current.date(from: dateComponents) ?? Date().addingTimeInterval(-1 * 60 * 60 * 24 * 45)
let sinceLastBetaPredicate = NSPredicate(format: "%K > %@", #keyPath(SaveState.modifiedDate), lastBetaDate as NSDate)
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, sinceLastBetaPredicate])
}
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: #keyPath(SaveState.game.name), cacheName: nil)
return fetchedResultsController
}
func updateDataSource()
{
let fetchedResultsController = self.makeSaveStatesFetchedResultsController()
self.saveStatesDataSource.fetchedResultsController = fetchedResultsController
}
func makeFilterButton() -> UIBarButtonItem
{
let recentAction = UIAction(title: NSLocalizedString("Past Month", comment: ""), image: UIImage(systemName: "calendar")) { [weak self] _ in
self?.filter = .recent
}
let allAction = UIAction(title: NSLocalizedString("All Time", comment: ""), image: UIImage(systemName: "clock")) { [weak self] _ in
self?.filter = .all
}
var options: UIMenu.Options = []
if #available(iOS 15, *)
{
options = .singleSelection
recentAction.state = self.filter == .recent ? .on : .off
allAction.state = self.filter == .all ? .on : .off
}
let filterMenu = UIMenu(options: options, children: [recentAction, allAction])
let filterButton = UIBarButtonItem(title: NSLocalizedString("Filter", comment: ""), image: UIImage(systemName: "calendar.badge.clock"), menu: filterMenu)
return filterButton
}
}
private extension ReviewSaveStatesViewController
{
func pickGame(for saveState: SaveState)
{
let gamePickerViewController = GamePickerViewController()
gamePickerViewController.gameHandler = { game in
guard let game else { return }
let previousGame = saveState.game
if previousGame != nil
{
// Move files to new location.
let destinationDirectory = DatabaseManager.saveStatesDirectoryURL(for: game)
for fileURL in [saveState.fileURL, saveState.imageFileURL]
{
guard FileManager.default.fileExists(atPath: fileURL.path) else { continue }
let destinationURL = destinationDirectory.appendingPathComponent(fileURL.lastPathComponent)
do
{
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true) // Copy, don't move, in case app quits before user confirms.
}
catch
{
Logger.database.error("Failed to copy SaveState “\(saveState.localizedName, privacy: .public)” from \(fileURL, privacy: .public) to \(destinationURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
}
}
let tempGame = self.managedObjectContext.object(with: game.objectID) as! Game
saveState.game = tempGame
Logger.database.notice("Re-associated SaveState “\(saveState.localizedName, privacy: .public)” with game “\(tempGame.name, privacy: .public)”. Previously: \(previousGame?.name ?? "nil", privacy: .public)")
}
self.navigationController?.pushViewController(gamePickerViewController, animated: true)
}
@objc func finish()
{
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = true
self.managedObjectContext.perform {
do
{
let saveStates: [SaveState]?
switch self.filter
{
case .recent, .all:
// Only upload metadata for changed SaveStates.
saveStates = self.managedObjectContext.updatedObjects.compactMap { $0 as? SaveState }
case .sinceLastBeta:
// Upload metadata for _all_ SaveStates.
saveStates = self.saveStatesDataSource.fetchedResultsController.fetchedObjects
}
try self.managedObjectContext.save()
if let saveStates = saveStates, let coordinator = SyncManager.shared.coordinator
{
let records = try coordinator.recordController.fetchRecords(for: saveStates)
if let context = records.first?.recordedObject?.managedObjectContext
{
try context.performAndWait {
for record in records
{
record.perform { managedRecord in
managedRecord.flags.insert(.isGameRelationshipVerified)
managedRecord.setNeedsMetadataUpdate()
let saveState = record.recordedObject
Logger.database.notice("Flagged SaveState “\(saveState?.localizedName ?? record.recordID.identifier, privacy: .public)” for metadata update.")
}
}
try context.save()
}
}
}
DispatchQueue.main.async {
self.completionHandler?()
}
}
catch
{
DispatchQueue.main.async {
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
let alertController = UIAlertController(title: NSLocalizedString("Unable to Save Changes", comment: ""), error: error)
(self._parentNavigationController ?? self).present(alertController, animated: true)
}
}
}
}
}
extension ReviewSaveStatesViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let saveState = self.dataSource.item(at: indexPath)
self.pickGame(for: saveState)
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
if section == 0
{
return nil
}
else
{
let section = section - 1
guard let gameName = self.saveStatesDataSource.fetchedResultsController.sections?[section].name else { return NSLocalizedString("Unknown Game", comment: "") }
return gameName
}
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
guard section == 0 else { return nil }
return NSLocalizedString("These save states have been modified recently and may be associated with the wrong game.\n\nPlease change any incorrectly associated save states to the correct game by tapping them.", comment: "")
}
}

View File

@ -0,0 +1,67 @@
//
// CopyDeepLinkActivity.swift
// Delta
//
// Created by Riley Testut on 8/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
extension UIActivity.ActivityType
{
static let copyDeepLink = UIActivity.ActivityType("com.rileytestut.Delta.CopyDeepLink")
}
class CopyDeepLinkActivity: UIActivity
{
private var deepLink: URL?
override class var activityCategory: UIActivity.Category {
return .action
}
override var activityType: UIActivity.ActivityType? {
return .copyDeepLink
}
override var activityTitle: String? {
return NSLocalizedString("Copy Deep Link", comment: "")
}
override var activityImage: UIImage? {
return UIImage(symbolNameIfAvailable: "link") ?? UIImage(named: "Link")
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool
{
if activityItems.contains(where: { $0 is Game })
{
return true
}
else
{
return false
}
}
override func prepare(withActivityItems activityItems: [Any])
{
guard let game = activityItems.first(where: { $0 is Game }) as? Game else { return }
self.deepLink = URL(action: .launchGame(identifier: game.identifier))
}
override func perform()
{
if let deepLink = self.deepLink
{
UIPasteboard.general.url = deepLink
self.activityDidFinish(true)
}
else
{
self.activityDidFinish(false)
}
}
}

View File

@ -0,0 +1,99 @@
//
// DeepLink.swift
// Delta
//
// Created by Riley Testut on 12/29/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
extension URL
{
init(action: DeepLink.Action)
{
switch action
{
case .launchGame(let identifier):
let deepLinkURL = URL(string: "delta://\(action.type.rawValue)/\(identifier)")!
self = deepLinkURL
}
}
}
extension UIApplicationShortcutItem
{
convenience init(localizedTitle: String, action: DeepLink.Action)
{
var userInfo: [String: NSSecureCoding]?
switch action
{
case .launchGame(let identifier): userInfo = [DeepLink.Key.identifier.rawValue: identifier as NSString]
}
self.init(type: action.type.rawValue, localizedTitle: localizedTitle, localizedSubtitle: nil, icon: nil, userInfo: userInfo)
}
}
extension DeepLink
{
enum Action
{
case launchGame(identifier: String)
var type: ActionType {
switch self
{
case .launchGame: return .launchGame
}
}
}
enum ActionType: String
{
case launchGame = "game"
}
enum Key: String
{
case identifier
case game
}
}
enum DeepLink
{
case url(URL)
case shortcut(UIApplicationShortcutItem)
var actionType: ActionType? {
switch self
{
case .url(let url):
guard let host = url.host else { return nil }
let type = ActionType(rawValue: host)
return type
case .shortcut(let shortcut):
let type = ActionType(rawValue: shortcut.type)
return type
}
}
var action: Action? {
guard let type = self.actionType else { return nil }
switch (self, type)
{
case (.url(let url), .launchGame):
let identifier = url.lastPathComponent
return .launchGame(identifier: identifier)
case (.shortcut(let shortcut), .launchGame):
guard let identifier = shortcut.userInfo?[Key.identifier.rawValue] as? String else { return nil }
return .launchGame(identifier: identifier)
}
}
}

View File

@ -0,0 +1,91 @@
//
// DeepLinkController.swift
// Delta
//
// Created by Riley Testut on 12/28/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
extension Notification.Name
{
static let deepLinkControllerLaunchGame = Notification.Name("deepLinkControllerLaunchGame")
}
extension UIViewController
{
var allowsDeepLinkingDismissal: Bool {
return true
}
}
struct DeepLinkController
{
private var window: UIWindow? {
if #available(iOS 13, *)
{
guard let delegate = UIApplication.shared.connectedScenes.lazy.compactMap({ $0.delegate as? UIWindowSceneDelegate }).first, let window = delegate.window else { return nil }
return window
}
else
{
guard let delegate = UIApplication.shared.delegate, let window = delegate.window else { return nil }
return window
}
}
private var topViewController: UIViewController? {
guard let window = self.window else { return nil }
var topViewController = window.rootViewController
while topViewController?.presentedViewController != nil
{
guard !(topViewController?.presentedViewController is UIAlertController) else { break }
topViewController = topViewController?.presentedViewController
}
return topViewController
}
}
extension DeepLinkController
{
@discardableResult func handle(_ deepLink: DeepLink) -> Bool
{
guard let action = deepLink.action else { return false }
switch action
{
case .launchGame(let identifier): return self.launchGame(withIdentifier: identifier)
}
}
}
private extension DeepLinkController
{
func launchGame(withIdentifier identifier: String) -> Bool
{
guard let topViewController = self.topViewController, topViewController.allowsDeepLinkingDismissal else { return false }
let fetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), identifier)
fetchRequest.returnsObjectsAsFaults = false
do
{
guard let game = try DatabaseManager.shared.viewContext.fetch(fetchRequest).first else { return false }
NotificationCenter.default.post(name: .deepLinkControllerLaunchGame, object: self, userInfo: [DeepLink.Key.game: game])
}
catch
{
print(error)
return false
}
return true
}
}

View File

@ -0,0 +1,29 @@
//
// 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
case toggleFastForward
}
extension ActionInput: Input
{
var type: InputType {
return .controller(.action)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -15,19 +15,31 @@ private var kvoContext = 0
class PreviewGameViewController: DeltaCore.GameViewController class PreviewGameViewController: DeltaCore.GameViewController
{ {
// If non-nil, will override the default preview action items returned in previewActionItems() // If non-nil, will override the default preview action items returned in previewActionItems()
// nilpreviewActionItems()
var overridePreviewActionItems: [UIPreviewActionItem]? var overridePreviewActionItems: [UIPreviewActionItem]?
// Save state to be loaded upon preview // Save state to be loaded upon preview
//
var previewSaveState: SaveStateProtocol? var previewSaveState: SaveStateProtocol?
// Initial image to be shown while loading // Initial image to be shown while loading
//
var previewImage: UIImage? { var previewImage: UIImage? {
didSet { didSet {
self.updatePreviewImage() self.updatePreviewImage()
} }
} }
fileprivate var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated) var isLivePreview: Bool = true
private var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
private var copiedSaveFiles = [(originalURL: URL, copyURL: URL)]()
private lazy var temporaryDirectoryURL: URL = {
let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent("preview-" + UUID().uuidString)
try? FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
return directoryURL
}()
override var game: GameProtocol? { override var game: GameProtocol? {
willSet { willSet {
@ -41,7 +53,8 @@ class PreviewGameViewController: DeltaCore.GameViewController
emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext) emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext)
self.preferredContentSize = emulatorCore.preferredRenderingSize let size = CGSize(width: emulatorCore.preferredRenderingSize.width * 2.0, height: emulatorCore.preferredRenderingSize.height * 2.0)
self.preferredContentSize = size
} }
} }
@ -50,8 +63,28 @@ class PreviewGameViewController: DeltaCore.GameViewController
return previewActionItems return previewActionItems
} }
public required init()
{
super.init()
self.delegate = self
}
public required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.delegate = self
}
deinit deinit
{ {
// Explicitly stop emulatorCore _before_ we remove ourselves as observer
// so we can wait until stopped before restoring save files (again).
// emulatorCore
//
self.emulatorCore?.stop()
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext) self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
} }
} }
@ -65,17 +98,26 @@ extension PreviewGameViewController
super.viewDidLoad() super.viewDidLoad()
self.controllerView.isHidden = true self.controllerView.isHidden = true
self.controllerView.controllerSkin = nil // Skip loading controller skin from disk, which may be slow.
// Temporarily prevent emulatorCore from updating gameView to prevent flicker of black, or other visual glitches // Temporarily prevent emulatorCore from updating gameView to prevent flicker of black, or other visual glitches
// emulatorCore gameView
self.emulatorCore?.remove(self.gameView) self.emulatorCore?.remove(self.gameView)
} }
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.copySaveFiles()
}
override func viewDidAppear(_ animated: Bool) override func viewDidAppear(_ animated: Bool)
{ {
super.viewDidAppear(animated) super.viewDidAppear(animated)
self.emulatorCoreQueue.async { self.emulatorCoreQueue.async {
self.emulatorCore?.resume() self.startEmulation()
} }
} }
@ -84,14 +126,33 @@ extension PreviewGameViewController
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
// Pause in viewWillDisappear and not viewDidDisappear like DeltaCore.GameViewController so the audio cuts off earlier if being dismissed // Pause in viewWillDisappear and not viewDidDisappear like DeltaCore.GameViewController so the audio cuts off earlier if being dismissed
// viewWillDisappear DeltaCore.GameViewController viewDidDisappear
self.emulatorCore?.pause() self.emulatorCore?.pause()
} }
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
// Already stopped = we've already restored save files and removed directory.
// =
if self.emulatorCore?.state != .stopped
{
// Pre-emptively restore save files in case something goes wrong while stopping emulation.
// This also ensures if the core is never stopped (for some reason), saves are still restored.
// 仿
//
self.restoreSaveFiles(removeCopyDirectory: false)
}
}
override func viewDidLayoutSubviews() override func viewDidLayoutSubviews()
{ {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
// Need to update in viewDidLayoutSubviews() to ensure bounds of gameView are not CGRect.zero // Need to update in viewDidLayoutSubviews() to ensure bounds of gameView are not CGRect.zero
// viewDidLayoutSubviews() gameView CGRect.zero
self.updatePreviewImage() self.updatePreviewImage()
} }
@ -99,6 +160,7 @@ extension PreviewGameViewController
{ {
super.didReceiveMemoryWarning() super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated. // Dispose of any resources that can be recreated.
//
} }
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
@ -111,17 +173,24 @@ extension PreviewGameViewController
let state = self.emulatorCore?.state let state = self.emulatorCore?.state
else { return } else { return }
if previousState == .stopped, state == .running switch state
{
self.emulatorCoreQueue.sync {
if self.isAppearing
{ {
case .running where previousState == .stopped:
self.emulatorCoreQueue.async {
// Pause to prevent it from starting before visible (in case user peeked slowly) // Pause to prevent it from starting before visible (in case user peeked slowly)
//
self.emulatorCore?.pause() self.emulatorCore?.pause()
}
self.preparePreview() self.preparePreview()
} }
case .stopped:
// Emulation has stopped, so we can safely restore save files,
// and also remove the directory they were copied to.
//
//
self.restoreSaveFiles(removeCopyDirectory: true)
default: break
} }
} }
} }
@ -171,6 +240,69 @@ private extension PreviewGameViewController
self.emulatorCore?.updateCheats() self.emulatorCore?.updateCheats()
// Re-enable emulatorCore to update gameView again // Re-enable emulatorCore to update gameView again
// emulatorCoregameView
self.emulatorCore?.add(self.gameView) self.emulatorCore?.add(self.gameView)
self.emulatorCore?.resume()
}
func copySaveFiles()
{
guard let game = self.game as? Game, let gameSave = game.gameSave else { return }
self.copiedSaveFiles.removeAll()
let fileURLs = gameSave.syncableFiles.lazy.map { $0.fileURL }
for fileURL in fileURLs
{
do
{
let destinationURL = self.temporaryDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
self.copiedSaveFiles.append((fileURL, destinationURL))
print("Copied save file:", fileURL.lastPathComponent)
}
catch
{
print("Failed to back up save file \(fileURL.lastPathComponent).", error)
}
}
}
func restoreSaveFiles(removeCopyDirectory: Bool)
{
for (originalURL, copyURL) in self.copiedSaveFiles
{
do
{
try FileManager.default.copyItem(at: copyURL, to: originalURL, shouldReplace: true)
print("Restored save file:", originalURL.lastPathComponent)
}
catch
{
print("Failed to restore copied save file \(copyURL.lastPathComponent).", error)
}
}
if removeCopyDirectory
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectoryURL)
}
catch
{
print("Failed to remove preview temporary directory.", error)
}
}
}
}
extension PreviewGameViewController: GameViewControllerDelegate
{
func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool
{
return self.isLivePreview
} }
} }

View File

@ -0,0 +1,53 @@
//
// ExperimentalFeatures.swift
// Delta
//
// Created by Riley Testut on 4/6/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import DeltaFeatures
struct ExperimentalFeatures: FeatureContainer
{
static let shared = ExperimentalFeatures()
@Feature(name: "AirPlay Skins",
description: "Customize the appearance of games when AirPlaying to your TV.",
options: AirPlaySkinsOptions())
var airPlaySkins
@Feature(name: "Variable Fast Forward",
description: "Change the preferred Fast Foward speed per-system. You can also change it by long-pressing the Fast Forward button from the Pause Menu.",
options: VariableFastForwardOptions())
var variableFastForward
@Feature(name: "Show Status Bar",
description: "Enable to show the Status Bar during gameplay.")
var showStatusBar
@Feature(name: "Game Screenshots",
description: "When enabled, a Screenshot button will appear in the Pause Menu, allowing you to save a screenshot of your game. You can choose to save the screenshot to Photos or Files.",
options: GameScreenshotsOptions())
var gameScreenshots
@Feature(name: "Toast Notifications",
description: "Show toast notifications as a confirmation for various actions, such as saving your game or loading a save state.",
options: ToastNotificationOptions())
var toastNotifications
@Feature(name: "Review Save States",
description: "Review recent Save States to make sure they are associated with the correct game.",
options: ReviewSaveStatesOptions())
var reviewSaveStates
@Feature(name: "Alternate App Icon",
description: "Change the app icon.",
options: AlternateAppIconOptions())
var alternateAppIcons
private init()
{
self.prepareFeatures()
}
}

View File

@ -0,0 +1,167 @@
//
// AirPlaySkins.swift
// Delta
//
// Created by Riley Testut on 4/20/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaFeatures
import DeltaCore
extension Feature where Options == AirPlaySkinsOptions
{
func preferredAirPlayControllerSkin(for gameType: GameType) -> ControllerSkin?
{
guard let identifier = self[gameType] else { return nil }
let predicate = NSPredicate(format: "%K == %@", #keyPath(ControllerSkin.identifier), identifier)
let controllerSkin = ControllerSkin.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: ControllerSkin.self).first
return controllerSkin
}
}
struct AirPlaySkinsOptions
{
@Option(name: "Manage Skins", detailView: { _ in SkinManager() })
private var skinManager: String = "" // Hack until I figure out how to support Void properties...
@Option(name: LocalizedStringKey(System.nes.localizedName), description: "The controller skin used when AirPlaying NES games.", detailView: { SkinPicker(gameType: .nes, controllerSkinID: $0) })
var nes: String?
@Option(name: LocalizedStringKey(System.snes.localizedName), description: "The controller skin used when AirPlaying SNES games.", detailView: { SkinPicker(gameType: .snes, controllerSkinID: $0) })
var snes: String?
@Option(name: LocalizedStringKey(System.genesis.localizedName), description: "The controller skin used when AirPlaying Genesis games.", detailView: { SkinPicker(gameType: .genesis, controllerSkinID: $0) })
var genesis: String?
@Option(name: LocalizedStringKey(System.n64.localizedName), description: "The controller skin used when AirPlaying N64 games.", detailView: { SkinPicker(gameType: .n64, controllerSkinID: $0) })
var n64: String?
@Option(name: LocalizedStringKey(System.gbc.localizedName), description: "The controller skin used when AirPlaying GBC games.", detailView: { SkinPicker(gameType: .gbc, controllerSkinID: $0) })
var gbc: String?
@Option(name: LocalizedStringKey(System.gba.localizedName), description: "The controller skin used when AirPlaying GBA games.", detailView: { SkinPicker(gameType: .gba, controllerSkinID: $0) })
var gba: String?
@Option(name: LocalizedStringKey(System.ds.localizedName), description: "The controller skin used when AirPlaying DS games.", detailView: { SkinPicker(gameType: .ds, controllerSkinID: $0) })
var ds: String?
subscript(gameType: GameType) -> String? {
guard let system = System(gameType: gameType) else { return nil }
switch system
{
case .nes: return self.nes
case .snes: return self.snes
case .genesis: return self.genesis
case .n64: return self.n64
case .gbc: return self.gbc
case .gba: return self.gba
case .ds: return self.ds
}
}
}
fileprivate extension AirPlaySkinsOptions
{
struct SkinPicker: View
{
let gameType: GameType
@Binding
var controllerSkinID: String?
@FetchRequest
private var controllerSkins: FetchedResults<ControllerSkin>
@Environment(\.featureOption)
private var option
var body: some View {
Picker(option.name ?? "", selection: $controllerSkinID) {
ForEach(controllerSkins, id: \.identifier) { controllerSkin in
Text(controllerSkin.name)
.tag(Optional<String>(controllerSkin.identifier)) // Must be Optional<String> in order for selection to work.
// .tag(controllerSkin.identifier)
}
Text("None")
.tag(String?.none)
}
.pickerStyle(.menu)
.displayInline()
}
init(gameType: GameType, controllerSkinID: Binding<String?>)
{
self.gameType = gameType
self._controllerSkinID = controllerSkinID
let configuration = ControllerSkinConfigurations.tvStandardLandscape
let predicate = NSPredicate(format: "%K == %@ AND (%K & %d) != 0 AND %K == NO",
#keyPath(ControllerSkin.gameType), self.gameType.rawValue,
#keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue,
#keyPath(ControllerSkin.isStandard))
self._controllerSkins = FetchRequest(entity: ControllerSkin.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)], predicate: predicate)
}
}
struct SkinManager: View
{
@FetchRequest(entity: ControllerSkin.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)],
predicate: {
let configuration = ControllerSkinConfigurations.tvStandardLandscape
return NSPredicate(format: "(%K & %d) != 0 AND %K == NO",
#keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue,
#keyPath(ControllerSkin.isStandard))
}())
private var controllerSkins: FetchedResults<ControllerSkin>
var body: some View {
if controllerSkins.isEmpty
{
Text("No AirPlay Skins")
.foregroundColor(.gray)
}
else
{
List {
ForEach(controllerSkins, id: \.identifier) { controllerSkin in
HStack {
Text(controllerSkin.name)
Spacer()
if let system = System(gameType: controllerSkin.gameType)
{
Text(system.localizedShortName)
.foregroundColor(.gray)
}
}
}
.onDelete(perform: deleteAirPlaySkins)
}
}
}
private func deleteAirPlaySkins(at indexes: IndexSet)
{
let objectIDs = indexes.map { controllerSkins[$0].objectID }
DatabaseManager.shared.performBackgroundTask { context in
let controllerSkins = objectIDs.compactMap { context.object(with: $0) as? ControllerSkin }
for controllerSkin in controllerSkins
{
context.delete(controllerSkin)
}
context.saveWithErrorLogging()
}
}
}
}

View File

@ -0,0 +1,124 @@
//
// AlternateAppIcons.swift
// Delta
//
// Created by Chris Rittenhouse on 5/2/23.
// Copyright © 2023 LitRitt. All rights reserved.
//
import SwiftUI
import DeltaFeatures
enum AppIcon: String, CaseIterable, CustomStringConvertible, Identifiable
{
case normal = "Default"
case gba4ios = "GBA4iOS"
case inverted = "Inverted"
case pixelated = "Pixelated"
case skin = "Controller Skin"
var description: String {
return self.rawValue
}
var id: String {
return self.rawValue
}
var author: String {
switch self
{
case .normal: return "Caroline Moore"
case .gba4ios: return "Paul Thorsen"
case .inverted, .skin, .pixelated: return "LitRitt"
}
}
var assetName: String {
switch self
{
case .normal: return "AppIcon"
case .gba4ios: return "IconGBA4iOS"
case .inverted: return "IconInverted"
case .pixelated: return "IconPixelated"
case .skin: return "IconSkin"
}
}
}
extension AppIcon: Equatable
{
static func == (lhs: AppIcon, rhs: AppIcon) -> Bool
{
return lhs.description == rhs.description
}
}
extension AppIcon: LocalizedOptionValue
{
var localizedDescription: Text {
Text(self.description)
}
}
struct AlternateAppIconOptions
{
@Option(name: "Alternate App Icon",
description: "Choose from alternate app icons created by the community.",
detailView: { value in
List {
ForEach(AppIcon.allCases) { icon in
HStack {
if icon == value.wrappedValue
{
Text("")
}
icon.localizedDescription
Text("- by \(icon.author)")
.font(.system(size: 15))
.foregroundColor(.gray)
Spacer()
Image(uiImage: Bundle.appIcon(for: icon) ?? UIImage())
.cornerRadius(13)
}
.contentShape(Rectangle())
.onTapGesture {
value.wrappedValue = icon
}
}
}
.onChange(of: value.wrappedValue) { _ in
updateAppIcon()
}
.displayInline()
})
var icon: AppIcon = .normal
}
extension AlternateAppIconOptions
{
static func updateAppIcon()
{
// Get current icon
let currentIcon = UIApplication.shared.alternateIconName
// Apply chosen icon if feature is enabled
if ExperimentalFeatures.shared.alternateAppIcons.isEnabled
{
let icon = ExperimentalFeatures.shared.alternateAppIcons.icon
// Only apply new icon if it's not already the current icon
switch icon
{
case .normal: if currentIcon != nil { UIApplication.shared.setAlternateIconName(nil) } // Default app icon
default: if currentIcon != icon.assetName { UIApplication.shared.setAlternateIconName(icon.assetName) } // Alternate app icon
}
}
else
{
// Remove alternate icons if feature is disabled
if currentIcon != nil { UIApplication.shared.setAlternateIconName(nil) }
}
}
}

View File

@ -0,0 +1,54 @@
//
// GameScreenshots.swift
// Delta
//
// Created by Chris Rittenhouse on 4/24/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaFeatures
enum ScreenshotSize: Double, CaseIterable, CustomStringConvertible
{
case x5 = 5
case x4 = 4
case x3 = 3
case x2 = 2
var description: String {
if #available(iOS 15, *)
{
let formattedText = self.rawValue.formatted(.number.decimalSeparator(strategy: .automatic))
return "\(formattedText)x Size"
}
else
{
return "\(self.rawValue)x Size"
}
}
}
extension ScreenshotSize: LocalizedOptionValue
{
var localizedDescription: Text {
Text(self.description)
}
static var localizedNilDescription: Text {
Text("Original Size")
}
}
struct GameScreenshotsOptions
{
@Option(name: "Save to Files", description: "Save the screenshot to the app's directory in Files.")
var saveToFiles: Bool = true
@Option(name: "Save to Photos", description: "Save the screenshot to the Photo Library.")
var saveToPhotos: Bool = false
@Option(name: "Image Size", description: "Choose the size of screenshots. This only increases the export size, it does not increase the quality.", values: ScreenshotSize.allCases)
var size: ScreenshotSize?
}

View File

@ -0,0 +1,30 @@
//
// LinkSaveStatesOptions.swift
// Delta
//
// Created by Riley Testut on 8/7/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaFeatures
struct ReviewSaveStatesView: UIViewControllerRepresentable
{
func makeUIViewController(context: Context) -> ReviewSaveStatesViewController
{
let viewController = ReviewSaveStatesViewController()
return viewController
}
func updateUIViewController(_ uiViewController: ReviewSaveStatesViewController, context: Context)
{
}
}
struct ReviewSaveStatesOptions
{
@Option(name: "View Save States", detailView: { _ in ReviewSaveStatesView() })
private var reviewSaveStates: String = "" // Hack until I figure out how to support Void properties...
}

View File

@ -0,0 +1,38 @@
//
// ToastNotificationOptions.swift
// Delta
//
// Created by Chris Rittenhouse on 4/25/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaFeatures
struct ToastNotificationOptions
{
@Option(name: "Duration", description: "Change how long toasts should be shown.", detailView: { duration in
HStack {
Text("Duration: \(duration.wrappedValue, specifier: "%.1f")s")
Slider(value: duration, in: 1...5, step: 0.5).displayInline()
}
})
var duration: Double = 1.5
@Option(name: "Game Data Saved",
description: "Show toasts when performing an in game save.")
var gameSaveEnabled: Bool = true
@Option(name: "Saved Save State",
description: "Show toasts when saving a save state.")
var stateSaveEnabled: Bool = true
@Option(name: "Loaded Save State",
description: "Show toasts when loading a save state.")
var stateLoadEnabled: Bool = true
@Option(name: "Fast Forward Toggled",
description: "Show toasts when toggling fast forward.")
var fastForwardEnabled: Bool = true
}

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