From 916a8065b844ad2f0555e58b6e2abdfe55922b0e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 23:35:19 +0000 Subject: [PATCH 1/2] Add DSU Client (cemuhook) settings screen for iOS - New ControllersDsuClientViewController: Enable switch, server list (add/swipe-to-delete), description with tappable wiki link. - New ControllersAddDsuServerViewController: push form (description, address, port) matching the Qt DualShockUDPClientWidget behavior, including live character filtering for ':' and ';' separators. - New DsuServerCell for the server list rows. - Wire the new cell into ControllersSettings.storyboard next to Touchscreen, plus the two new scenes. - Add the one missing localized string used only by the iOS-side validation alert (Core.strings, en.lproj). - Register the new source files in the Xcode project (PBXFileReference, PBXBuildFile, Controllers group, Sources build phase). - Add a lightweight GitHub Actions workflow (dsu-client-ci.yml) that builds the DiOS (NJB) scheme in Debug on a GitHub-hosted macos-14 runner, for quick compile validation on push/PR without requiring the self-hosted signing runner used by build.yml. --- .github/workflows/dsu-client-ci.yml | 119 ++++++++ .../UI/Localization/en.lproj/Core.strings | 1 + .../App/DolphiniOS.xcodeproj/project.pbxproj | 20 ++ .../ControllersAddDsuServerViewController.h | 22 ++ .../ControllersAddDsuServerViewController.mm | 74 +++++ ...ollersAddDsuServerViewControllerDelegate.h | 19 ++ .../ControllersDsuClientViewController.h | 17 ++ .../ControllersDsuClientViewController.mm | 246 ++++++++++++++++ .../ControllersSettings.storyboard | 275 ++++++++++++++++++ .../UI/Settings/Controllers/DsuServerCell.h | 14 + .../UI/Settings/Controllers/DsuServerCell.m | 8 + 11 files changed, 815 insertions(+) create mode 100644 .github/workflows/dsu-client-ci.yml create mode 100644 Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewController.h create mode 100644 Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewController.mm create mode 100644 Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewControllerDelegate.h create mode 100644 Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersDsuClientViewController.h create mode 100644 Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersDsuClientViewController.mm create mode 100644 Source/iOS/App/DolphiniOS/UI/Settings/Controllers/DsuServerCell.h create mode 100644 Source/iOS/App/DolphiniOS/UI/Settings/Controllers/DsuServerCell.m diff --git a/.github/workflows/dsu-client-ci.yml b/.github/workflows/dsu-client-ci.yml new file mode 100644 index 0000000000..4c4f5fa564 --- /dev/null +++ b/.github/workflows/dsu-client-ci.yml @@ -0,0 +1,119 @@ +name: DSU Client CI + +# Workflow de validacion en runners hosteados por GitHub (sin infraestructura +# propia, sin necesidad de una Mac local). +# +# El workflow existente del repo (`build.yml`) hace el archive/IPA/DEB +# completo pero requiere `runs-on: [self-hosted, macOS]` con el toolchain de +# jailbreak ya instalado. Este workflow es mas liviano: solo COMPILA (no +# archiva) el scheme "DiOS (NJB)" en Debug, con firma de codigo deshabilitada, +# para detectar errores de compilacion en cada push/PR. Pensado especialmente +# para validar los cambios de la pantalla DSU Client (Controllers/Controllers +# DsuClient*, Controllers/DsuServerCell*, Controllers/ControllersAddDsuServer*) +# y cualquier otro cambio en el arbol de iOS. +# +# Sobre el runner y la version de Xcode: +# - `runs-on:` en GitHub Actions usa etiquetas por VERSION DE macOS, no por +# version de Xcode: macos-26 (Apple Silicon, la mas nueva), macos-15, +# macos-14 (en baja: deprecada desde el 6/jul/2026, se apaga el 2/nov/2026 +# - no usar). No existe "runs-on: xcode-27". +# - Xcode se selecciona con un paso dentro del job (`xcode-select` o la +# action `maxim-lobanov/setup-xcode`), porque cada imagen trae varias +# versiones de Xcode preinstaladas en paralelo. +# - Las versiones beta/RC de Xcode (como Xcode 27, en preview publico) solo +# se instalan en la imagen de macOS mas nueva (macos-26) - por eso el +# runner tiene que ser macos-26 si se quiere probar contra Xcode 27. +# +# Por eso hay DOS jobs: +# - build-stable: Xcode estable mas reciente -> senal confiable de "esto +# compila con lo que compilaria un usuario comun hoy". +# - build-xcode27-preview: Xcode 27 (preview publico) -> para adelantarse a +# roturas de SDK/toolchain, como haces otros proyectos de emuladores. +# No bloquea el PR si falla (continue-on-error), porque un preview puede +# romper por cosas ajenas a este cambio. + +on: + push: + branches: [ master ] + paths: + - 'Source/iOS/**' + - 'Source/Core/InputCommon/ControllerInterface/DualShockUDPClient/**' + - '.github/workflows/dsu-client-ci.yml' + pull_request: + branches: [ master ] + paths: + - 'Source/iOS/**' + - 'Source/Core/InputCommon/ControllerInterface/DualShockUDPClient/**' + - '.github/workflows/dsu-client-ci.yml' + workflow_dispatch: {} + +env: + XCODE_COMMON_BUILD_ARGS: -project Source/iOS/App/DolphiniOS.xcodeproj -derivedDataPath "${{ github.workspace }}/build-Xcode" -sdk iphoneos -destination generic/platform=iOS DOL_PBID_ORGANIZATION_IDENTIFIER="me.oatmealdome" DOL_BUILD_SOURCE="development" CODE_SIGNING_ALLOWED="NO" CODE_SIGNING_REQUIRED="NO" + +jobs: + build-stable: + name: Build (Xcode estable) + runs-on: macos-26 + + steps: + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Seleccionar Xcode estable mas reciente + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Mostrar version de Xcode seleccionada + run: xcodebuild -version + + - name: Compilar DiOS (NJB) - Debug + run: | + xcodebuild build \ + -configuration "Debug (Non-Jailbroken)" \ + -scheme "DiOS (NJB)" \ + ${{ env.XCODE_COMMON_BUILD_ARGS }} + + - name: Subir el .app compilado (sin firmar) como artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: DolphiniOS-NJB-Debug-xcode-estable + path: ${{ github.workspace }}/build-Xcode/Build/Products/Debug*iphoneos/DolphiniOS.app + if-no-files-found: ignore + retention-days: 14 + + build-xcode27-preview: + name: Build (Xcode 27 preview) + runs-on: macos-26 + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Seleccionar Xcode 27 (preview publico, incluye betas) + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + + - name: Mostrar version de Xcode seleccionada + run: xcodebuild -version + + - name: Compilar DiOS (NJB) - Debug + run: | + xcodebuild build \ + -configuration "Debug (Non-Jailbroken)" \ + -scheme "DiOS (NJB)" \ + ${{ env.XCODE_COMMON_BUILD_ARGS }} + + - name: Subir el .app compilado (sin firmar) como artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: DolphiniOS-NJB-Debug-xcode27-preview + path: ${{ github.workspace }}/build-Xcode/Build/Products/Debug*iphoneos/DolphiniOS.app + if-no-files-found: ignore + retention-days: 14 diff --git a/Source/iOS/App/Common/UI/Localization/en.lproj/Core.strings b/Source/iOS/App/Common/UI/Localization/en.lproj/Core.strings index c00153d610..020261d53e 100644 --- a/Source/iOS/App/Common/UI/Localization/en.lproj/Core.strings +++ b/Source/iOS/App/Common/UI/Localization/en.lproj/Core.strings @@ -940,6 +940,7 @@ Would you like to ignore this line and continue parsing?"; "Enable" = "Enable"; "Add..." = "Add..."; "Remove" = "Remove"; +"Please enter a valid server address and port." = "Please enter a valid server address and port."; "DSU protocol enables the use of input and motion data from compatible sources, like PlayStation, Nintendo Switch and Steam controllers.

