From 884719241911900d2d1781ceb42518cde42e2c5e Mon Sep 17 00:00:00 2001 From: lixd <946951925@qq.com> Date: Thu, 13 Aug 2026 12:01:26 +0800 Subject: [PATCH 1/2] Fix microphone permission sync on macOS 26 --- .../Controls/PTFakeTouch/NSObject+Swizzle.m | 49 +++++++++++++++++-- Scripts/verify-macos26-microphone.sh | 15 ++++++ docs/macos26-microphone-permission-design.md | 39 +++++++++++++++ 3 files changed, 98 insertions(+), 5 deletions(-) create mode 100755 Scripts/verify-macos26-microphone.sh create mode 100644 docs/macos26-microphone-permission-design.md diff --git a/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m b/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m index e50200fb..4e321b3f 100644 --- a/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m +++ b/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m @@ -13,6 +13,7 @@ #import "PTFakeMetaTouch.h" #import #import +#import #import #import @@ -176,11 +177,37 @@ - (NSString *)hook_stringByReplacingOccurrencesOfRegularExpressionPattern:(NSStr } - (void)hook_requestRecordPermission:(void (^)(BOOL))response { - BOOL granted = [[AVAudioSession sharedInstance] recordPermission] == AVAudioSessionRecordPermissionGranted; - if (granted) { - response(granted); + if (@available(iOS 17.0, macOS 14.0, *)) { + AVAudioApplicationRecordPermission permission = [AVAudioApplication sharedInstance].recordPermission; + if (permission == AVAudioApplicationRecordPermissionGranted) { + response(YES); + } else if (permission == AVAudioApplicationRecordPermissionDenied) { + response(NO); + } else { + [AVAudioApplication requestRecordPermissionWithCompletionHandler:response]; + } + } else { + BOOL granted = [[AVAudioSession sharedInstance] recordPermission] == AVAudioSessionRecordPermissionGranted; + if (granted) { + response(YES); + } else { + [self hook_requestRecordPermission:response]; + } + } +} + ++ (void)hook_requestRecordPermissionWithCompletionHandler:(void (^)(BOOL))response { + if (@available(iOS 17.0, macOS 14.0, *)) { + AVAudioApplicationRecordPermission permission = [AVAudioApplication sharedInstance].recordPermission; + if (permission == AVAudioApplicationRecordPermissionGranted) { + response(YES); + } else if (permission == AVAudioApplicationRecordPermissionDenied) { + response(NO); + } else { + [self hook_requestRecordPermissionWithCompletionHandler:response]; + } } else { - [self hook_requestRecordPermission:response]; + response(NO); } } @@ -351,7 +378,19 @@ + (void)load { } if ([[PlaySettings shared] checkMicPermissionSync]) { - [objc_getClass("AVAudioSession") swizzleInstanceMethod:@selector(requestRecordPermission:) withMethod:@selector(hook_requestRecordPermission:)]; + Class audioSession = objc_getClass("AVAudioSession"); + SEL legacyRequestSelector = @selector(requestRecordPermission:); + if (audioSession && class_getInstanceMethod(audioSession, legacyRequestSelector)) { + [audioSession swizzleInstanceMethod:legacyRequestSelector + withMethod:@selector(hook_requestRecordPermission:)]; + } + + Class audioApplication = objc_getClass("AVAudioApplication"); + SEL applicationRequestSelector = @selector(requestRecordPermissionWithCompletionHandler:); + if (audioApplication && class_getClassMethod(audioApplication, applicationRequestSelector)) { + [audioApplication swizzleClassMethod:applicationRequestSelector + withMethod:@selector(hook_requestRecordPermissionWithCompletionHandler:)]; + } } if ([[PlaySettings shared] limitMotionUpdateFrequency]) { diff --git a/Scripts/verify-macos26-microphone.sh b/Scripts/verify-macos26-microphone.sh new file mode 100755 index 00000000..3ef75967 --- /dev/null +++ b/Scripts/verify-macos26-microphone.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +source_file="PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m" + +test -f "$source_file" +rg -q '#import ' "$source_file" +rg -q 'hook_requestRecordPermissionWithCompletionHandler:' "$source_file" +rg -q 'requestRecordPermissionWithCompletionHandler:' "$source_file" +rg -q 'AVAudioApplicationRecordPermissionGranted' "$source_file" +rg -q 'AVAudioApplicationRecordPermissionDenied' "$source_file" +rg -q 'AVAudioSessionRecordPermissionGranted' "$source_file" +rg -q 'swizzleClassMethod:applicationRequestSelector' "$source_file" + +echo "macOS 26 microphone compatibility checks passed" diff --git a/docs/macos26-microphone-permission-design.md b/docs/macos26-microphone-permission-design.md new file mode 100644 index 00000000..997cec3c --- /dev/null +++ b/docs/macos26-microphone-permission-design.md @@ -0,0 +1,39 @@ +# macOS 26 Microphone Permission Compatibility + +## Problem + +PlayTools currently synchronizes the legacy `AVAudioSession` microphone +permission callback. On macOS 26, the supported application-level permission +API is `AVAudioApplication`, so games that reach the newer API—or games whose +legacy request is bridged through the newer API—can report a denied permission +without creating a TCC request for the translated app. + +## Design + +Keep the existing opt-in `checkMicPermissionSync` behavior and extend it at the +PlayTools boundary: + +1. Add a class-method hook for + `AVAudioApplication.requestRecordPermissionWithCompletionHandler:`. +2. When the new API is available, make the legacy + `AVAudioSession.requestRecordPermission:` hook use the new API as its + authorization source. A granted or denied state is returned synchronously; + an undetermined state is forwarded to the real + `AVAudioApplication.requestRecordPermissionWithCompletionHandler:` request + so macOS can update TCC and invoke the game callback. +3. Keep the existing `AVAudioSession` fallback for older runtimes where + `AVAudioApplication` is unavailable. +4. Install both hooks only when `checkMicPermissionSync` is enabled, matching + the existing per-app opt-in behavior. + +The hook does not fabricate permission. It delegates the decision to Apple's +permission API and only normalizes an already-determined result to synchronous +callback behavior required by affected games. + +## Verification + +- A source-level regression script checks that the new API header, hook, + fallback, and registration are all present. +- The PlayTools Xcode project is built with the current macOS 26 SDK. +- The resulting framework is inspected for the new selectors and its + signature is verified after Catalyst conversion. From f277e818e043a2bf154b0235c0b6e51b022f21bc Mon Sep 17 00:00:00 2001 From: lixd <946951925@qq.com> Date: Thu, 13 Aug 2026 13:47:24 +0800 Subject: [PATCH 2/2] Fix microphone settings path in macOS sandbox --- PlayTools/PlaySettings.swift | 6 ++++++ Scripts/verify-macos26-microphone.sh | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/PlayTools/PlaySettings.swift b/PlayTools/PlaySettings.swift index cf14058d..1b28e3d9 100644 --- a/PlayTools/PlaySettings.swift +++ b/PlayTools/PlaySettings.swift @@ -4,6 +4,12 @@ import UIKit let settings = PlaySettings.shared func playCoverUserHomeDirectoryPath() -> String { + let bundlePath = Bundle.main.bundlePath + let containerMarker = "/Library/Containers/io.playcover.PlayCover/" + if let markerRange = bundlePath.range(of: containerMarker) { + return String(bundlePath[..' "$source_file" rg -q 'hook_requestRecordPermissionWithCompletionHandler:' "$source_file" rg -q 'requestRecordPermissionWithCompletionHandler:' "$source_file" @@ -11,5 +13,7 @@ rg -q 'AVAudioApplicationRecordPermissionGranted' "$source_file" rg -q 'AVAudioApplicationRecordPermissionDenied' "$source_file" rg -q 'AVAudioSessionRecordPermissionGranted' "$source_file" rg -q 'swizzleClassMethod:applicationRequestSelector' "$source_file" +rg -q 'Bundle\.main\.bundlePath' "$settings_file" +rg -q '/Library/Containers/io\.playcover\.PlayCover/' "$settings_file" echo "macOS 26 microphone compatibility checks passed"