From 1137189b5761f357f3fdc795d0d86799acd8bb0d Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Wed, 26 Apr 2023 16:07:17 -0500 Subject: [PATCH] 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) --- Cores/DeltaCore | 2 +- Delta.xcodeproj/project.pbxproj | 16 ++++ Delta/Base.lproj/Settings.storyboard | 88 +++++++++++++++++-- .../Database/Model/Human/ControllerSkin.swift | 5 ++ Delta/Emulation/GameViewController.swift | 50 ++++++++++- .../MelonDSCoreSettingsViewController.swift | 73 ++++++++++++--- Delta/Settings/Features/DSAirPlay.swift | 23 +++++ Delta/Settings/Features/Features.swift | 25 ++++++ Delta/Settings/Settings.swift | 2 + 9 files changed, 256 insertions(+), 28 deletions(-) create mode 100644 Delta/Settings/Features/DSAirPlay.swift create mode 100644 Delta/Settings/Features/Features.swift diff --git a/Cores/DeltaCore b/Cores/DeltaCore index 4b5084c..3ce43f3 160000 --- a/Cores/DeltaCore +++ b/Cores/DeltaCore @@ -1 +1 @@ -Subproject commit 4b5084c75149dc0fc7b4abb4e6ab4f41df025ecd +Subproject commit 3ce43f3103c637dfdb27f85fc0d0041c8a37292b diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 4f55f29..f7fdabf 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -196,6 +196,8 @@ D5A9C00329DDED6D00A8D610 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.swift */; }; D5A9C01D29DE058C00A8D610 /* VariableFastForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */; }; D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF27629884F8600F21ACF /* CheatDevice.swift */; }; + D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ADD12129F33FBF00CE0560 /* Features.swift */; }; + D5D78AE529F9BC3700E064F0 /* DSAirPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */; }; D5D797E6298D946200738869 /* Contributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D797E5298D946200738869 /* Contributor.swift */; }; D5D797E9298DCC7300738869 /* cheatbase.zip in Resources */ = {isa = PBXBuildFile; fileRef = D5D797E7298DC9E200738869 /* cheatbase.zip */; }; D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */; }; @@ -453,6 +455,8 @@ D5A9C01929DDFBDD00A8D610 /* SettingsName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsName.swift; sourceTree = ""; }; D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariableFastForward.swift; sourceTree = ""; }; D5AAF27629884F8600F21ACF /* CheatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatDevice.swift; sourceTree = ""; }; + D5ADD12129F33FBF00CE0560 /* Features.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features.swift; sourceTree = ""; }; + D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSAirPlay.swift; sourceTree = ""; }; D5D797E5298D946200738869 /* Contributor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contributor.swift; sourceTree = ""; }; D5D797E7298DC9E200738869 /* cheatbase.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = cheatbase.zip; sourceTree = ""; }; D5D7C1E629E5F90200663793 /* OptionValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionValue.swift; sourceTree = ""; }; @@ -845,6 +849,7 @@ BF11734E1DA32CEC00047DF8 /* Controllers */, BF1DAD5B1D9F574900E752A7 /* Controller Skins */, BF48F74C219A16C100BC2FC1 /* Syncing */, + D5D78AE329F9BC0200E064F0 /* Features */, D5A9BFFF29DDECF500A8D610 /* Experimental Features */, D5D797E4298D944C00738869 /* Contributors */, ); @@ -1092,6 +1097,15 @@ path = "Experimental Features"; sourceTree = ""; }; + D5D78AE329F9BC0200E064F0 /* Features */ = { + isa = PBXGroup; + children = ( + D5ADD12129F33FBF00CE0560 /* Features.swift */, + D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */, + ); + path = Features; + sourceTree = ""; + }; D5D797E4298D944C00738869 /* Contributors */ = { isa = PBXGroup; children = ( @@ -1519,11 +1533,13 @@ D560BD8629EDC45600289847 /* ExternalDisplaySceneDelegate.swift in Sources */, BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */, BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */, + D5D78AE529F9BC3700E064F0 /* DSAirPlay.swift in Sources */, BF4828841F9027B600028B97 /* Delta.xcdatamodeld in Sources */, BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */, BFC3628021ADE2BA00EF2BE6 /* UIAlertController+Error.swift in Sources */, BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */, D5D797E6298D946200738869 /* Contributor.swift in Sources */, + D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */, BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */, BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */, D586497229774ABD0081477E /* CheatBase.swift in Sources */, diff --git a/Delta/Base.lproj/Settings.storyboard b/Delta/Base.lproj/Settings.storyboard index f185163..3231f46 100644 --- a/Delta/Base.lproj/Settings.storyboard +++ b/Delta/Base.lproj/Settings.storyboard @@ -1956,10 +1956,80 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1997,7 +2067,7 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. - + @@ -2020,7 +2090,7 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. - + @@ -2043,7 +2113,7 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. - + @@ -2070,7 +2140,7 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. - + @@ -2093,7 +2163,7 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. - + @@ -2116,7 +2186,7 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. - + @@ -2139,7 +2209,7 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. - + @@ -2166,7 +2236,7 @@ Delta uses OpenVGDB to provide automatic artwork for imported games. - + diff --git a/Delta/Database/Model/Human/ControllerSkin.swift b/Delta/Database/Model/Human/ControllerSkin.swift index 23b03d6..6ab08bb 100644 --- a/Delta/Database/Model/Human/ControllerSkin.swift +++ b/Delta/Database/Model/Human/ControllerSkin.swift @@ -90,6 +90,11 @@ extension ControllerSkin: ControllerSkinProtocol { return self.controllerSkin?.aspectRatio(for: traits) } + + public func contentSize(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize? + { + return self.controllerSkin?.contentSize(for: traits) + } } extension ControllerSkin: Syncable diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index 714eb97..eef3557 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -670,6 +670,12 @@ private extension GameViewController { var touchControllerSkin = TouchControllerSkin(controllerSkin: controllerSkin) + if UIApplication.shared.isExternalDisplayConnected + { + // Only show touch screen if external display is connected. + touchControllerSkin.screenPredicate = { $0.isTouchScreen } + } + if self.view.bounds.width > self.view.bounds.height { touchControllerSkin.screenLayoutAxis = .horizontal @@ -1119,8 +1125,10 @@ private extension GameViewController // We need to receive gameViewController(_:didUpdateGameViews) callback. scene.gameViewController.delegate = self - self.updateGameViews() - self.updateExternalDisplay() + self.updateControllerSkin() + + // Implicitly called from updateControllerSkin() + // self.updateExternalDisplay() } func updateExternalDisplay() @@ -1132,7 +1140,37 @@ private extension GameViewController scene.game = self.game } - self.updateExternalDisplayGameViews() + var controllerSkin: ControllerSkinProtocol? + + if let game = self.game, let traits = scene.gameViewController.controllerView.controllerSkinTraits + { + if let standardSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type), standardSkin.supports(traits) + { + if standardSkin.hasTouchScreen(for: traits) + { + // Only use TouchControllerSkin for standard controller skins with touch screens. + + var touchControllerSkin = DeltaCore.TouchControllerSkin(controllerSkin: standardSkin) + touchControllerSkin.screenLayoutAxis = Settings.features.dsAirPlay.layoutAxis + + if Settings.features.dsAirPlay.topScreenOnly + { + touchControllerSkin.screenPredicate = { !$0.isTouchScreen } + } + + controllerSkin = touchControllerSkin + } + else + { + controllerSkin = standardSkin + } + } + } + + scene.gameViewController.controllerView.controllerSkin = controllerSkin + + // Implicitly called when assigning controllerSkin. + // self.updateExternalDisplayGameViews() } func updateExternalDisplayGameViews() @@ -1154,7 +1192,7 @@ private extension GameViewController self.emulatorCore?.remove(gameView) } - self.updateGameViews() + self.updateControllerSkin() // Reset TouchControllerSkin + GameViews } } @@ -1317,6 +1355,10 @@ private extension GameViewController case .syncingService, .isAltJITEnabled: break + case Settings.features.dsAirPlay.$topScreenOnly.settingsKey: fallthrough + case Settings.features.dsAirPlay.$layoutAxis.settingsKey: + self.updateExternalDisplay() + default: break } } diff --git a/Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift b/Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift index aa8c23a..ded593e 100644 --- a/Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift +++ b/Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift @@ -23,12 +23,19 @@ private extension MelonDSCoreSettingsViewController enum Section: Int { case general + case airPlay case performance case dsBIOS case dsiBIOS case changeCore } + enum AirPlayRow: Int, CaseIterable + { + case topScreenOnly + case layoutHorizontally + } + @available(iOS 13, *) enum BIOSError: LocalizedError { @@ -265,6 +272,32 @@ private extension MelonDSCoreSettingsViewController Settings.isAltJITEnabled = sender.isOn } + @IBAction func toggleTopScreenOnly(_ sender: UISwitch) + { + Settings.features.dsAirPlay.topScreenOnly = sender.isOn + + self.tableView.performBatchUpdates({ + let layoutHorizontallyIndexPath = IndexPath(row: AirPlayRow.layoutHorizontally.rawValue, section: Section.airPlay.rawValue) + if sender.isOn + { + self.tableView.deleteRows(at: [layoutHorizontallyIndexPath], with: .automatic) + } + else + { + self.tableView.insertRows(at: [layoutHorizontallyIndexPath], with: .automatic) + } + }) { _ in + self.tableView.reloadSections([Section.airPlay.rawValue], with: .none) + } + } + + @IBAction func toggleLayoutHorizontally(_ sender: UISwitch) + { + Settings.features.dsAirPlay.layoutAxis = sender.isOn ? .horizontal : .vertical + + self.tableView.reloadSections([Section.airPlay.rawValue], with: .none) + } + @objc func willEnterForeground(_ notification: Notification) { self.tableView.reloadData() @@ -277,13 +310,11 @@ extension MelonDSCoreSettingsViewController { let section = Section(rawValue: sectionIndex)! - if isSectionHidden(section) + switch section { - return 0 - } - else - { - return super.tableView(tableView, numberOfRowsInSection: sectionIndex) + case _ where isSectionHidden(section): return 0 + case .airPlay where Settings.features.dsAirPlay.topScreenOnly: return 1 // Layout axis is irrelevant if only AirPlaying top screen. + default: return super.tableView(tableView, numberOfRowsInSection: sectionIndex) } } @@ -313,6 +344,16 @@ extension MelonDSCoreSettingsViewController cell.contentView.isHidden = (item == nil) + case .airPlay: + let cell = cell as! SwitchTableViewCell + + let row = AirPlayRow.allCases[indexPath.row] + switch row + { + case .topScreenOnly: cell.switchView.isOn = Settings.features.dsAirPlay.topScreenOnly + case .layoutHorizontally: cell.switchView.isOn = (Settings.features.dsAirPlay.layoutAxis == .horizontal) + } + case .performance: let cell = cell as! SwitchTableViewCell cell.switchView.isOn = Settings.isAltJITEnabled @@ -396,7 +437,7 @@ extension MelonDSCoreSettingsViewController case .changeCore: self.changeCore() - case .performance: break + case .airPlay, .performance: break } } @@ -417,14 +458,18 @@ extension MelonDSCoreSettingsViewController override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { let section = Section(rawValue: section)! - - if isSectionHidden(section) + switch section { - return nil - } - else - { - return super.tableView(tableView, titleForFooterInSection: section.rawValue) + case _ where isSectionHidden(section): return nil + case .airPlay: + switch (Settings.features.dsAirPlay.topScreenOnly, Settings.features.dsAirPlay.layoutAxis) + { + case (true, _): return NSLocalizedString("When AirPlaying DS games, only the top screen will appear on the external display.", comment: "") + case (false, .vertical): return NSLocalizedString("When AirPlaying DS games, both screens will be stacked vertically on the external display.", comment: "") + case (false, .horizontal): return NSLocalizedString("When AirPlaying DS games, both screens will be placed side-by-side on the external display.", comment: "") + } + + default: return super.tableView(tableView, titleForFooterInSection: section.rawValue) } } diff --git a/Delta/Settings/Features/DSAirPlay.swift b/Delta/Settings/Features/DSAirPlay.swift new file mode 100644 index 0000000..d684b27 --- /dev/null +++ b/Delta/Settings/Features/DSAirPlay.swift @@ -0,0 +1,23 @@ +// +// DSAirPlay.swift +// Delta +// +// Created by Riley Testut on 4/26/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import SwiftUI + +import DeltaFeatures +import DeltaCore + +extension TouchControllerSkin.LayoutAxis: OptionValue {} + +struct DSAirPlayOptions +{ + @Option + var topScreenOnly: Bool = true + + @Option + var layoutAxis: TouchControllerSkin.LayoutAxis = .vertical +} diff --git a/Delta/Settings/Features/Features.swift b/Delta/Settings/Features/Features.swift new file mode 100644 index 0000000..038cce4 --- /dev/null +++ b/Delta/Settings/Features/Features.swift @@ -0,0 +1,25 @@ +// +// Features.swift +// Delta +// +// Created by Riley Testut on 4/21/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import DeltaFeatures + +extension Settings +{ + struct Features: FeatureContainer + { + static let shared = Features() + + @Feature(name: "DS AirPlay", options: DSAirPlayOptions()) + var dsAirPlay + + private init() + { + self.prepareFeatures() + } + } +} diff --git a/Delta/Settings/Settings.swift b/Delta/Settings/Settings.swift index 5ca84f3..24ad1cf 100644 --- a/Delta/Settings/Settings.swift +++ b/Delta/Settings/Settings.swift @@ -49,6 +49,8 @@ extension Settings struct Settings { + static let features = Features.shared + static func registerDefaults() { let defaults = [#keyPath(UserDefaults.translucentControllerSkinOpacity): 0.7,