For setup instructions, refer to this page." = "DSU protocol enables the use of input and motion data from compatible sources, like PlayStation, Nintendo Switch and Steam controllers.

For setup instructions, refer to this page."; "Size" = "Size"; "Disc" = "Disc"; diff --git a/Source/iOS/App/DolphiniOS.xcodeproj/project.pbxproj b/Source/iOS/App/DolphiniOS.xcodeproj/project.pbxproj index 3b7b7f7124..11b54d4acb 100644 --- a/Source/iOS/App/DolphiniOS.xcodeproj/project.pbxproj +++ b/Source/iOS/App/DolphiniOS.xcodeproj/project.pbxproj @@ -177,6 +177,9 @@ B9DC5FB92884CB7C004BD94A /* GraphicsChoiceCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = B9DC5FB82884CB7C004BD94A /* GraphicsChoiceCell.mm */; }; B9EC1A43295FFFD30013EBE4 /* SoftwareListiOSViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = B9EC1A42295FFFD30013EBE4 /* SoftwareListiOSViewController.mm */; }; B9EC28382D21D429000FAE09 /* ControllersTouchscreenViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = B9EC28372D21D429000FAE09 /* ControllersTouchscreenViewController.mm */; }; + CFCE2A026C13DB1B9B62AD4B /* ControllersDsuClientViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = CE99DB7E010EF4A5584CB5DA /* ControllersDsuClientViewController.mm */; }; + CEAC14FEE3BC2980C98DB85F /* DsuServerCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EDA3BEE4FE789DBD3CFDF55 /* DsuServerCell.m */; }; + 73169C33207036903039304A /* ControllersAddDsuServerViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = FC8D428F66DEF8467F8DF51C /* ControllersAddDsuServerViewController.mm */; }; B9EC283A2D21DA56000FAE09 /* TCWiiTouchIRMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9EC28392D21DA56000FAE09 /* TCWiiTouchIRMode.swift */; }; B9EC283D2D21DE2F000FAE09 /* ControllersTouchscreenIRModeViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = B9EC283C2D21DE2F000FAE09 /* ControllersTouchscreenIRModeViewController.mm */; }; B9EFF8532E7F56790029F016 /* AppIcon_Glass.icon in Resources */ = {isa = PBXBuildFile; fileRef = B9EFF8522E7F56790029F016 /* AppIcon_Glass.icon */; }; @@ -548,6 +551,13 @@ B9EC1A42295FFFD30013EBE4 /* SoftwareListiOSViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SoftwareListiOSViewController.mm; sourceTree = ""; }; B9EC28362D21D429000FAE09 /* ControllersTouchscreenViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ControllersTouchscreenViewController.h; sourceTree = ""; }; B9EC28372D21D429000FAE09 /* ControllersTouchscreenViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ControllersTouchscreenViewController.mm; sourceTree = ""; }; + E723480BA2A352B04E8FD38E /* ControllersDsuClientViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ControllersDsuClientViewController.h; sourceTree = ""; }; + CE99DB7E010EF4A5584CB5DA /* ControllersDsuClientViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ControllersDsuClientViewController.mm; sourceTree = ""; }; + C623440D4EF2976092F76843 /* DsuServerCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DsuServerCell.h; sourceTree = ""; }; + 4EDA3BEE4FE789DBD3CFDF55 /* DsuServerCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DsuServerCell.m; sourceTree = ""; }; + BA352A1E6E317A3FBBBC4A87 /* ControllersAddDsuServerViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ControllersAddDsuServerViewController.h; sourceTree = ""; }; + FC8D428F66DEF8467F8DF51C /* ControllersAddDsuServerViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ControllersAddDsuServerViewController.mm; sourceTree = ""; }; + 088316AAD0B43DC1AB1ED40C /* ControllersAddDsuServerViewControllerDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ControllersAddDsuServerViewControllerDelegate.h; sourceTree = ""; }; B9EC28392D21DA56000FAE09 /* TCWiiTouchIRMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCWiiTouchIRMode.swift; sourceTree = ""; }; B9EC283B2D21DE2F000FAE09 /* ControllersTouchscreenIRModeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ControllersTouchscreenIRModeViewController.h; sourceTree = ""; }; B9EC283C2D21DE2F000FAE09 /* ControllersTouchscreenIRModeViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ControllersTouchscreenIRModeViewController.mm; sourceTree = ""; }; @@ -770,6 +780,13 @@ B9EC28372D21D429000FAE09 /* ControllersTouchscreenViewController.mm */, B9EC283B2D21DE2F000FAE09 /* ControllersTouchscreenIRModeViewController.h */, B9EC283C2D21DE2F000FAE09 /* ControllersTouchscreenIRModeViewController.mm */, + E723480BA2A352B04E8FD38E /* ControllersDsuClientViewController.h */, + CE99DB7E010EF4A5584CB5DA /* ControllersDsuClientViewController.mm */, + C623440D4EF2976092F76843 /* DsuServerCell.h */, + 4EDA3BEE4FE789DBD3CFDF55 /* DsuServerCell.m */, + BA352A1E6E317A3FBBBC4A87 /* ControllersAddDsuServerViewController.h */, + FC8D428F66DEF8467F8DF51C /* ControllersAddDsuServerViewController.mm */, + 088316AAD0B43DC1AB1ED40C /* ControllersAddDsuServerViewControllerDelegate.h */, ); path = Controllers; sourceTree = ""; @@ -2135,6 +2152,9 @@ B9B2C661288E569E00CB6569 /* ExternalDisplayEmulationViewController.mm in Sources */, B950CFA32961696100197719 /* BootNoticeNavigationViewController.m in Sources */, B9EC28382D21D429000FAE09 /* ControllersTouchscreenViewController.mm in Sources */, + CFCE2A026C13DB1B9B62AD4B /* ControllersDsuClientViewController.mm in Sources */, + CEAC14FEE3BC2980C98DB85F /* DsuServerCell.m in Sources */, + 73169C33207036903039304A /* ControllersAddDsuServerViewController.mm in Sources */, B9B1483C29642CB8002C4BE7 /* UpdateRequiredNoticeViewController.m in Sources */, B900A7B027F2866D000361DD /* UserFolderUtil.swift in Sources */, B974923C2960096C00794509 /* ImportFileManager.m in Sources */, diff --git a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewController.h b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewController.h new file mode 100644 index 0000000000..1e81460151 --- /dev/null +++ b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewController.h @@ -0,0 +1,22 @@ +// Copyright 2026 DolphiniOS Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#import + +@protocol ControllersAddDsuServerViewControllerDelegate; + +NS_ASSUME_NONNULL_BEGIN + +@interface ControllersAddDsuServerViewController : UITableViewController + +@property (weak, nonatomic, nullable) id delegate; + +@property (weak, nonatomic) IBOutlet UITextField* descriptionField; +@property (weak, nonatomic) IBOutlet UITextField* addressField; +@property (weak, nonatomic) IBOutlet UITextField* portField; + +- (IBAction)addPressed:(id)sender; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewController.mm b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewController.mm new file mode 100644 index 0000000000..3602b2d8f1 --- /dev/null +++ b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewController.mm @@ -0,0 +1,74 @@ +// Copyright 2026 DolphiniOS Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#import "ControllersAddDsuServerViewController.h" + +#import + +#import "InputCommon/ControllerInterface/DualShockUDPClient/DualShockUDPClient.h" + +#import "ControllersAddDsuServerViewControllerDelegate.h" +#import "FoundationStringUtil.h" +#import "LocalizationUtil.h" + +@interface ControllersAddDsuServerViewController () + +@end + +@implementation ControllersAddDsuServerViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.navigationItem.title = DOLCoreLocalizedString(@"Add New DSU Server"); + + self.descriptionField.placeholder = DOLCoreLocalizedString(@"BetterJoy, DS4Windows, etc"); + self.addressField.text = CppToFoundationString(std::string(ciface::DualShockUDPClient::DEFAULT_SERVER_ADDRESS)); + self.portField.text = [NSString stringWithFormat:@"%d", ciface::DualShockUDPClient::DEFAULT_SERVER_PORT]; +} + +// Mirrors ServerStringValidator: the description and address fields are +// used as fields in a "description:address:port;" serialized entry, so +// ':' and ';' can never be allowed in them. +- (BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string { + if (textField != self.descriptionField && textField != self.addressField) { + return true; + } + + return [string rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@":;"]].location == NSNotFound; +} + +- (void)showInvalidInputAlert { + UIAlertController* alert = [UIAlertController alertControllerWithTitle:DOLCoreLocalizedString(@"Error") + message:DOLCoreLocalizedString(@"Please enter a valid server address and port.") + preferredStyle:UIAlertControllerStyleAlert]; + + [alert addAction:[UIAlertAction actionWithTitle:DOLCoreLocalizedString(@"OK") style:UIAlertActionStyleDefault handler:nil]]; + + [self presentViewController:alert animated:true completion:nil]; +} + +- (IBAction)addPressed:(id)sender { + NSString* description = [self.descriptionField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString* address = [self.addressField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if (description.length == 0 || address.length == 0) { + [self showInvalidInputAlert]; + return; + } + + NSInteger port = [self.portField.text integerValue]; + if (port <= 0 || port > 0xFFFF) { + [self showInvalidInputAlert]; + return; + } + + [self.delegate addDsuServerViewController:self + didAddServerWithDescription:description + address:address + port:(uint16_t)port]; + + [self.navigationController popViewControllerAnimated:true]; +} + +@end diff --git a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewControllerDelegate.h b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewControllerDelegate.h new file mode 100644 index 0000000000..b01dbe0979 --- /dev/null +++ b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersAddDsuServerViewControllerDelegate.h @@ -0,0 +1,19 @@ +// Copyright 2026 DolphiniOS Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#import + +@class ControllersAddDsuServerViewController; + +NS_ASSUME_NONNULL_BEGIN + +@protocol ControllersAddDsuServerViewControllerDelegate + +- (void)addDsuServerViewController:(ControllersAddDsuServerViewController*)viewController + didAddServerWithDescription:(NSString*)description + address:(NSString*)address + port:(uint16_t)port; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersDsuClientViewController.h b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersDsuClientViewController.h new file mode 100644 index 0000000000..ca3c9981a3 --- /dev/null +++ b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersDsuClientViewController.h @@ -0,0 +1,17 @@ +// Copyright 2026 DolphiniOS Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ControllersDsuClientViewController : UITableViewController + +@property (weak, nonatomic) IBOutlet UISwitch* enabledSwitch; +@property (weak, nonatomic) IBOutlet UITextView* descriptionTextView; + +- (IBAction)enabledSwitchChanged:(id)sender; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersDsuClientViewController.mm b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersDsuClientViewController.mm new file mode 100644 index 0000000000..bb4dbe3dee --- /dev/null +++ b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersDsuClientViewController.mm @@ -0,0 +1,246 @@ +// Copyright 2026 DolphiniOS Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#import "ControllersDsuClientViewController.h" + +#import +#import + +#import + +#import "Common/CommonTypes.h" +#import "Common/Config/Config.h" +#import "Common/StringUtil.h" + +#import "InputCommon/ControllerInterface/DualShockUDPClient/DualShockUDPClient.h" + +#import "ControllersAddDsuServerViewController.h" +#import "ControllersAddDsuServerViewControllerDelegate.h" +#import "DsuServerCell.h" +#import "FoundationStringUtil.h" +#import "LocalizationUtil.h" + +namespace +{ +struct DsuServerEntry +{ + std::string description; + std::string address; + u16 port; +}; + +// Mirrors ciface::DualShockUDPClient::InputBackend::ConfigChanged and +// DualShockUDPClientWidget::RefreshServerList: entries are stored as +// "description:address:port;" and old single-server settings (kept only +// for backwards compatibility) are folded into the new list on first read. +std::vector LoadDsuServers() +{ + const auto legacy_address = Config::Get(ciface::DualShockUDPClient::Settings::SERVER_ADDRESS); + const auto legacy_port = Config::Get(ciface::DualShockUDPClient::Settings::SERVER_PORT); + + if (!legacy_address.empty() && legacy_port != 0) + { + const auto& current_setting = Config::Get(ciface::DualShockUDPClient::Settings::SERVERS); + Config::SetBaseOrCurrent(ciface::DualShockUDPClient::Settings::SERVERS, + current_setting + fmt::format("{}:{}:{};", "DS4", legacy_address, + legacy_port)); + Config::SetBase(ciface::DualShockUDPClient::Settings::SERVER_ADDRESS, ""); + Config::SetBase(ciface::DualShockUDPClient::Settings::SERVER_PORT, 0); + } + + std::vector servers; + + const auto& servers_setting = Config::Get(ciface::DualShockUDPClient::Settings::SERVERS); + for (const auto& entry : SplitString(servers_setting, ';')) + { + const auto parts = SplitString(entry, ':'); + if (parts.size() < 3) + { + continue; + } + + const int port = std::atoi(parts[2].c_str()); + if (port < 0 || port > 0xFFFF) + { + continue; + } + + servers.push_back({parts[0], parts[1], static_cast(port)}); + } + + return servers; +} + +void SaveDsuServers(const std::vector& servers) +{ + std::string new_setting; + for (const auto& server : servers) + { + new_setting += fmt::format("{}:{}:{};", server.description, server.address, server.port); + } + + Config::SetBaseOrCurrent(ciface::DualShockUDPClient::Settings::SERVERS, new_setting); +} +} // namespace + +@interface ControllersDsuClientViewController () + +@end + +@implementation ControllersDsuClientViewController { + std::vector _servers; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.title = DOLCoreLocalizedString(@"DSU Client"); + + NSString* htmlDescription = DOLCoreLocalizedString( + @"DSU protocol enables the use of input and motion data from compatible " + @"sources, like PlayStation, Nintendo Switch and Steam controllers.

" + @"For setup instructions, " + @"" + @"refer to this page."); + + NSData* htmlData = [htmlDescription dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary* options = @{ + NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType, + NSCharacterEncodingDocumentAttribute : @(NSUTF8StringEncoding) + }; + + NSAttributedString* attributedDescription = + [[NSAttributedString alloc] initWithData:htmlData options:options + documentAttributes:nil error:nil]; + + if (attributedDescription != nil) { + self.descriptionTextView.attributedText = attributedDescription; + } else { + // Fall back to plain text if, for whatever reason, the HTML failed to parse. + self.descriptionTextView.text = htmlDescription; + } + + self.descriptionTextView.font = [UIFont systemFontOfSize:15]; + self.descriptionTextView.textColor = [UIColor secondaryLabelColor]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + self->_servers = LoadDsuServers(); + + self.enabledSwitch.on = Config::Get(ciface::DualShockUDPClient::Settings::SERVERS_ENABLED); + + [self.tableView reloadData]; +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView { + return 3; +} + +- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section { + switch (section) { + case 0: // Enable + return 1; + case 1: // Servers + Add row + return self->_servers.size() + 1; + default: // Description + return 1; + } +} + +- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath { + if (indexPath.section == 0) { + return [tableView dequeueReusableCellWithIdentifier:@"enableCell" forIndexPath:indexPath]; + } + + if (indexPath.section == 1) { + if ((NSUInteger)indexPath.row == self->_servers.size()) { + UITableViewCell* addCell = [tableView dequeueReusableCellWithIdentifier:@"addCell" forIndexPath:indexPath]; + addCell.userInteractionEnabled = self.enabledSwitch.on; + addCell.textLabel.alpha = self.enabledSwitch.on ? 1.0 : 0.5; + return addCell; + } + + DsuServerCell* serverCell = [tableView dequeueReusableCellWithIdentifier:@"serverCell" forIndexPath:indexPath]; + + const auto& server = self->_servers[indexPath.row]; + serverCell.nameLabel.text = [NSString stringWithFormat:@"%@:%@ - %@", + CppToFoundationString(server.address), + @(server.port), + CppToFoundationString(server.description)]; + serverCell.nameLabel.alpha = self.enabledSwitch.on ? 1.0 : 0.5; + + return serverCell; + } + + return [tableView dequeueReusableCellWithIdentifier:@"descriptionCell" forIndexPath:indexPath]; +} + +- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:true]; + + if (indexPath.section != 1 || (NSUInteger)indexPath.row != self->_servers.size()) { + return; + } + + if (!self.enabledSwitch.on) { + return; + } + + [self performSegueWithIdentifier:@"addServer" sender:nil]; +} + +- (BOOL)tableView:(UITableView*)tableView canEditRowAtIndexPath:(NSIndexPath*)indexPath { + if (indexPath.section != 1 || (NSUInteger)indexPath.row == self->_servers.size()) { + return false; + } + + return self.enabledSwitch.on; +} + +- (void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath*)indexPath { + if (editingStyle != UITableViewCellEditingStyleDelete) { + return; + } + + self->_servers.erase(self->_servers.begin() + indexPath.row); + SaveDsuServers(self->_servers); + + [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; +} + +#pragma mark - Actions + +- (IBAction)enabledSwitchChanged:(id)sender { + Config::SetBaseOrCurrent(ciface::DualShockUDPClient::Settings::SERVERS_ENABLED, (bool)self.enabledSwitch.on); + + // Section 1 needs to redraw: the server rows and the "Add..." row both + // reflect the enabled state (dimmed + non-interactive when disabled). + [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic]; +} + +#pragma mark - Navigation + +- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender { + if ([segue.identifier isEqualToString:@"addServer"]) { + ControllersAddDsuServerViewController* addController = segue.destinationViewController; + addController.delegate = self; + } +} + +#pragma mark - ControllersAddDsuServerViewControllerDelegate + +- (void)addDsuServerViewController:(ControllersAddDsuServerViewController*)viewController + didAddServerWithDescription:(NSString*)description + address:(NSString*)address + port:(uint16_t)port { + self->_servers.push_back({FoundationToCppString(description), FoundationToCppString(address), port}); + SaveDsuServers(self->_servers); + + [self.tableView reloadData]; +} + +@end diff --git a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersSettings.storyboard b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersSettings.storyboard index 157662d7fc..637c8e1c36 100644 --- a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersSettings.storyboard +++ b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/ControllersSettings.storyboard @@ -330,6 +330,31 @@ + + + + + + + + + + + + + + + + + + + + @@ -691,5 +716,255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/DsuServerCell.h b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/DsuServerCell.h new file mode 100644 index 0000000000..bf4716cab2 --- /dev/null +++ b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/DsuServerCell.h @@ -0,0 +1,14 @@ +// Copyright 2026 DolphiniOS Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DsuServerCell : UITableViewCell + +@property (weak, nonatomic) IBOutlet UILabel* nameLabel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/DsuServerCell.m b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/DsuServerCell.m new file mode 100644 index 0000000000..aa2a640ad4 --- /dev/null +++ b/Source/iOS/App/DolphiniOS/UI/Settings/Controllers/DsuServerCell.m @@ -0,0 +1,8 @@ +// Copyright 2026 DolphiniOS Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#import "DsuServerCell.h" + +@implementation DsuServerCell + +@end From 28c1d7531c4c47916720e5eca383e0efac5c9162 Mon Sep 17 00:00:00 2001 From: KavrixIDK Date: Thu, 3 Sep 2026 22:57:45 -0300 Subject: [PATCH 2/2] Change build runner to macos-latest --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ac3a9f95c..d47035cec9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: jobs: build: # runs-on: macos-12 - runs-on: [self-hosted, macOS] + runs-on: macos-latest env: XCODE_COMMON_BUILD_ARGS: -project Source/iOS/App/DolphiniOS.xcodeproj -derivedDataPath "${{ github.workspace }}/build-Xcode" -sdk iphoneos -destination generic/platform=iOS DOL_PBID_ORGANIZATION_IDENTIFIER="me.oatmealdome" DOL_BUILD_SOURCE="development" CODE_SIGNING_ALLOWED="NO" CODE_SIGNING_REQUIRED="NO" @@ -52,4 +52,4 @@ jobs: run: | "${{ github.workspace }}/Source/iOS/App/Project/Scripts/CreateDeb.sh" "${{ github.workspace }}/archives/Jailbroken.xcarchive/Products/Applications/DolphiniOS.app" "OatmealDome Software" "${{ github.workspace }}/Source/iOS/App/Project/Entitlements/Private.entitlements" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/${{ env.DEB_CONTROL_NAME }}.in" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/postinst.sh" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/postrm.sh" "Applications" "${{ github.workspace }}/products/Jailbroken-Rootful.deb" "${{ github.workspace }}/Source/iOS/App/Project/Scripts/CreateDeb.sh" "${{ github.workspace }}/archives/Jailbroken.xcarchive/Products/Applications/DolphiniOS.app" "OatmealDome Software" "${{ github.workspace }}/Source/iOS/App/Project/Entitlements/Private.entitlements" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/${{ env.DEB_CONTROL_NAME }}-rootless.in" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/postinst.sh" "${{ github.workspace }}/Source/iOS/App/Project/Package/Deb/postrm.sh" "var/jb/Applications" "${{ github.workspace }}/products/Jailbroken-Rootless.deb" - \ No newline at end of file +