diff --git a/AKPlugin.swift b/AKPlugin.swift index 43003749..ee9e72b1 100644 --- a/AKPlugin.swift +++ b/AKPlugin.swift @@ -18,11 +18,11 @@ class AKPlugin: NSObject, Plugin { } var mousePoint: CGPoint { - NSApplication.shared.windows.first!.mouseLocationOutsideOfEventStream as CGPoint + NSApplication.shared.windows.first?.mouseLocationOutsideOfEventStream ?? CGPoint() } var windowFrame: CGRect { - NSApplication.shared.windows.first!.frame as CGRect + NSApplication.shared.windows.first?.frame ?? CGRect() } var isMainScreenEqualToFirst: Bool { @@ -37,9 +37,19 @@ class AKPlugin: NSObject, Plugin { NSApplication.shared.windows.first!.styleMask.contains(.fullScreen) } + var cmdPressed: Bool = false + func hideCursor() { NSCursor.hide() CGAssociateMouseAndMouseCursorPosition(0) + warpCursor() + } + + func warpCursor() { + guard let firstScreen = NSScreen.screens.first else {return} + let frame = windowFrame + // Convert from NS coordinates to CG coordinates + CGWarpMouseCursorPosition(CGPoint(x: frame.midX, y: firstScreen.frame.height - frame.midY)) } func unhideCursor() { @@ -51,25 +61,90 @@ class AKPlugin: NSObject, Plugin { NSApplication.shared.terminate(self) } - func eliminateRedundantKeyPressEvents(_ dontIgnore: @escaping() -> Bool) { + private var modifierFlag: UInt = 0 + func setupKeyboard(keyboard: @escaping(UInt16, Bool, Bool) -> Bool, + swapMode: @escaping() -> Bool) { + func checkCmd(modifier: NSEvent.ModifierFlags) -> Bool { + if modifier.contains(.command) { + self.cmdPressed = true + return true + } else if self.cmdPressed { + self.cmdPressed = false + } + return false + } NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: { event in - if dontIgnore() { + if checkCmd(modifier: event.modifierFlags) { return event } - return nil + let consumed = keyboard(event.keyCode, true, event.isARepeat) + if consumed { + return nil + } + return event + }) + NSEvent.addLocalMonitorForEvents(matching: .keyUp, handler: { event in + if checkCmd(modifier: event.modifierFlags) { + return event + } + let consumed = keyboard(event.keyCode, false, false) + if consumed { + return nil + } + return event + }) + NSEvent.addLocalMonitorForEvents(matching: .flagsChanged, handler: { event in + if checkCmd(modifier: event.modifierFlags) { + return event + } + let pressed = self.modifierFlag < event.modifierFlags.rawValue + let changed = self.modifierFlag ^ event.modifierFlags.rawValue + self.modifierFlag = event.modifierFlags.rawValue + if pressed && NSEvent.ModifierFlags(rawValue: changed).contains(.option) { + if swapMode() { + return nil + } + return event + } + let consumed = keyboard(event.keyCode, pressed, false) + if consumed { + return nil + } + return event + }) + } + + func setupMouseMoved(mouseMoved: @escaping(CGFloat, CGFloat) -> Bool) { + let mask: NSEvent.EventTypeMask = [.leftMouseDragged, .otherMouseDragged, .rightMouseDragged] + NSEvent.addLocalMonitorForEvents(matching: mask, handler: { event in + let consumed = mouseMoved(event.deltaX, event.deltaY) + if consumed { + return nil + } + return event + }) + // transpass mouse moved event when no button pressed, for traffic light button to light up + NSEvent.addLocalMonitorForEvents(matching: .mouseMoved, handler: { event in + _ = mouseMoved(event.deltaX, event.deltaY) + return event }) } - func setupMouseButton(_ _up: Int, _ _down: Int, _ dontIgnore: @escaping(Int, Bool, Bool) -> Bool) { - NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask(rawValue: UInt64(_up)), handler: { event in - let isEventWindow = event.window == NSApplication.shared.windows.first! - if dontIgnore(_up, true, isEventWindow) { + func setupMouseButton(left: Bool, right: Bool, _ dontIgnore: @escaping(Bool) -> Bool) { + let downType: NSEvent.EventTypeMask = left ? .leftMouseDown : right ? .rightMouseDown : .otherMouseDown + let upType: NSEvent.EventTypeMask = left ? .leftMouseUp : right ? .rightMouseUp : .otherMouseUp + NSEvent.addLocalMonitorForEvents(matching: downType, handler: { event in + // For traffic light buttons when fullscreen + if event.window != NSApplication.shared.windows.first! { + return event + } + if dontIgnore(true) { return event } return nil }) - NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask(rawValue: UInt64(_down)), handler: { event in - if dontIgnore(_up, false, true) { + NSEvent.addLocalMonitorForEvents(matching: upType, handler: { event in + if dontIgnore(false) { return event } return nil @@ -78,7 +153,12 @@ class AKPlugin: NSObject, Plugin { func setupScrollWheel(_ onMoved: @escaping(CGFloat, CGFloat) -> Bool) { NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.scrollWheel, handler: { event in - let consumed = onMoved(event.scrollingDeltaX, event.scrollingDeltaY) + var deltaX = event.scrollingDeltaX, deltaY = event.scrollingDeltaY + if !event.hasPreciseScrollingDeltas { + deltaX *= 16 + deltaY *= 16 + } + let consumed = onMoved(deltaX, deltaY) if consumed { return nil } diff --git a/PlayTools.xcodeproj/project.pbxproj b/PlayTools.xcodeproj/project.pbxproj index eba2ce4f..7bdd955b 100644 --- a/PlayTools.xcodeproj/project.pbxproj +++ b/PlayTools.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 6E7663A528D0FEBE00DE4AF9 /* AKPluginLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7663A428D0FEBE00DE4AF9 /* AKPluginLoader.swift */; }; 6E84A14528D0F94E00BF7495 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA818CBA287ABFD5000BEE9D /* UIKit.framework */; }; 6E84A15028D0F97500BF7495 /* AKInterface.bundle in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E84A14C28D0F96D00BF7495 /* AKInterface.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 951D8275299D097C00D35B20 /* Playtools.strings in Resources */ = {isa = PBXBuildFile; fileRef = 951D8277299D097C00D35B20 /* Playtools.strings */; }; + 95A553E729F2BBB200E34C26 /* PlayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A553E629F2BBB200E34C26 /* PlayController.swift */; }; AA71970B287A44D200623C15 /* PlayLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = AA719702287A44D200623C15 /* PlayLoader.m */; }; AA71970D287A44D200623C15 /* PlaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA719704287A44D200623C15 /* PlaySettings.swift */; }; AA71970E287A44D200623C15 /* PlayLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = AA719705287A44D200623C15 /* PlayLoader.h */; }; @@ -45,6 +47,9 @@ AA719870287A81A000623C15 /* UIEvent+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = AA719842287A81A000623C15 /* UIEvent+Private.h */; }; AA719872287A81A000623C15 /* UITouch+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = AA719844287A81A000623C15 /* UITouch+Private.h */; }; AA818CB9287ABFB1000BEE9D /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA818CB8287ABFB1000BEE9D /* IOKit.framework */; }; + AB7DA47529B85BFB0034ACB2 /* PlayShadow.m in Sources */ = {isa = PBXBuildFile; fileRef = AB7DA47429B85BFB0034ACB2 /* PlayShadow.m */; }; + AB7DA47729B8A78B0034ACB2 /* PlayShadow.h in Headers */ = {isa = PBXBuildFile; fileRef = AB7DA47629B8A78B0034ACB2 /* PlayShadow.h */; }; + ABCECEE629750BA600746595 /* PlayedApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCECEE529750BA600746595 /* PlayedApple.swift */; }; B127172228817AB90025112B /* SwordRPC in Frameworks */ = {isa = PBXBuildFile; productRef = B127172128817AB90025112B /* SwordRPC */; }; B127172528817C040025112B /* DiscordIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B127172428817C040025112B /* DiscordIPC.swift */; }; B1271729288284BE0025112B /* DiscordActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1271728288284BE0025112B /* DiscordActivity.swift */; }; @@ -72,6 +77,9 @@ 6E7663A028D0FB5300DE4AF9 /* AKPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AKPlugin.swift; sourceTree = ""; }; 6E7663A428D0FEBE00DE4AF9 /* AKPluginLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AKPluginLoader.swift; sourceTree = ""; }; 6E84A14C28D0F96D00BF7495 /* AKInterface.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AKInterface.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + 951D8276299D097C00D35B20 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Playtools.strings; sourceTree = ""; }; + 951D8278299D098000D35B20 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Playtools.strings"; sourceTree = ""; }; + 95A553E629F2BBB200E34C26 /* PlayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayController.swift; sourceTree = ""; }; AA7196D8287A447700623C15 /* PlayTools.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PlayTools.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AA719702287A44D200623C15 /* PlayLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayLoader.m; sourceTree = ""; }; AA719704287A44D200623C15 /* PlaySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaySettings.swift; sourceTree = ""; }; @@ -107,6 +115,9 @@ AA719844287A81A000623C15 /* UITouch+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITouch+Private.h"; sourceTree = ""; }; AA818CB8287ABFB1000BEE9D /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/Library/Frameworks/IOKit.framework; sourceTree = DEVELOPER_DIR; }; AA818CBA287ABFD5000BEE9D /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk/System/iOSSupport/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + AB7DA47429B85BFB0034ACB2 /* PlayShadow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlayShadow.m; sourceTree = ""; }; + AB7DA47629B8A78B0034ACB2 /* PlayShadow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlayShadow.h; sourceTree = ""; }; + ABCECEE529750BA600746595 /* PlayedApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayedApple.swift; sourceTree = ""; }; B127172428817C040025112B /* DiscordIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscordIPC.swift; sourceTree = ""; }; B1271728288284BE0025112B /* DiscordActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscordActivity.swift; sourceTree = ""; }; B1E8CF8928BBE2AB004340D3 /* Keymapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keymapping.swift; sourceTree = ""; }; @@ -146,6 +157,7 @@ AA7196CE287A447700623C15 = { isa = PBXGroup; children = ( + 951D8277299D097C00D35B20 /* Playtools.strings */, AA7196DA287A447700623C15 /* PlayTools */, 6E76639928D0FA6F00DE4AF9 /* AKInterface */, AA7196D9287A447700623C15 /* Products */, @@ -165,6 +177,7 @@ AA7196DA287A447700623C15 /* PlayTools */ = { isa = PBXGroup; children = ( + ABCECEE729750BB100746595 /* MysticRunes */, B127172328817AC70025112B /* DiscordActivity */, AA719721287A480C00623C15 /* Controls */, AA719799287A481500623C15 /* Keymap */, @@ -191,6 +204,7 @@ AA719755287A480C00623C15 /* PlayInput.swift */, AA719756287A480C00623C15 /* ControlMode.swift */, AA719757287A480C00623C15 /* MenuController.swift */, + 95A553E629F2BBB200E34C26 /* PlayController.swift */, ); path = Controls; sourceTree = ""; @@ -262,6 +276,16 @@ name = Frameworks; sourceTree = ""; }; + ABCECEE729750BB100746595 /* MysticRunes */ = { + isa = PBXGroup; + children = ( + ABCECEE529750BA600746595 /* PlayedApple.swift */, + AB7DA47429B85BFB0034ACB2 /* PlayShadow.m */, + AB7DA47629B8A78B0034ACB2 /* PlayShadow.h */, + ); + path = MysticRunes; + sourceTree = ""; + }; B127172328817AC70025112B /* DiscordActivity */ = { isa = PBXGroup; children = ( @@ -279,6 +303,7 @@ buildActionMask = 2147483647; files = ( AA71986E287A81A000623C15 /* NSObject+Swizzle.h in Headers */, + AB7DA47729B8A78B0034ACB2 /* PlayShadow.h in Headers */, AA719846287A81A000623C15 /* IOHIDEvent+KIF.h in Headers */, AA719862287A81A000623C15 /* UITouch-KIFAdditions.h in Headers */, AA71986B287A81A000623C15 /* UIApplication+Private.h in Headers */, @@ -358,6 +383,7 @@ knownRegions = ( en, Base, + "zh-Hans", ); mainGroup = AA7196CE287A447700623C15; packageReferences = ( @@ -385,6 +411,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 951D8275299D097C00D35B20 /* Playtools.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -425,6 +452,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 95A553E729F2BBB200E34C26 /* PlayController.swift in Sources */, AA71975A287A480D00623C15 /* Toucher.swift in Sources */, AA7197A1287A481500623C15 /* CircleMenuLoader.swift in Sources */, 6E76639B28D0FAE700DE4AF9 /* Plugin.swift in Sources */, @@ -447,17 +475,32 @@ AA7197A3287A481500623C15 /* CircleMenuButton.swift in Sources */, AA7197A9287A481500623C15 /* PlayInfo.swift in Sources */, AA71986A287A81A000623C15 /* PTFakeMetaTouch.m in Sources */, + ABCECEE629750BA600746595 /* PlayedApple.swift in Sources */, AA71986C287A81A000623C15 /* NSObject+Swizzle.m in Sources */, AA71970F287A44D200623C15 /* PlayCover.swift in Sources */, B127172528817C040025112B /* DiscordIPC.swift in Sources */, AA719850287A81A000623C15 /* UITouch-KIFAdditions.m in Sources */, AA71985F287A81A000623C15 /* IOHIDEvent+KIF.m in Sources */, B1E8CF8A28BBE2AB004340D3 /* Keymapping.swift in Sources */, + AB7DA47529B85BFB0034ACB2 /* PlayShadow.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXVariantGroup section */ + 951D8277299D097C00D35B20 /* Playtools.strings */ = { + isa = PBXVariantGroup; + children = ( + 951D8276299D097C00D35B20 /* en */, + 951D8278299D098000D35B20 /* zh-Hans */, + ); + name = Playtools.strings; + path = PlayTools; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 6E84A14E28D0F96D00BF7495 /* Debug */ = { isa = XCBuildConfiguration; diff --git a/PlayTools/Controls/ControlMode.swift b/PlayTools/Controls/ControlMode.swift index 35661efb..f690406c 100644 --- a/PlayTools/Controls/ControlMode.swift +++ b/PlayTools/Controls/ControlMode.swift @@ -10,33 +10,59 @@ let mode = ControlMode.mode public class ControlMode { static public let mode = ControlMode() - public var visible: Bool = PlaySettings.shared.mouseMapping + public var visible: Bool = true + public var keyboardMapped = true + + public static func trySwap() -> Bool { + if PlayInput.shouldLockCursor { + mode.show(!mode.visible) + return true + } + mode.show(true) + return false + } + + func setMapping(_ mapped: Bool) { + if mapped { + PlayInput.shared.parseKeymap() + } else { + show(true) + PlayInput.shared.invalidate() + } + keyboardMapped = mapped + } func show(_ show: Bool) { - if !editor.editorMode { + if keyboardMapped { if show { if !visible { + NotificationCenter.default.post(name: NSNotification.Name.playtoolsCursorWillShow, + object: nil, userInfo: [:]) if screen.fullscreen { screen.switchDock(true) } - if PlaySettings.shared.mouseMapping { - AKInterface.shared!.unhideCursor() - } - PlayInput.shared.invalidate() + AKInterface.shared!.unhideCursor() } } else { if visible { - if PlaySettings.shared.mouseMapping { - AKInterface.shared!.hideCursor() - } + NotificationCenter.default.post(name: NSNotification.Name.playtoolsCursorWillHide, + object: nil, userInfo: [:]) + AKInterface.shared!.hideCursor() if screen.fullscreen { screen.switchDock(false) } - - PlayInput.shared.setup() } } + Toucher.writeLog(logMessage: "cursor show switched to \(show)") visible = show } } } + +extension NSNotification.Name { + public static let playtoolsCursorWillHide: NSNotification.Name + = NSNotification.Name("playtools.cursorWillHide") + + public static let playtoolsCursorWillShow: NSNotification.Name + = NSNotification.Name("playtools.cursorWillShow") +} diff --git a/PlayTools/Controls/MenuController.swift b/PlayTools/Controls/MenuController.swift index 3ec96d13..1da0786c 100644 --- a/PlayTools/Controls/MenuController.swift +++ b/PlayTools/Controls/MenuController.swift @@ -44,6 +44,13 @@ extension UIApplication { func downscaleElement(_ sender: AnyObject) { EditorController.shared.focusedControl?.resize(down: true) } + + // put a mark in the toucher log, so as to align with tester description + @objc + func markToucherLog(_ sender: AnyObject) { + Toucher.writeLog(logMessage: "mark") + Toast.showHint(title: "Log marked") + } } extension UIViewController { @@ -61,26 +68,66 @@ extension UIViewController { struct CommandsList { static let KeymappingToolbox = "keymapping" } - -var keymapping = ["Open/Close Keymapping Editor", - "Delete selected element", - "Upsize selected element", - "Downsize selected element", - "Rotate display area"] +// have to use a customized name, in case it conflicts with the game's localization file +var keymapping = [ + NSLocalizedString("menu.keymapping.toggleEditor", tableName: "Playtools", + value: "Open/Close Keymapping Editor", comment: ""), + NSLocalizedString("menu.keymapping.deleteElement", tableName: "Playtools", + value: "Delete selected element", comment: ""), + NSLocalizedString("menu.keymapping.upsizeElement", tableName: "Playtools", + value: "Upsize selected element", comment: ""), + NSLocalizedString("menu.keymapping.downsizeElement", tableName: "Playtools", + value: "Downsize selected element", comment: ""), + NSLocalizedString("menu.keymapping.rotateDisplay", tableName: "Playtools", + value: "Rotate display area", comment: "") + ] var keymappingSelectors = [#selector(UIApplication.switchEditorMode(_:)), #selector(UIApplication.removeElement(_:)), #selector(UIApplication.upscaleElement(_:)), #selector(UIApplication.downscaleElement(_:)), - #selector(UIViewController.rotateView(_:))] + #selector(UIViewController.rotateView(_:)) + ] class MenuController { init(with builder: UIMenuBuilder) { + if Toucher.logEnabled { + builder.insertSibling(MenuController.debuggingMenu(), afterMenu: .view) + } builder.insertSibling(MenuController.keymappingMenu(), afterMenu: .view) } - class func keymappingMenu() -> UIMenu { - let keyCommands = [ "K", UIKeyCommand.inputDelete, UIKeyCommand.inputUpArrow, UIKeyCommand.inputDownArrow, "R" ] + static func debuggingMenu() -> UIMenu { + let menuTitle = [ + "Put a mark in toucher log" + ] + let keyCommands = ["L"] + let selectors = [ + #selector(UIApplication.markToucherLog) + ] + let arrowKeyChildrenCommands = zip(keyCommands, menuTitle).map { (command, btn) in + UIKeyCommand(title: btn, + image: nil, + action: selectors[menuTitle.firstIndex(of: btn)!], + input: command, + modifierFlags: .command, + propertyList: [CommandsList.KeymappingToolbox: btn] + ) + } + return UIMenu(title: "Debug", + image: nil, + identifier: .debuggingMenu, + options: [], + children: [ + UIMenu(title: "", + image: nil, + identifier: .debuggingOptionsMenu, + options: .displayInline, + children: arrowKeyChildrenCommands)]) + } + class func keymappingMenu() -> UIMenu { + let keyCommands = [ "K", UIKeyCommand.inputDelete, + UIKeyCommand.inputUpArrow, UIKeyCommand.inputDownArrow, "R", "L"] let arrowKeyChildrenCommands = zip(keyCommands, keymapping).map { (command, btn) in UIKeyCommand(title: btn, image: nil, @@ -97,7 +144,8 @@ class MenuController { options: .displayInline, children: arrowKeyChildrenCommands) - return UIMenu(title: NSLocalizedString("Keymapping", comment: ""), + return UIMenu(title: NSLocalizedString("menu.keymapping", tableName: "Playtools", + value: "Keymapping", comment: ""), image: nil, identifier: .keymappingMenu, options: [], @@ -108,4 +156,6 @@ class MenuController { extension UIMenu.Identifier { static var keymappingMenu: UIMenu.Identifier { UIMenu.Identifier("io.playcover.PlayTools.menus.editor") } static var keymappingOptionsMenu: UIMenu.Identifier { UIMenu.Identifier("io.playcover.PlayTools.menus.keymapping") } + static var debuggingMenu: UIMenu.Identifier { UIMenu.Identifier("io.playcover.PlayTools.menus.debug") } + static var debuggingOptionsMenu: UIMenu.Identifier { UIMenu.Identifier("io.playcover.PlayTools.menus.debugging") } } diff --git a/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m b/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m index e6614c6c..9733eba3 100644 --- a/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m +++ b/PlayTools/Controls/PTFakeTouch/NSObject+Swizzle.m @@ -11,7 +11,6 @@ #import "UIKit/UIKit.h" #import #import "PTFakeMetaTouch.h" -#import __attribute__((visibility("hidden"))) @interface PTSwizzleLoader : NSObject @@ -94,11 +93,13 @@ - (long long) hook_orientation { } - (double) hook_nativeScale { - return 2.0; + return [[PlaySettings shared] customScaler]; } - (double) hook_scale { - return 2.0; + // Return rounded value of [[PlaySettings shared] customScaler] + // Even though it is a double return, this will only accept .0 value or apps will crash + return round([[PlaySettings shared] customScaler]); } - (double) get_default_height { @@ -111,11 +112,6 @@ - (double) get_default_width { } -- (void) hook_setCurrentSubscription:(VSSubscription *)currentSubscription { - // do nothing -} - - bool menuWasCreated = false; - (id) initWithRootMenuHook:(id)rootMenu { self = [self initWithRootMenuHook:rootMenu]; @@ -183,21 +179,26 @@ + (void)load { } } else { - CGFloat newValueW = (CGFloat) [self get_default_width]; - [[PlaySettings shared] setValue:@(newValueW) forKey:@"windowSizeWidth"]; - - CGFloat newValueH = (CGFloat)[self get_default_height]; - [[PlaySettings shared] setValue:@(newValueH) forKey:@"windowSizeHeight"]; - if (![[PlaySettings shared] inverseScreenValues]) { - [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frameDefault)]; - [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(bounds) withMethod:@selector(hook_boundsDefault)]; - [objc_getClass("FBSDisplayMode") swizzleInstanceMethod:@selector(size) withMethod:@selector(hook_sizeDelfault)]; + if ([[PlaySettings shared] windowFixMethod] == 1) { + // do nothing:tm: + } + else { + CGFloat newValueW = (CGFloat) [self get_default_width]; + [[PlaySettings shared] setValue:@(newValueW) forKey:@"windowSizeWidth"]; + + CGFloat newValueH = (CGFloat)[self get_default_height]; + [[PlaySettings shared] setValue:@(newValueH) forKey:@"windowSizeHeight"]; + if (![[PlaySettings shared] inverseScreenValues]) { + [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(frame) withMethod:@selector(hook_frameDefault)]; + [objc_getClass("FBSSceneSettings") swizzleInstanceMethod:@selector(bounds) withMethod:@selector(hook_boundsDefault)]; + [objc_getClass("FBSDisplayMode") swizzleInstanceMethod:@selector(size) withMethod:@selector(hook_sizeDelfault)]; + } + [objc_getClass("UIDevice") swizzleInstanceMethod:@selector(orientation) withMethod:@selector(hook_orientation)]; + [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(nativeBounds) withMethod:@selector(hook_nativeBoundsDefault)]; + + [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(nativeScale) withMethod:@selector(hook_nativeScale)]; + [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(scale) withMethod:@selector(hook_scale)]; } - [objc_getClass("UIDevice") swizzleInstanceMethod:@selector(orientation) withMethod:@selector(hook_orientation)]; - [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(nativeBounds) withMethod:@selector(hook_nativeBoundsDefault)]; - - [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(nativeScale) withMethod:@selector(hook_nativeScale)]; - [objc_getClass("UIScreen") swizzleInstanceMethod:@selector(scale) withMethod:@selector(hook_scale)]; } } else { @@ -210,8 +211,6 @@ + (void)load { [objc_getClass("_UIMenuBuilder") swizzleInstanceMethod:sel_getUid("initWithRootMenu:") withMethod:@selector(initWithRootMenuHook:)]; [objc_getClass("IOSViewController") swizzleInstanceMethod:@selector(prefersPointerLocked) withMethod:@selector(hook_prefersPointerLocked)]; - - [objc_getClass("VSSubscriptionRegistrationCenter") swizzleInstanceMethod:@selector(setCurrentSubscription:) withMethod:@selector(hook_setCurrentSubscription:)]; } @end diff --git a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m index cfcd3c5b..b3e76150 100644 --- a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m +++ b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m @@ -65,7 +65,7 @@ + (NSInteger)fakeTouchId: (NSInteger)pointId AtPoint: (CGPoint)point withTouchPh touch = toStationarify; toStationarify = NULL; if(touch.phase == UITouchPhaseBegan) { - [touch setPhaseAndUpdateTimestamp:UITouchPhaseStationary]; + [touch setPhaseAndUpdateTimestamp:UITouchPhaseMoved]; } } // respect the semantics of touch phase, allocate new touch on touch began. diff --git a/PlayTools/Controls/PlayAction.swift b/PlayTools/Controls/PlayAction.swift index a346578e..cc49f5d7 100644 --- a/PlayTools/Controls/PlayAction.swift +++ b/PlayTools/Controls/PlayAction.swift @@ -4,73 +4,33 @@ // import Foundation -import GameController protocol Action { func invalidate() } -extension GCKeyboard { - static func pressed(key: GCKeyCode) -> Bool { - return GCKeyboard.coalesced?.keyboardInput?.button(forKeyCode: key)?.isPressed ?? false - } -} - class ButtonAction: Action { func invalidate() { Toucher.touchcam(point: point, phase: UITouch.Phase.ended, tid: &id) - if let gcKey = GCKeyboard.coalesced?.keyboardInput?.button(forKeyCode: keyCode) { - gcKey.pressedChangedHandler = nil - - } else if let gcControllerElement = GCController.current?.extendedGamepad?.elements[keyName] { - - if let gcControllerButton = gcControllerElement as? GCControllerButtonInput { - gcControllerButton.pressedChangedHandler = nil - } - - } } - let keyCode: GCKeyCode + let keyCode: Int let keyName: String let point: CGPoint var id: Int? - private func getChangedHandler(handler: ((T1, Float, Bool) -> Void)?) -> (T1, Float, Bool) -> Void { - return { button, value, pressed in - if !PlayInput.cmdPressed() { - self.update(pressed: pressed) - } - if let previous = handler { - previous(button, value, pressed) - } - } - } - - init(keyCode: GCKeyCode, keyName: String, point: CGPoint) { + init(keyCode: Int, keyName: String, point: CGPoint) { self.keyCode = keyCode self.keyName = keyName self.point = point - if PlayMice.shared.setMiceButtons(keyCode.rawValue, action: self) { - // No more work to do for mouse buttons - } else if let gcKey = GCKeyboard.coalesced!.keyboardInput!.button(forKeyCode: keyCode) { - let handler = gcKey.pressedChangedHandler - gcKey.pressedChangedHandler = getChangedHandler(handler: handler) - - } else if let gcControllerElement = GCController.current?.extendedGamepad?.elements[keyName] { - - if let gcControllerButton = gcControllerElement as? GCControllerButtonInput { - let handler = gcControllerButton.pressedChangedHandler - gcControllerButton.pressedChangedHandler = getChangedHandler(handler: handler) - } - - } else { - Toast.showOver(msg: "failed to map button at point \(point)") - } + let code = keyCode + let codeName = KeyCodeNames.keyCodes[code] ?? "Btn" + // TODO: set both key names in draggable button, so as to depracate key code + PlayInput.registerButton(key: code == KeyCodeNames.defaultCode ? keyName: codeName, handler: self.update) } convenience init(data: Button) { - let keyCode = GCKeyCode(rawValue: data.keyCode) + let keyCode = data.keyCode self.init( keyCode: keyCode, keyName: data.keyName, @@ -91,26 +51,26 @@ class ButtonAction: Action { class DraggableButtonAction: ButtonAction { var releasePoint: CGPoint - override init(keyCode: GCKeyCode, keyName: String, point: CGPoint) { + override init(keyCode: Int, keyName: String, point: CGPoint) { self.releasePoint = point super.init(keyCode: keyCode, keyName: keyName, point: point) - _ = PlayMice.shared.setupThumbstickChangedHandler(name: keyName) } override func update(pressed: Bool) { if pressed { Toucher.touchcam(point: point, phase: UITouch.Phase.began, tid: &id) self.releasePoint = point - PlayMice.shared.draggableHandler[keyName] = self.onMouseMoved + PlayInput.draggableHandler[keyName] = self.onMouseMoved + AKInterface.shared!.hideCursor() } else { - PlayMice.shared.draggableHandler.removeValue(forKey: keyName) + PlayInput.draggableHandler.removeValue(forKey: keyName) Toucher.touchcam(point: releasePoint, phase: UITouch.Phase.ended, tid: &id) + AKInterface.shared!.unhideCursor() } } override func invalidate() { - PlayMice.shared.draggableHandler.removeValue(forKey: keyName) - PlayMice.shared.stop() + PlayInput.draggableHandler.removeValue(forKey: keyName) super.invalidate() } @@ -136,10 +96,10 @@ class ContinuousJoystickAction: Action { self.key = data.keyName position = center self.sensitivity = data.transform.size.absoluteSize / 4 - if PlayMice.shared.setupThumbstickChangedHandler(name: key) { - PlayMice.shared.joystickHandler[key] = thumbstickUpdate + if key == PlayMice.elementName { + PlayInput.joystickHandler[key] = self.mouseUpdate } else { - PlayMice.shared.joystickHandler[key] = mouseUpdate + PlayInput.joystickHandler[key] = self.thumbstickUpdate } } @@ -171,41 +131,36 @@ class ContinuousJoystickAction: Action { } func invalidate() { - PlayMice.shared.joystickHandler.removeValue(forKey: key) + PlayInput.joystickHandler.removeValue(forKey: key) } } class JoystickAction: Action { - let keys: [GCKeyCode] + let keys: [Int] let center: CGPoint + var touch: CGPoint let shift: CGFloat var id: Int? - var moving = false - - init(keys: [GCKeyCode], center: CGPoint, shift: CGFloat) { + private var keyPressed = [Bool](repeating: false, count: 4) + init(keys: [Int], center: CGPoint, shift: CGFloat) { self.keys = keys self.center = center - self.shift = shift / 2 - if let keyboard = GCKeyboard.coalesced?.keyboardInput { - for key in keys { - let handler = keyboard.button(forKeyCode: key)?.pressedChangedHandler - keyboard.button(forKeyCode: key)?.pressedChangedHandler = { button, value, pressed in - Toucher.touchQueue.async(execute: self.update) - if let previous = handler { - previous(button, value, pressed) - } - } - } + self.touch = center + self.shift = shift / 4 + for index in 0.. (Bool) -> Void { + // if the size of joystick is large, set control type to free, otherwise fixed. + // this is a temporary method. ideally should give the user an option. + if shift < 200 { + return { pressed in + self.updateTouch(index: index, pressed: pressed) + self.handleFixed() + } + } else { + return { pressed in + self.updateTouch(index: index, pressed: pressed) + self.handleFree() } } } - func update() { - if mode.visible { - return - } - var touch = center - var start = center - if GCKeyboard.pressed(key: keys[0]) { - touch.y -= shift / 3 - } else if GCKeyboard.pressed(key: keys[1]) { - touch.y += shift / 3 - } - if GCKeyboard.pressed(key: keys[2]) { - touch.x -= shift / 3 - } else if GCKeyboard.pressed(key: keys[3]) { - touch.x += shift / 3 + func updateTouch(index: Int, pressed: Bool) { + self.keyPressed[index] = pressed + let isPlus = index & 1 != 0 + let realShift = isPlus ? shift : -shift + if index > 1 { + if pressed { + touch.x = center.x + realShift + } else if self.keyPressed[index ^ 1] { + touch.x = center.x - realShift + } else { + touch.x = center.x + } + } else { + if pressed { + touch.y = center.y + realShift + } else if self.keyPressed[index ^ 1] { + touch.y = center.y - realShift + } else { + touch.y = center.y + } } - if moving { - if touch.equalTo(center) { - moving = false + } + + func atCenter() -> Bool { + return (center.x - touch.x).magnitude + (center.y - touch.y).magnitude < 8 + } + + func handleCommon(_ begin: () -> Void) { + let moving = id != nil + if atCenter() { + if moving { Toucher.touchcam(point: touch, phase: UITouch.Phase.ended, tid: &id) - } else { - Toucher.touchcam(point: touch, phase: UITouch.Phase.moved, tid: &id) } } else { - if !touch.equalTo(center) { - start.x += (touch.x - start.x) / 8 - start.y += (touch.y - start.y) / 8 - moving = true - Toucher.touchcam(point: start, phase: UITouch.Phase.began, tid: &id) - Toucher.touchQueue.asyncAfter(deadline: .now() + 0.04) { - if self.moving { - Toucher.touchcam(point: touch, phase: UITouch.Phase.moved, tid: &self.id) - } // end if - } // end closure - } // end if - } // end else + if moving { + Toucher.touchcam(point: touch, phase: UITouch.Phase.moved, tid: &id) + } else { + begin() + } + } + } + + func handleFree() { + handleCommon { + Toucher.touchcam(point: self.center, phase: UITouch.Phase.began, tid: &id) + PlayInput.touchQueue.asyncAfter(deadline: .now() + 0.04, qos: .userInitiated) { + if self.id == nil { + return + } + Toucher.touchcam(point: self.touch, phase: UITouch.Phase.moved, tid: &self.id) + } // end closure + } + } + + func handleFixed() { + handleCommon { + Toucher.touchcam(point: self.touch, phase: UITouch.Phase.began, tid: &id) + } } } diff --git a/PlayTools/Controls/PlayController.swift b/PlayTools/Controls/PlayController.swift new file mode 100644 index 00000000..64c641ec --- /dev/null +++ b/PlayTools/Controls/PlayController.swift @@ -0,0 +1,121 @@ +// +// PlayController.swift +// PlayTools +// +// Created by 许沂聪 on 2023/4/21. +// + +import Foundation +import GameController + +class PlayController { + private static var directionPadXValue: Float = 0, + directionPadYValue: Float = 0, + thumbstickCursorControl: [String: (((CGFloat, CGFloat) -> Void)?, CGFloat, CGFloat) -> Void] + = ["Left Thumbstick": ThumbstickCursorControl().update, "Right Thumbstick": ThumbstickCursorControl().update] + + public static func initialize() { + GCController.current?.extendedGamepad?.valueChangedHandler = handleEvent + } + + static func handleEditorEvent(_ profile: GCExtendedGamepad, _ element: GCControllerElement) { + // This is the index of controller buttons, which is String, not Int + var alias: String = element.aliases.first! + if alias == "Direction Pad" { + guard let dpadElement = element as? GCControllerDirectionPad else { + Toast.showOver(msg: "cannot map direction pad: element type not recognizable") + return + } + if dpadElement.xAxis.value > 0 { + alias = dpadElement.right.aliases.first! + } else if dpadElement.xAxis.value < 0 { + alias = dpadElement.left.aliases.first! + } + if dpadElement.yAxis.value > 0 { + alias = dpadElement.down.aliases.first! + } else if dpadElement.yAxis.value < 0 { + alias = dpadElement.up.aliases.first! + } + } + EditorController.shared.setKey(alias) + } + + static func handleEvent(_ profile: GCExtendedGamepad, _ element: GCControllerElement) { + let name: String = element.aliases.first! + if let buttonElement = element as? GCControllerButtonInput { + guard let handlers = PlayInput.buttonHandlers[name] else { return } +// Toast.showOver(msg: name + ": \(buttonElement.isPressed)") + for handler in handlers { + handler(buttonElement.isPressed) + } + } else if let dpadElement = element as? GCControllerDirectionPad { + PlayController.handleDirectionPad(profile, dpadElement) + } else { + Toast.showOver(msg: "unrecognised controller element input happens") + } + } + public static func handleDirectionPad(_ profile: GCExtendedGamepad, _ dpad: GCControllerDirectionPad) { + let name = dpad.aliases.first! + let xAxis = dpad.xAxis, yAxis = dpad.yAxis + if name == "Direction Pad" { + if (xAxis.value > 0) != (directionPadXValue > 0) { + PlayController.handleEvent(profile, dpad.right) + } + if (xAxis.value < 0) != (directionPadXValue < 0) { + PlayController.handleEvent(profile, dpad.left) + } + if (yAxis.value > 0) != (directionPadYValue > 0) { + PlayController.handleEvent(profile, dpad.up) + } + if (yAxis.value < 0) != (directionPadYValue < 0) { + PlayController.handleEvent(profile, dpad.down) + } + directionPadXValue = xAxis.value + directionPadYValue = yAxis.value + return + } + let deltaX = xAxis.value, deltaY = yAxis.value + let cgDx = CGFloat(deltaX) + let cgDy = CGFloat(deltaY) + thumbstickCursorControl[name]!( + PlayInput.draggableHandler[name] ?? PlayInput.cameraMoveHandler[name], cgDx * 6, cgDy * 6) + PlayInput.joystickHandler[name]?(cgDx, cgDy) + } +} + +class ThumbstickCursorControl { + private var thumbstickVelocity: CGVector = CGVector.zero, + thumbstickPolling: Bool = false, + eventHandler: ((CGFloat, CGFloat) -> Void)! + + static private func isVectorSignificant(_ vector: CGVector) -> Bool { + return vector.dx.magnitude + vector.dy.magnitude > 0.2 + } + + public func update(handler: ((CGFloat, CGFloat) -> Void)?, velocityX: CGFloat, velocityY: CGFloat) { + guard let hdlr = handler else { + if thumbstickPolling { + self.thumbstickVelocity.dx = 0 + self.thumbstickVelocity.dy = 0 + } + return + } + self.eventHandler = hdlr + self.thumbstickVelocity.dx = velocityX + self.thumbstickVelocity.dy = velocityY + if !thumbstickPolling { + PlayInput.touchQueue.async(execute: self.thumbstickPoll) + self.thumbstickPolling = true + } + } + + private func thumbstickPoll() { + if !ThumbstickCursorControl.isVectorSignificant(self.thumbstickVelocity) { + self.thumbstickPolling = false + return + } + self.eventHandler(self.thumbstickVelocity.dx, self.thumbstickVelocity.dy) + PlayInput.touchQueue.asyncAfter( + deadline: DispatchTime.now() + 0.017, execute: self.thumbstickPoll) + } +} diff --git a/PlayTools/Controls/PlayInput.swift b/PlayTools/Controls/PlayInput.swift index 0698c746..d9866252 100644 --- a/PlayTools/Controls/PlayInput.swift +++ b/PlayTools/Controls/PlayInput.swift @@ -5,20 +5,36 @@ import UIKit class PlayInput { static let shared = PlayInput() var actions = [Action]() - var timeoutForBind = true + static var shouldLockCursor = true - static private var lCmdPressed = false - static private var rCmdPressed = false + static var touchQueue = DispatchQueue.init(label: "playcover.toucher", qos: .userInteractive, + autoreleaseFrequency: .workItem) + static public var buttonHandlers: [String: [(Bool) -> Void]] = [:], + draggableHandler: [String: (CGFloat, CGFloat) -> Void] = [:], + cameraMoveHandler: [String: (CGFloat, CGFloat) -> Void] = [:], + cameraScaleHandler: [String: (CGFloat, CGFloat) -> Void] = [:], + joystickHandler: [String: (CGFloat, CGFloat) -> Void] = [:] func invalidate() { - PlayMice.shared.stop() for action in self.actions { action.invalidate() } } + static public func registerButton(key: String, handler: @escaping (Bool) -> Void) { + if "LMB" == key { + PlayInput.shouldLockCursor = true + } + if PlayInput.buttonHandlers[key] == nil { + PlayInput.buttonHandlers[key] = [] + } + PlayInput.buttonHandlers[key]!.append(handler) + } + func parseKeymap() { - actions = [] + actions = [PlayMice.shared] + PlayInput.buttonHandlers.removeAll(keepingCapacity: true) + PlayInput.shouldLockCursor = false for button in keymap.keymapData.buttonModels { actions.append(ButtonAction(data: button)) } @@ -28,9 +44,7 @@ class PlayInput { } for mouse in keymap.keymapData.mouseAreaModel { - if mouse.keyName.hasSuffix("tick") || settings.mouseMapping { - actions.append(CameraAction(data: mouse)) - } + actions.append(CameraAction(data: mouse)) } for joystick in keymap.keymapData.joystickModel { @@ -41,59 +55,79 @@ class PlayInput { actions.append(JoystickAction(data: joystick)) } } + if !PlayInput.shouldLockCursor { + PlayInput.shouldLockCursor = PlayMice.shared.mouseMovementMapped() + } } public func toggleEditor(show: Bool) { - mode.show(show) + mode.setMapping(!show) + Toucher.writeLog(logMessage: "editor opened? \(show)") if show { + self.invalidate() if let keyboard = GCKeyboard.coalesced!.keyboardInput { - keyboard.keyChangedHandler = { _, _, keyCode, _ in - if !PlayInput.cmdPressed() - && !PlayInput.FORBIDDEN.contains(keyCode) - && self.isSafeToBind(keyboard) { - EditorController.shared.setKey(keyCode.rawValue) - } + keyboard.keyChangedHandler = { _, _, keyCode, pressed in + PlayKeyboard.handleEditorEvent(keyCode: keyCode, pressed: pressed) } } if let controller = GCController.current?.extendedGamepad { - controller.valueChangedHandler = { _, element in - // This is the index of controller buttons, which is String, not Int - let alias: String! = element.aliases.first - EditorController.shared.setKey(alias) - } + controller.valueChangedHandler = PlayController.handleEditorEvent } } else { - GCKeyboard.coalesced!.keyboardInput!.keyChangedHandler = nil - GCController.current?.extendedGamepad?.valueChangedHandler = nil + GCKeyboard.coalesced?.keyboardInput?.keyChangedHandler = nil + PlayController.initialize() + parseKeymap() + _ = ControlMode.trySwap() } } - func setup() { - parseKeymap() + static public func cmdPressed() -> Bool { + return AKInterface.shared!.cmdPressed + } + + func initialize() { + if !PlaySettings.shared.keymapping { + return + } - for mouse in GCMouse.mice() { - if settings.mouseMapping { - mouse.mouseInput?.mouseMovedHandler = PlayMice.shared.handleMouseMoved + let centre = NotificationCenter.default + let main = OperationQueue.main + + centre.addObserver(forName: NSNotification.Name.GCControllerDidConnect, object: nil, queue: main) { _ in + if EditorController.shared.editorMode { + self.toggleEditor(show: true) } else { - mouse.mouseInput?.mouseMovedHandler = PlayMice.shared.handleFakeMouseMoved + PlayController.initialize() + } + } + parseKeymap() + centre.addObserver(forName: NSNotification.Name(rawValue: "NSWindowDidBecomeKeyNotification"), object: nil, + queue: main) { _ in + if !mode.visible { + AKInterface.shared!.warpCursor() } } + DispatchQueue.main.asyncAfter(deadline: .now() + 5, qos: .utility) { + if !mode.visible || self.actions.count <= 0 || !PlayInput.shouldLockCursor { + return + } + Toast.initialize() + } + PlayKeyboard.initialize() + PlayMice.shared.initialize() } +} - static public func cmdPressed() -> Bool { - return lCmdPressed || rCmdPressed +class PlayKeyboard { + public static func handleEditorEvent(keyCode: GCKeyCode, pressed: Bool) { + if !PlayInput.cmdPressed() + && !PlayKeyboard.FORBIDDEN.contains(keyCode) + && KeyCodeNames.keyCodes[keyCode.rawValue] != nil { + EditorController.shared.setKey(keyCode.rawValue) + } } - private func isSafeToBind(_ input: GCKeyboardInput) -> Bool { - var result = true - for forbidden in PlayInput.FORBIDDEN where input.button(forKeyCode: forbidden)?.isPressed ?? false { - result = false - break - } - return result - } - private static let FORBIDDEN: [GCKeyCode] = [ .leftGUI, .rightGUI, @@ -102,74 +136,43 @@ class PlayInput { .printScreen ] - private func swapMode(_ pressed: Bool) { - if !settings.mouseMapping { - return + static func handleEvent(_ keyCode: UInt16, _ pressed: Bool) -> Bool { + let name = KeyCodeNames.virtualCodes[keyCode] ?? "Btn" + guard let handlers = PlayInput.buttonHandlers[name] else { + return false } - if pressed { - if !mode.visible { - self.invalidate() - } - mode.show(!mode.visible) + var mapped = false + for handler in handlers { + PlayInput.touchQueue.async(qos: .userInteractive, execute: { + handler(pressed) + }) + mapped = true } + return mapped } - var root: UIViewController? { - return screen.window?.rootViewController - } - - func setupShortcuts() { - if let keyboard = GCKeyboard.coalesced?.keyboardInput { - keyboard.button(forKeyCode: .leftGUI)?.pressedChangedHandler = { _, _, pressed in - PlayInput.lCmdPressed = pressed - } - keyboard.button(forKeyCode: .rightGUI)?.pressedChangedHandler = { _, _, pressed in - PlayInput.rCmdPressed = pressed - } - keyboard.button(forKeyCode: .leftAlt)?.pressedChangedHandler = { _, _, pressed in - self.swapMode(pressed) - } - keyboard.button(forKeyCode: .rightAlt)?.pressedChangedHandler = { _, _, pressed in - self.swapMode(pressed) - } - } - } - - func initialize() { - if !PlaySettings.shared.keymapping { - return - } - + public static func initialize() { let centre = NotificationCenter.default let main = OperationQueue.main - - centre.addObserver(forName: NSNotification.Name.GCKeyboardDidConnect, object: nil, queue: main) { _ in - self.setupShortcuts() - if !mode.visible { - self.setup() - } + centre.addObserver(forName: UIApplication.keyboardDidHideNotification, object: nil, queue: main) { _ in + mode.setMapping(true) + Toucher.writeLog(logMessage: "virtual keyboard did hide") } - - centre.addObserver(forName: NSNotification.Name.GCMouseDidConnect, object: nil, queue: main) { _ in - if !mode.visible { - self.setup() - } + centre.addObserver(forName: UIApplication.keyboardWillShowNotification, object: nil, queue: main) { _ in + mode.setMapping(false) + Toucher.writeLog(logMessage: "virtual keyboard will show") } - - centre.addObserver(forName: NSNotification.Name.GCControllerDidConnect, object: nil, queue: main) { _ in - if !mode.visible { - self.setup() + AKInterface.shared!.setupKeyboard(keyboard: {keycode, pressed, isRepeat in + if !mode.keyboardMapped { + // explicitly ignore repeated Enter key + return isRepeat && keycode == 36 } - } - - setupShortcuts() - - // Fix beep sound - AKInterface.shared! - .eliminateRedundantKeyPressEvents(self.dontIgnore) - } - - func dontIgnore() -> Bool { - (mode.visible && !EditorController.shared.editorMode) || PlayInput.cmdPressed() + if isRepeat { + return true + } + let mapped = PlayKeyboard.handleEvent(keycode, pressed) + return mapped + }, + swapMode: ControlMode.trySwap) } } diff --git a/PlayTools/Controls/PlayMice.swift b/PlayTools/Controls/PlayMice.swift index 5fec34b6..487090db 100644 --- a/PlayTools/Controls/PlayMice.swift +++ b/PlayTools/Controls/PlayMice.swift @@ -4,40 +4,53 @@ // import Foundation -import GameController -public class PlayMice { +public class PlayMice: Action { public static let shared = PlayMice() public static let elementName = "Mouse" - private static var isInit = false - private var acceptMouseEvents = !PlaySettings.shared.mouseMapping - - public init() { - if !PlayMice.isInit { - setupMouseButton(_up: 2, _down: 4) - setupMouseButton(_up: 8, _down: 16) - setupMouseButton(_up: 33554432, _down: 67108864) - if !acceptMouseEvents { - setupScrollWheelHandler() + public func initialize() { + setupLeftButton() + setupMouseButton(right: true) + setupMouseButton(right: false) + AKInterface.shared!.setupMouseMoved(mouseMoved: {deltaX, deltaY in + // this closure's return value only takes effect when any mouse button pressed + if !mode.keyboardMapped { + return false } - PlayMice.isInit = true - } + PlayInput.touchQueue.async(qos: .userInteractive, execute: { + self.handleMouseMoved(deltaX: deltaX, deltaY: deltaY) + }) + return true + }) + AKInterface.shared!.setupScrollWheel({deltaX, deltaY in + if let cameraScale = PlayInput.cameraScaleHandler[PlayMice.elementName] { + cameraScale(deltaX, deltaY) + let eventConsumed = !mode.visible + return eventConsumed + } + return false + }) } var fakedMouseTouchPointId: Int? var fakedMousePressed: Bool {fakedMouseTouchPointId != nil} - private var thumbstickVelocity: CGVector = CGVector.zero - public var draggableHandler: [String: (CGFloat, CGFloat) -> Void] = [:], - cameraMoveHandler: [String: (CGFloat, CGFloat) -> Void] = [:], - cameraScaleHandler: [String: (CGFloat, CGFloat) -> Void] = [:], - joystickHandler: [String: (CGFloat, CGFloat) -> Void] = [:] - public func cursorPos() -> CGPoint { - var point = CGPoint(x: 0, y: 0) - point = AKInterface.shared!.mousePoint + public func mouseMovementMapped() -> Bool { + for handler in [PlayInput.cameraMoveHandler, PlayInput.joystickHandler] + where handler[PlayMice.elementName] != nil { + return true + } + return false + } + + public func cursorPos() -> CGPoint? { + var point = AKInterface.shared!.mousePoint let rect = AKInterface.shared!.windowFrame + if rect.width < 1 || rect.height < 1 { + return nil + } let viewRect: CGRect = screen.screenRect let widthRate = viewRect.width / rect.width var rate = viewRect.height / rect.height @@ -45,179 +58,119 @@ public class PlayMice { // Keep aspect ratio rate = widthRate } - // Horizontally in center - point.x -= (rect.width - viewRect.width / rate)/2 - point.x *= rate if screen.fullscreen { // Vertically in center point.y -= (rect.height - viewRect.height / rate)/2 } point.y *= rate point.y = viewRect.height - point.y - + // For traffic light buttons when not fullscreen + if point.y < 0 { + return nil + } + // Horizontally in center + point.x -= (rect.width - viewRect.width / rate)/2 + point.x *= rate return point } - static private func isVectorSignificant(_ vector: CGVector) -> Bool { - return vector.dx.magnitude + vector.dy.magnitude > 0.2 - } - - public func setupScrollWheelHandler() { - AKInterface.shared!.setupScrollWheel({deltaX, deltaY in - if let cameraScale = self.cameraScaleHandler[PlayMice.elementName] { - cameraScale(deltaX, deltaY) - let eventConsumed = !mode.visible - return eventConsumed - } - return false - }) - } - - public func setupThumbstickChangedHandler(name: String) -> Bool { - if let thumbstick = GCController.current?.extendedGamepad?.elements[name] as? GCControllerDirectionPad { - thumbstick.valueChangedHandler = { _, deltaX, deltaY in - if !PlayMice.isVectorSignificant(self.thumbstickVelocity) { - if let closure = self.thumbstickPoll(name) { - DispatchQueue.main.async(execute: closure) - } - } - self.thumbstickVelocity.dx = CGFloat(deltaX * 6) - self.thumbstickVelocity.dy = CGFloat(deltaY * 6) -// Toast.showOver(msg: "thumbstick") - if let joystickUpdate = self.joystickHandler[name] { - joystickUpdate(self.thumbstickVelocity.dx, self.thumbstickVelocity.dy) + public func handleMouseMoved(deltaX: CGFloat, deltaY: CGFloat) { + let sensy = CGFloat(PlaySettings.shared.sensitivity * 0.6) + let cgDx = deltaX * sensy, + cgDy = -deltaY * sensy + let name = PlayMice.elementName + if let draggableUpdate = PlayInput.draggableHandler[name] { + draggableUpdate(cgDx, cgDy) + } else if mode.visible { + if self.fakedMousePressed { + if let pos = self.cursorPos() { + Toucher.touchcam(point: pos, phase: UITouch.Phase.moved, tid: &fakedMouseTouchPointId) } } - return true + } else { + PlayInput.cameraMoveHandler[name]?(cgDx, cgDy) + PlayInput.joystickHandler[name]?(cgDx, cgDy) } - return false } - private func thumbstickPoll(_ name: String) -> (() -> Void)? { -// DispatchQueue.main.async { -// Toast.showOver(msg: "polling") -// } - let draggableUpdate = self.draggableHandler[name] - let cameraUpdate = self.cameraMoveHandler[name] - if draggableUpdate == nil && cameraUpdate == nil { - return nil + private func setupMouseButton(right: Bool) { + let keyCode = right ? -2 : -3 + guard let keyName = KeyCodeNames.keyCodes[keyCode] else { + Toast.showHint(title: "Failed initializing \(right ? "right" : "other") mouse button input") + return } - return { - if PlayMice.isVectorSignificant(self.thumbstickVelocity) { - var captured = false - if let draggableUpdate = self.draggableHandler[name] { - draggableUpdate(self.thumbstickVelocity.dx, self.thumbstickVelocity.dy) - captured = true - } - if !captured { - if let cameraUpdate = self.cameraMoveHandler[name] { - cameraUpdate(self.thumbstickVelocity.dx, self.thumbstickVelocity.dy) - } + AKInterface.shared!.setupMouseButton(left: false, right: right) {pressed in + if mode.keyboardMapped { // if mapping + if let handlers = PlayInput.buttonHandlers[keyName] { + PlayInput.touchQueue.async(qos: .userInteractive, execute: { + for handler in handlers { + handler(pressed) + } + }) + // if mapped to any button, consumed and dispatch + return false } - if let closure = self.thumbstickPoll(name) { - DispatchQueue.main.asyncAfter( - deadline: DispatchTime.now() + 0.017, execute: closure) + // if not mapped, transpass to app + return true + } else if EditorController.shared.editorMode { // if editor is open, consumed and set button + if pressed { + // asynced to return quickly. this branch contains UI operation so main queue. + // main queue is fine. should not be slower than keyboard + DispatchQueue.main.async(qos: .userInteractive, execute: { + EditorController.shared.setKey(keyCode) + Toucher.writeLog(logMessage: "mouse button editor set") + }) } + return false + } else { // if typing, transpass event to app + Toucher.writeLog(logMessage: "mouse button pressed? \(pressed)") + return true } } } - - public func handleFakeMouseMoved(_: GCMouseInput, deltaX: Float, deltaY: Float) { - if self.fakedMousePressed { - Toucher.touchcam(point: self.cursorPos(), phase: UITouch.Phase.moved, tid: &fakedMouseTouchPointId) - } - } - - public func handleMouseMoved(_: GCMouseInput, deltaX: Float, deltaY: Float) { - let sensy = CGFloat(PlaySettings.shared.sensitivity) - let cgDx = CGFloat(deltaX) * sensy, - cgDy = CGFloat(deltaY) * sensy - let name = PlayMice.elementName - if let draggableUpdate = self.draggableHandler[name] { - draggableUpdate(cgDx, cgDy) - return - } - self.cameraMoveHandler[name]?(cgDx, cgDy) - self.joystickHandler[name]?(cgDx, cgDy) - } - - public func stop() { - mouseActions.keys.forEach { key in - mouseActions[key] = [] - } - for mouse in GCMouse.mice() { - mouse.mouseInput?.mouseMovedHandler = { _, _, _ in} - } - } - - func setMiceButtons(_ keyId: Int, action: ButtonAction) -> Bool { - if (-3 ... -1).contains(keyId) { - setMiceButton(keyId, action: action) - return true - } - return false - } - - var mouseActions: [Int: [ButtonAction]] = [2: [], 8: [], 33554432: []] - - private func setupMouseButton(_up: Int, _down: Int) { - AKInterface.shared!.setupMouseButton(_up, _down, dontIgnore(_:_:_:)) - } - - private func dontIgnore(_ actionIndex: Int, _ state: Bool, _ isEventWindow: Bool) -> Bool { - if EditorController.shared.editorMode { - if state { - if actionIndex == 8 { - EditorController.shared.setKey(-2) - } else if actionIndex == 33554432 { - EditorController.shared.setKey(-3) - } + // using high priority event handlers to prevent lag and stutter in demanding games + // but no free lunch. high priority handlers cannot execute for too long + // exceeding the time limit causes even more lag + private func setupLeftButton() { + AKInterface.shared!.setupMouseButton(left: true, right: false) {pressed in + if !mode.keyboardMapped { + Toucher.writeLog(logMessage: "left button pressed? \(pressed)") + return true } - return true - } - if self.acceptMouseEvents { - let curPos = self.cursorPos() - if state { - if !self.fakedMousePressed - // For traffic light buttons when not fullscreen - && curPos.y > 0 - // For traffic light buttons when fullscreen - && isEventWindow { + guard let curPos = self.cursorPos() else { return true } + PlayInput.touchQueue.async(qos: .userInteractive, execute: { + // considering cases where cursor becomes hidden while holding left button + if self.fakedMousePressed { + Toucher.touchcam(point: curPos, phase: UITouch.Phase.ended, tid: &self.fakedMouseTouchPointId) + return + } + if mode.visible && pressed { Toucher.touchcam(point: curPos, phase: UITouch.Phase.began, - tid: &fakedMouseTouchPointId) - return false + tid: &self.fakedMouseTouchPointId) + return } - } else { - if self.fakedMousePressed { - Toucher.touchcam(point: curPos, phase: UITouch.Phase.ended, tid: &fakedMouseTouchPointId) - return false + // considering cases where cursor becomes visible while holding left button + if let handlers = PlayInput.buttonHandlers["LMB"] { + for handler in handlers { + handler(pressed) + } + return } - } - return true - } - if !mode.visible { - self.mouseActions[actionIndex]!.forEach({ buttonAction in - buttonAction.update(pressed: state) }) return false } - return true } - - private func setMiceButton(_ keyId: Int, action: ButtonAction) { - switch keyId { - case -1: mouseActions[2]!.append(action) - case -2: mouseActions[8]!.append(action) - case -3: mouseActions[33554432]!.append(action) - default: - mouseActions[2]!.append(action) - } + // For all other actions, this is a destructor. should release held resources. + func invalidate() { + Toucher.touchcam(point: self.cursorPos() ?? CGPoint(x: 10, y: 10), + phase: UITouch.Phase.ended, tid: &self.fakedMouseTouchPointId) } } class CameraAction: Action { var swipeMove, swipeScale1, swipeScale2: SwipeAction + static var swipeDrag = SwipeAction() var key: String! var center: CGPoint var distance1: CGFloat = 100, distance2: CGFloat = 100 @@ -229,9 +182,16 @@ class CameraAction: Action { swipeMove = SwipeAction() swipeScale1 = SwipeAction() swipeScale2 = SwipeAction() - _ = PlayMice.shared.setupThumbstickChangedHandler(name: key) - PlayMice.shared.cameraMoveHandler[key] = self.moveUpdated - PlayMice.shared.cameraScaleHandler[PlayMice.elementName] = self.scaleUpdated + PlayInput.cameraMoveHandler[key] = self.moveUpdated + PlayInput.cameraScaleHandler[PlayMice.elementName] = {deltaX, deltaY in + PlayInput.touchQueue.async(qos: .userInteractive, execute: { + if mode.visible { + CameraAction.dragUpdated(deltaX, deltaY) + } else { + self.scaleUpdated(deltaX, deltaY) + } + }) + } } func moveUpdated(_ deltaX: CGFloat, _ deltaY: CGFloat) { swipeMove.move(from: {return center}, deltaX: deltaX, deltaY: deltaY) @@ -253,66 +213,69 @@ class CameraAction: Action { return CGPoint(x: center.x, y: center.y + 100) }, deltaX: 0, deltaY: -moveY) } - // Event handlers SHOULD be SMALL - // DO NOT check things like mode.visible in an event handler - // change the handler itself instead - func dragUpdated(_ deltaX: CGFloat, _ deltaY: CGFloat) { - swipeMove.move(from: PlayMice.shared.cursorPos, deltaX: deltaX * 4, deltaY: -deltaY * 4) + + static func dragUpdated(_ deltaX: CGFloat, _ deltaY: CGFloat) { + swipeDrag.move(from: PlayMice.shared.cursorPos, deltaX: deltaX * 4, deltaY: -deltaY * 4) } func invalidate() { - PlayMice.shared.cameraMoveHandler.removeValue(forKey: key) - PlayMice.shared.cameraScaleHandler[PlayMice.elementName] = self.dragUpdated + PlayInput.cameraMoveHandler.removeValue(forKey: key) + PlayInput.cameraScaleHandler[PlayMice.elementName] = nil + swipeMove.invalidate() + swipeScale1.invalidate() + swipeScale2.invalidate() } } class SwipeAction: Action { var location: CGPoint = CGPoint.zero - var id: Int? + private var id: Int? + let timer = DispatchSource.makeTimerSource(flags: [], queue: PlayInput.touchQueue) init() { - // in rare cases the cooldown reset task is lost by the dispatch queue - self.cooldown = false + timer.schedule(deadline: DispatchTime.now() + 1, repeating: 0.1, leeway: DispatchTimeInterval.milliseconds(50)) + timer.setEventHandler(qos: .userInteractive, handler: self.checkEnded) + timer.activate() + timer.suspend() } func delay(_ delay: Double, closure: @escaping () -> Void) { let when = DispatchTime.now() + delay - Toucher.touchQueue.asyncAfter(deadline: when, execute: closure) + PlayInput.touchQueue.asyncAfter(deadline: when, execute: closure) } - - // like sequence but resets when touch begins. Used to calc touch duration + // Count swipe duration var counter = 0 // if should wait before beginning next touch var cooldown = false - // in how many tests has this been identified as stationary - var stationaryCount = 0 - let stationaryThreshold = 2 + var lastCounter = 0 func checkEnded() { - // if been stationary for enough time - if self.stationaryCount < self.stationaryThreshold || (self.stationaryCount < 20 - self.counter) { - self.stationaryCount += 1 - self.delay(0.04, closure: self.checkEnded) - return + if self.counter == self.lastCounter { + if self.counter < 4 { + counter += 1 + } else { + timer.suspend() + self.doLiftOff() + } } - self.doLiftOff() + self.lastCounter = self.counter } - public func move(from: () -> CGPoint, deltaX: CGFloat, deltaY: CGFloat) { - // count touch duration - counter += 1 + public func move(from: () -> CGPoint?, deltaX: CGFloat, deltaY: CGFloat) { if id == nil { if cooldown { return } + guard let start = from() else {return} + location = start counter = 0 - location = from() Toucher.touchcam(point: location, phase: UITouch.Phase.began, tid: &id) - delay(0.01, closure: checkEnded) + timer.resume() } + // count touch duration + counter += 1 self.location.x += deltaX self.location.y -= deltaY Toucher.touchcam(point: self.location, phase: UITouch.Phase.moved, tid: &id) - stationaryCount = 0 } public func doLiftOff() { @@ -320,9 +283,6 @@ class SwipeAction: Action { return } Toucher.touchcam(point: self.location, phase: UITouch.Phase.ended, tid: &id) - // ending and beginning too frequently leads to the beginning event not recognized - // so let the beginning event wait some time - // pause for one frame or two delay(0.02) { self.cooldown = false } @@ -330,6 +290,7 @@ class SwipeAction: Action { } func invalidate() { - // pass + timer.cancel() + self.doLiftOff() } } diff --git a/PlayTools/Controls/Toucher.swift b/PlayTools/Controls/Toucher.swift index 60a27be5..a4dce82d 100644 --- a/PlayTools/Controls/Toucher.swift +++ b/PlayTools/Controls/Toucher.swift @@ -9,47 +9,71 @@ import UIKit class Toucher { static weak var keyWindow: UIWindow? static weak var keyView: UIView? - static var touchQueue = DispatchQueue.init(label: "playcover.toucher", qos: .userInteractive) - static var nextId: Int = 0 - static var idMap = [Int?](repeating: nil, count: 64) + // For debug only + static var logEnabled = false + static var logFilePath = + NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/toucher.log" + static private var logCount = 0 + static var logFile: FileHandle? /** on invocations with phase "began", an int id is allocated, which can be used later to refer to this touch point. on invocations with phase "ended", id is set to nil representing the touch point is no longer valid. */ static func touchcam(point: CGPoint, phase: UITouch.Phase, tid: inout Int?) { if phase == UITouch.Phase.began { - tid = nextId - nextId += 1 -// Toast.showOver(msg: tid!.description) - } - guard let bigId = tid else { - // sending other phases with empty id is no-op + if tid != nil { + return + } + tid = -1 + keyWindow = screen.keyWindow + keyView = keyWindow!.hitTest(point, with: nil) + } else if tid == nil { return } - if phase == UITouch.Phase.ended || phase == UITouch.Phase.cancelled { + tid = PTFakeMetaTouch.fakeTouchId(tid!, at: point, with: phase, in: keyWindow, on: keyView) + writeLog(logMessage: + "\(phase.rawValue.description) \(tid!.description) \(point.debugDescription)") + if tid! < 0 { tid = nil } - touchQueue.async { - if keyWindow == nil || keyView == nil { - keyWindow = screen.keyWindow - DispatchQueue.main.sync { - keyView = keyWindow?.hitTest(point, with: nil) - } - } - var pointId: Int = 0 - if phase != UITouch.Phase.began { - guard let id = idMap.firstIndex(of: bigId) else { - // sending other phases before began is no-op - return - } - pointId = id - } - let resultingId = PTFakeMetaTouch.fakeTouchId(pointId, at: point, with: phase, in: keyWindow, on: keyView) - if resultingId < 0 { - idMap[pointId] = nil - } else { - idMap[resultingId] = bigId - } + } + + static func setupLogfile() { + if FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil) { + logFile = FileHandle(forWritingAtPath: logFilePath) + Toast.showOver(msg: logFilePath) + } else { + Toast.showHint(title: "logFile creation failed") + return + } + NotificationCenter.default.addObserver( + forName: NSNotification.Name(rawValue: "NSApplicationWillTerminateNotification"), + object: nil, + queue: OperationQueue.main + ) { _ in + try? logFile?.close() + } + } + + static func writeLog(logMessage: String) { + if !logEnabled { + return + } + guard let file = logFile else { + setupLogfile() + return + } + let message = "\(DispatchTime.now().rawValue) \(logMessage)\n" + guard let data = message.data(using: .utf8) else { + Toast.showHint(title: "log message is utf8 uncodable") + return + } + logCount += 1 + // roll over + if logCount > 60000 { + file.seek(toFileOffset: 0) + logCount = 0 } + file.write(data) } } diff --git a/PlayTools/Keymap/ControlModel.swift b/PlayTools/Keymap/ControlModel.swift index b5ea7dc4..28fc7f42 100644 --- a/PlayTools/Keymap/ControlModel.swift +++ b/PlayTools/Keymap/ControlModel.swift @@ -1,5 +1,7 @@ import GameController +// Data structure definition should match those in +// https://github.com/PlayCover/PlayCover/blob/develop/PlayCover/Model/Keymapping.swift class ControlData { var keyCodes: [Int] var keyName: String diff --git a/PlayTools/Keymap/EditorController.swift b/PlayTools/Keymap/EditorController.swift index 1fe47c66..3b44ee66 100644 --- a/PlayTools/Keymap/EditorController.swift +++ b/PlayTools/Keymap/EditorController.swift @@ -60,14 +60,21 @@ class EditorController { previousWindow?.makeKeyAndVisible() PlayInput.shared.toggleEditor(show: false) focusedControl = nil - Toast.showOver(msg: "Keymapping saved") + Toast.showHint(title: NSLocalizedString("hint.keymapSaved", + tableName: "Playtools", value: "Keymap Saved", comment: "")) } else { PlayInput.shared.toggleEditor(show: true) previousWindow = screen.keyWindow editorWindow = initWindow() editorWindow?.makeKeyAndVisible() showButtons() - Toast.showOver(msg: "Click to start keymmaping edit") + Toast.showHint(title: NSLocalizedString("hint.keymappingEditor.title", + tableName: "Playtools", value: "Keymapping Editor", comment: ""), + text: [NSLocalizedString("hint.keymappingEditor.content", + tableName: "Playtools", + value: "Click a button to edit its position or key bind\n" + + "Click an empty area to open input menu", comment: "")], + notification: NSNotification.Name.playtoolsCursorWillHide) } // Toast.showOver(msg: "\(UIApplication.shared.windows.count)") lock.unlock() diff --git a/PlayTools/Keymap/KeyCodeNames.swift b/PlayTools/Keymap/KeyCodeNames.swift index 8df24d67..4e62696b 100644 --- a/PlayTools/Keymap/KeyCodeNames.swift +++ b/PlayTools/Keymap/KeyCodeNames.swift @@ -1,3 +1,4 @@ +// Should match https://github.com/PlayCover/PlayCover/blob/develop/PlayCover/Model/KeyCodeNames.swift exactly class KeyCodeNames { public static let defaultCode = -10 @@ -25,6 +26,8 @@ class KeyCodeNames { 43: "Tab", 227: "LCmd", 226: "LOpt", + 224: "LCtrl", + 228: "RCtrl", 231: "RCmd", 230: "ROpt", 40: "Enter", @@ -46,7 +49,7 @@ class KeyCodeNames { 67: "F10", 68: "F11", 69: "F12", - 100: "§", +// 100: "§", 30: "1", 31: "2", 32: "3", @@ -95,4 +98,164 @@ class KeyCodeNames { 55: ".", 56: "/" ] +public static let virtualCodes: [UInt16: String] = [ + 0: "A", + 11: "B", + 8: "C", + 2: "D", + 14: "E", + 3: "F", + 5: "G", + 4: "H", + 34: "I", + 38: "J", + 40: "K", + 37: "L", + 46: "M", + 45: "N", + 31: "O", + 35: "P", + 12: "Q", + 15: "R", + 1: "S", + 17: "T", + 32: "U", + 9: "V", + 13: "W", + 7: "X", + 16: "Y", + 6: "Z", + 18: "1", + 19: "2", + 20: "3", + 21: "4", + 23: "5", + 22: "6", + 26: "7", + 28: "8", + 25: "9", + 29: "0", + 36: "Enter", + 53: "Esc", + 51: "Del", + 48: "Tab", + 49: "Spc", + 27: "-", + 24: "=", + 33: "[", + 30: "]", + 42: "\\", + 41: ";", + 39: "'", + 50: "`", + 43: ",", + 47: ".", + 44: "/", + 57: "Caps", + 122: "F1", + 120: "F2", + 99: "F3", + 118: "F4", + 96: "F5", + 97: "F6", + 98: "F7", + 100: "F8", + 101: "F9", + 109: "F10", + 103: "F11", + 111: "F12", + 124: "Right", + 123: "Left", + 125: "Down", + 126: "Up", +// §: "§", + 56: "Lshft", + 58: "LOpt", + 55: "LCmd", + 60: "Rshft", + 61: "ROpt", + 54: "RCmd", + 59: "LCtrl", + 62: "RCtrl" +] } +let mapGCKeyCodeRawValuetoNSEventVirtualCode = [ + 41: 53, // Esc + 44: 49, + 225: 0x38, // "Lshft", + 57: 0x39, // "Caps", + 43: 48, + 227: 0x37, // "LCmd", + 226: 0x3A, // "LOpt", + 231: 0x36, // "RCmd", + 230: 0x3D, // "ROpt", + 40: 36, + 42: 51, + 229: 0x3C, // "Rshft", + 80: 123, + 79: 124, + 82: 126, + 81: 125, + 58: 122, // "F1", + 59: 120, // "F2", + 60: 99, // "F3", + 61: 118, // "F4", + 62: 96, // "F5", + 63: 97, // "F6", + 64: 98, // "F7", + 65: 100, // "F8", + 66: 101, + 67: 109, + 68: 0x67, // "F11", + 69: 111, // "F12", +// 100: "§", + 30: 18, // "1", + 31: 19, // "2", + 32: 20, // "3", + 33: 21, // "4", + 34: 23, // "5", + 35: 22, // "6", + 36: 26, // "7", + 37: 28, // "8", + 38: 25, // "9", + 39: 29, // -"0", + 45: 27, // "-", + 46: 24, // "=", + 20: 12, // "Q", + 26: 13, // "W", + 8: 14, // "E", + 21: 15, // "R", + 23: 17, // u"T", + 28: 16, // "Y", + 24: 32, // "U", + 12: 34, // "I", + 18: 31, // "O", + 19: 35, // "P", + 47: 33, // "[", + 48: 30, // "]", + 4: 0, // "A", + 22: 1, // "S", + 7: 2, // "D", + 9: 3, // "F", + 10: 5, // "G", + 11: 4, // "H", + 13: 38, // "J", + 14: 40, // "K", + 15: 37, // "L", + 51: 41, // ";", + 52: 39, // "'", + 49: 42, // "\\", + 29: 6, // "Z", + 53: 50, // "`", + 27: 7, // "X", + 6: 8, // "C", + 25: 9, // "V", + 5: 11, // "B", + 17: 45, // "N", + 16: 46, // "M", + 54: 43, // ",", + 55: 47, // ".", + 56: 44, // "/" + 224: 59, // "LCtrl", + 228: 62 // "RCtrl", + ] diff --git a/PlayTools/Keymap/Keymapping.swift b/PlayTools/Keymap/Keymapping.swift index 5fb866de..af3fb26b 100644 --- a/PlayTools/Keymap/Keymapping.swift +++ b/PlayTools/Keymap/Keymapping.swift @@ -63,7 +63,8 @@ class Keymapping { } } } - +// Data structure definition should match those in +// https://github.com/PlayCover/PlayCover/blob/develop/PlayCover/Model/Keymapping.swift struct KeyModelTransform: Codable { var size: CGFloat var xCoord: CGFloat diff --git a/PlayTools/MysticRunes/PlayShadow.h b/PlayTools/MysticRunes/PlayShadow.h new file mode 100644 index 00000000..014602c9 --- /dev/null +++ b/PlayTools/MysticRunes/PlayShadow.h @@ -0,0 +1,18 @@ +// +// PlayShadow.h +// PlayTools +// +// Created by Venti on 08/03/2023. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSObject (ShadowSwizzle) + +- (void)swizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector; ++ (void)swizzleClassMethod: (SEL)origSelector withMethod: (SEL)newSelector + +@end +NS_ASSUME_NONNULL_END diff --git a/PlayTools/MysticRunes/PlayShadow.m b/PlayTools/MysticRunes/PlayShadow.m new file mode 100644 index 00000000..edf61ccb --- /dev/null +++ b/PlayTools/MysticRunes/PlayShadow.m @@ -0,0 +1,315 @@ +// +// PlayShadow.m +// PlayTools +// +// Created by Venti on 08/03/2023. +// + +#import +#import +#import + +__attribute__((visibility("hidden"))) +@interface PlayShadowLoader : NSObject +@end + +@implementation NSObject (ShadowSwizzle) + +- (void) swizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector +{ + Class cls = [self class]; + // If current class doesn't exist selector, then get super + Method originalMethod = class_getInstanceMethod(cls, origSelector); + Method swizzledMethod = class_getInstanceMethod(cls, newSelector); + + // Add selector if it doesn't exist, implement append with method + if (class_addMethod(cls, + origSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)) ) { + // Replace class instance method, added if selector not exist + // For class cluster, it always adds new selector here + class_replaceMethod(cls, + newSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + + } else { + // SwizzleMethod maybe belongs to super + class_replaceMethod(cls, + newSelector, + class_replaceMethod(cls, + origSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)), + method_getTypeEncoding(originalMethod)); + } +} + ++ (void) swizzleClassMethod:(SEL)origSelector withMethod:(SEL)newSelector { + Class cls = object_getClass((id)self); + Method originalMethod = class_getClassMethod(cls, origSelector); + Method swizzledMethod = class_getClassMethod(cls, newSelector); + + if (class_addMethod(cls, + origSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)) ) { + class_replaceMethod(cls, + newSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + } else { + class_replaceMethod(cls, + newSelector, + class_replaceMethod(cls, + origSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)), + method_getTypeEncoding(originalMethod)); + } +} + +// Instance methods + +- (NSInteger) pm_hook_deviceType { + return 1; +} + +- (bool) pm_return_false { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return false; +} + +- (bool) pm_return_true { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return true; +} + +- (BOOL) pm_return_yes { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return YES; +} + +- (BOOL) pm_return_no { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return NO; +} + +- (int) pm_return_0 { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return 0; +} + +- (int) pm_return_1 { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return 1; +} + +- (NSString *) pm_return_empty { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return @""; +} + +- (NSDictionary *) pm_return_empty_dictionary { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return @{}; +} + +// Class methods + ++ (void) pm_return_2_with_completion_handler:(void (^)(NSInteger))completionHandler { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + completionHandler(2); +} + ++ (NSInteger) pm_return_2 { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return 2; +} + ++ (bool) pm_clsm_return_false { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return false; +} + ++ (bool) pm_clsm_return_true { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return true; +} + ++ (BOOL) pm_clsm_return_yes { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return YES; +} + ++ (BOOL) pm_clsm_return_no { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return NO; +} + ++ (int) pm_clsm_do_nothing_with_callback:(void (^)(int))callback { + // NSLog(@"PC-DEBUG: [PlayMask] Jailbreak Detection Attempted"); + return 0; +} + +@end + +@implementation PlayShadowLoader + ++ (void) load { + [self debugLogger:@"PlayShadow is now loading"]; + if ([[PlaySettings shared] bypass]) [self loadJailbreakBypass]; + // if ([[PlaySettings shared] bypass]) [self loadEnvironmentBypass]; # disabled as it might be too powerful + + // Swizzle ATTrackingManager + [objc_getClass("ATTrackingManager") swizzleClassMethod:@selector(requestTrackingAuthorizationWithCompletionHandler:) withMethod:@selector(pm_return_2_with_completion_handler:)]; + [objc_getClass("ATTrackingManager") swizzleClassMethod:@selector(trackingAuthorizationStatus) withMethod:@selector(pm_return_2)]; +} + ++ (void) loadJailbreakBypass { + [self debugLogger:@"Jailbreak bypass loading"]; + // Swizzle NSProcessInfo to troll every app that tries to detect macCatalyst + // [objc_getClass("NSProcessInfo") swizzleInstanceMethod:@selector(isMacCatalystApp) withMethod:@selector(pm_return_false)]; + // [objc_getClass("NSProcessInfo") swizzleInstanceMethod:@selector(isiOSAppOnMac) withMethod:@selector(pm_return_true)]; + + // Some device info class + [objc_getClass("UIDevice") swizzleInstanceMethod:@selector(platform) withMethod:@selector(pm_return_empty)]; + [objc_getClass("UIDevice") swizzleInstanceMethod:@selector(hwModel) withMethod:@selector(pm_return_empty)]; + [objc_getClass("RNDeviceInfo") swizzleInstanceMethod:@selector(getDeviceType) withMethod:@selector(pm_hook_deviceType)]; + + // Class: UIDevice + [objc_getClass("UIDevice") swizzleClassMethod:@selector(isJailbroken) withMethod:@selector(pm_clsm_return_no)]; + [objc_getClass("UIDevice") swizzleInstanceMethod:@selector(isJailBreak) withMethod:@selector(pm_return_no)]; + [objc_getClass("UIDevice") swizzleInstanceMethod:@selector(isJailBroken) withMethod:@selector(pm_return_no)]; + + // Class: JailbreakDetectionVC + [objc_getClass("JailbreakDetectionVC") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_no)]; + + // Class: DTTJailbreakDetection + [objc_getClass("DTTJailbreakDetection") swizzleClassMethod:@selector(isJailbroken) withMethod:@selector(pm_clsm_return_no)]; + + // Class: ANSMetadata + [objc_getClass("ANSMetadata") swizzleInstanceMethod:@selector(computeIsJailbroken) withMethod:@selector(pm_return_no)]; + [objc_getClass("ANSMetadata") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_no)]; + + // Class: AppsFlyerUtils + [objc_getClass("AppsFlyerUtils") swizzleClassMethod:@selector(isJailBreakon) withMethod:@selector(pm_clsm_return_no)]; + [objc_getClass("AppsFlyerUtils") swizzleClassMethod:@selector(isJailbroken) withMethod:@selector(pm_clsm_return_no)]; + [objc_getClass("AppsFlyerUtils") swizzleClassMethod:@selector(isJailbrokenWithSkipAdvancedJailbreakValidation:) withMethod:@selector(pm_clsm_return_false)]; + + // Class: jailBreak + [objc_getClass("jailBreak") swizzleClassMethod:@selector(isJailBreak) withMethod:@selector(pm_clsm_return_false)]; + + // Class: GBDeviceInfo + [objc_getClass("GBDeviceInfo") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_no)]; + + // Class: CMARAppRestrictionsDelegate + [objc_getClass("CMARAppRestrictionsDelegate") swizzleInstanceMethod:@selector(isDeviceNonCompliant) withMethod:@selector(pm_return_false)]; + + // Class: ADYSecurityChecks + [objc_getClass("ADYSecurityChecks") swizzleClassMethod:@selector(isDeviceJailbroken) withMethod:@selector(pm_clsm_return_false)]; + + // Class: UBReportMetadataDevice + [objc_getClass("UBReportMetadataDevice") swizzleInstanceMethod:@selector(is_rooted) withMethod:@selector(pm_return_null)]; + + // Class: UtilitySystem + [objc_getClass("UtilitySystem") swizzleClassMethod:@selector(isJailbreak) withMethod:@selector(pm_clsm_return_false)]; + + // Class: GemaltoConfiguration + [objc_getClass("GemaltoConfiguration") swizzleClassMethod:@selector(isJailbreak) withMethod:@selector(pm_clsm_return_false)]; + + // Class: CPWRDeviceInfo + [objc_getClass("CPWRDeviceInfo") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_false)]; + + // Class: CPWRSessionInfo + [objc_getClass("CPWRSessionInfo") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_false)]; + + // Class: KSSystemInfo + [objc_getClass("KSSystemInfo") swizzleClassMethod:@selector(isJailbroken) withMethod:@selector(pm_clsm_return_false)]; + + // Class: EMDSKPPConfiguration + [objc_getClass("EMDSKPPConfiguration") swizzleInstanceMethod:@selector(jailBroken) withMethod:@selector(pm_return_false)]; + + // Class: EnrollParameters + [objc_getClass("EnrollParameters") swizzleInstanceMethod:@selector(jailbroken) withMethod:@selector(pm_return_null)]; + + // Class: EMDskppConfigurationBuilder + [objc_getClass("EMDskppConfigurationBuilder") swizzleInstanceMethod:@selector(jailbreakStatus) withMethod:@selector(pm_return_false)]; + + // Class: FCRSystemMetadata + [objc_getClass("FCRSystemMetadata") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_false)]; + + // Class: v_VDMap + [objc_getClass("v_VDMap") swizzleInstanceMethod:@selector(isJailbrokenDetected) withMethod:@selector(pm_return_false)]; + [objc_getClass("v_VDMap") swizzleInstanceMethod:@selector(isJailBrokenDetectedByVOS) withMethod:@selector(pm_return_false)]; + [objc_getClass("v_VDMap") swizzleInstanceMethod:@selector(isDFPHookedDetecedByVOS) withMethod:@selector(pm_return_false)]; + [objc_getClass("v_VDMap") swizzleInstanceMethod:@selector(isCodeInjectionDetectedByVOS) withMethod:@selector(pm_return_false)]; + [objc_getClass("v_VDMap") swizzleInstanceMethod:@selector(isDebuggerCheckDetectedByVOS) withMethod:@selector(pm_return_false)]; + [objc_getClass("v_VDMap") swizzleInstanceMethod:@selector(isAppSignerCheckDetectedByVOS) withMethod:@selector(pm_return_false)]; + [objc_getClass("v_VDMap") swizzleInstanceMethod:@selector(v_checkAModified) withMethod:@selector(pm_return_false)]; + [objc_getClass("v_VDMap") swizzleInstanceMethod:@selector(isRuntimeTamperingDetected) withMethod:@selector(pm_return_false)]; + + // Class: SDMUtils + [objc_getClass("SDMUtils") swizzleInstanceMethod:@selector(isJailBroken) withMethod:@selector(pm_return_no)]; + + // Class: OneSignalJailbreakDetection + [objc_getClass("OneSignalJailbreakDetection") swizzleClassMethod:@selector(isJailbroken) withMethod:@selector(pm_clsm_return_no)]; + + // Class: DigiPassHandler + [objc_getClass("DigiPassHandler") swizzleInstanceMethod:@selector(rootedDeviceTestResult) withMethod:@selector(pm_return_no)]; + + // Class: AWMyDeviceGeneralInfo + [objc_getClass("AWMyDeviceGeneralInfo") swizzleInstanceMethod:@selector(isCompliant) withMethod:@selector(pm_return_true)]; + + // Class: DTXSessionInfo + [objc_getClass("DTXSessionInfo") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_false)]; + + // Class: DTXDeviceInfo + [objc_getClass("DTXDeviceInfo") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_false)]; + + // Class: JailbreakDetection + [objc_getClass("JailbreakDetection") swizzleInstanceMethod:@selector(jailbroken) withMethod:@selector(pm_return_false)]; + + // Class: jailBrokenJudge + [objc_getClass("jailBrokenJudge") swizzleInstanceMethod:@selector(isJailBreak) withMethod:@selector(pm_return_false)]; + [objc_getClass("jailBrokenJudge") swizzleInstanceMethod:@selector(isCydiaJailBreak) withMethod:@selector(pm_return_false)]; + [objc_getClass("jailBrokenJudge") swizzleInstanceMethod:@selector(isApplicationsJailBreak) withMethod:@selector(pm_return_false)]; + [objc_getClass("jailBrokenJudge") swizzleInstanceMethod:@selector(ischeckCydiaJailBreak) withMethod:@selector(pm_return_false)]; + [objc_getClass("jailBrokenJudge") swizzleInstanceMethod:@selector(isPathJailBreak) withMethod:@selector(pm_return_false)]; + [objc_getClass("jailBrokenJudge") swizzleInstanceMethod:@selector(boolIsjailbreak) withMethod:@selector(pm_return_false)]; + + // Class: FBAdBotDetector + [objc_getClass("FBAdBotDetector") swizzleInstanceMethod:@selector(isJailBrokenDevice) withMethod:@selector(pm_return_false)]; + + // Class: TNGDeviceTool + [objc_getClass("TNGDeviceTool") swizzleClassMethod:@selector(isJailBreak) withMethod:@selector(pm_clsm_return_false)]; + [objc_getClass("TNGDeviceTool") swizzleClassMethod:@selector(isJailBreak_file) withMethod:@selector(pm_clsm_return_false)]; + [objc_getClass("TNGDeviceTool") swizzleClassMethod:@selector(isJailBreak_cydia) withMethod:@selector(pm_clsm_return_false)]; + [objc_getClass("TNGDeviceTool") swizzleClassMethod:@selector(isJailBreak_appList) withMethod:@selector(pm_clsm_return_false)]; + [objc_getClass("TNGDeviceTool") swizzleClassMethod:@selector(isJailBreak_env) withMethod:@selector(pm_clsm_return_false)]; + + // Class: DTDeviceInfo + [objc_getClass("DTDeviceInfo") swizzleClassMethod:@selector(isJailbreak) withMethod:@selector(pm_clsm_return_false)]; + + // Class: SecVIDeviceUtil + [objc_getClass("SecVIDeviceUtil") swizzleClassMethod:@selector(isJailbreak) withMethod:@selector(pm_clsm_return_false)]; + + // Class: RVPBridgeExtension4Jailbroken + [objc_getClass("RVPBridgeExtension4Jailbroken") swizzleInstanceMethod:@selector(isJailbroken) withMethod:@selector(pm_return_false)]; + + // Class: ZDetection + [objc_getClass("ZDetection") swizzleClassMethod:@selector(isRootedOrJailbroken) withMethod:@selector(pm_clsm_return_false)]; +} + ++ (void) loadEnvironmentBypass { + [self debugLogger:@"Environment bypass loading"]; + // Completely nuke everything in the environment variables + [objc_getClass("NSProcessInfo") swizzleInstanceMethod:@selector(environment) withMethod:@selector(pm_return_empty_dictionary)]; +} + ++ (void) debugLogger: (NSString *) message { + NSLog(@"PC-DEBUG: %@", message); +} + +@end diff --git a/PlayTools/MysticRunes/PlayedApple.swift b/PlayTools/MysticRunes/PlayedApple.swift new file mode 100644 index 00000000..34acf2b5 --- /dev/null +++ b/PlayTools/MysticRunes/PlayedApple.swift @@ -0,0 +1,167 @@ +// +// PlayWeRuinedIt.swift +// PlayTools +// +// Created by Venti on 16/01/2023. +// + +import Foundation +import Security + +// Implementation for PlayKeychain +// World's Most Advanced Keychain Replacement Solution:tm: +// This is a joke, don't take it seriously + +public class PlayKeychain: NSObject { + static let shared = PlayKeychain() + + private static func getKeychainDirectory() -> URL? { + let bundleID = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String ?? "" + let keychainFolder = URL(fileURLWithPath: "/Users/\(NSUserName())/Library/Containers/io.playcover.PlayCover") + .appendingPathComponent("PlayChain") + .appendingPathComponent(bundleID) + + // Create the keychain folder if it doesn't exist + if !FileManager.default.fileExists(atPath: keychainFolder.path) { + do { + try FileManager.default.createDirectory(at: keychainFolder, + withIntermediateDirectories: true, + attributes: nil) + } catch { + debugLogger("Failed to create keychain folder") + } + } + + return keychainFolder + } + + private static func keychainPath(_ attributes: NSDictionary) -> URL { + let keychainFolder = getKeychainDirectory() + // Generate a key path based on the key attributes + let accountName = attributes[kSecAttrAccount as String] as? String ?? "" + let serviceName = attributes[kSecAttrService as String] as? String ?? "" + let classType = attributes[kSecClass as String] as? String ?? "" + return keychainFolder! + .appendingPathComponent("\(serviceName)-\(accountName)-\(classType).plist") + } + + @objc public static func debugLogger(_ logContent: String) { + if PlaySettings.shared.settingsData.playChainDebugging { + NSLog("PC-DEBUG: \(logContent)") + } + } + // Emulates SecItemAdd, SecItemUpdate, SecItemDelete and SecItemCopyMatching + // Store the entire dictionary as a plist + // SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result) + @objc static public func add(_ attributes: NSDictionary, result: UnsafeMutablePointer?) -> OSStatus { + let keychainPath = keychainPath(attributes) + // Check if the keychain file already exists + if FileManager.default.fileExists(atPath: keychainPath.path) { + debugLogger("Keychain file already exists") + return errSecDuplicateItem + } + // Write the dictionary to the keychain file + do { + try attributes.write(to: keychainPath) + debugLogger("Wrote keychain file to \(keychainPath)") + } catch { + debugLogger("Failed to write keychain file") + return errSecIO + } + // Place v_Data in the result + if let v_data = attributes["v_Data"] { + result?.pointee = v_data as CFTypeRef + } + return errSecSuccess + } + + // SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate) + @objc static public func update(_ query: NSDictionary, attributesToUpdate: NSDictionary) -> OSStatus { + // Get the path to the keychain file + let keychainPath = keychainPath(query) + // Read the dictionary from the keychain file + let keychainDict = NSDictionary(contentsOf: keychainPath) + debugLogger("Read keychain file from \(keychainPath)") + // Check if the file exist + if keychainDict == nil { + debugLogger("Keychain file not found at \(keychainPath)") + return errSecItemNotFound + } + // Reconstruct the dictionary (subscripting won't work as assignment is not allowed) + let newKeychainDict = NSMutableDictionary() + for (key, value) in keychainDict! { + newKeychainDict.setValue(value, forKey: key as! String) // swiftlint:disable:this force_cast + } + // Update the dictionary + for (key, value) in attributesToUpdate { + newKeychainDict.setValue(value, forKey: key as! String) // swiftlint:disable:this force_cast + } + // Write the dictionary to the keychain file + do { + try newKeychainDict.write(to: keychainPath) + debugLogger("Wrote keychain file to \(keychainPath)") + } catch { + debugLogger("Failed to write keychain file") + return errSecIO + } + + return errSecSuccess + } + + // SecItemDelete(CFDictionaryRef query) + @objc static public func delete(_ query: NSDictionary) -> OSStatus { + // Get the path to the keychain file + let keychainPath = keychainPath(query) + // Delete the keychain file + do { + try FileManager.default.removeItem(at: keychainPath) + debugLogger("Deleted keychain file at \(keychainPath)") + } catch { + debugLogger("Failed to delete keychain file") + return errSecIO + } + return errSecSuccess + } + + // SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result) + @objc static public func copyMatching(_ query: NSDictionary, result: UnsafeMutablePointer?) + -> OSStatus { + // Get the path to the keychain file + let keychainPath = keychainPath(query) + // Read the dictionary from the keychain file + let keychainDict = NSDictionary(contentsOf: keychainPath) + // Check the `r_Attributes` key. If it is set to 1 in the query + // DROP, NOT IMPLEMENTED + let classType = query[kSecClass as String] as? String ?? "" + if query["r_Attributes"] as? Int == 1 { + return errSecItemNotFound + } + // If the keychain file doesn't exist, return errSecItemNotFound + if keychainDict == nil { + debugLogger("Keychain file not found at \(keychainPath)") + return errSecItemNotFound + } + // Return v_Data if it exists + if let vData = keychainDict!["v_Data"] { + debugLogger("Read keychain file from \(keychainPath)") + // Check the class type, if it is a key we need to return the data + // as SecKeyRef, otherwise we can return it as a CFTypeRef + if classType == "keys" { + // kSecAttrKeyType is stored as `type` in the dictionary + // kSecAttrKeyClass is stored as `kcls` in the dictionary + let keyAttributes = [ + kSecAttrKeyType: keychainDict!["type"] as! CFString, // swiftlint:disable:this force_cast + kSecAttrKeyClass: keychainDict!["kcls"] as! CFString // swiftlint:disable:this force_cast + ] + let keyData = vData as! Data // swiftlint:disable:this force_cast + let key = SecKeyCreateWithData(keyData as CFData, keyAttributes as CFDictionary, nil) + result?.pointee = key + return errSecSuccess + } + result?.pointee = vData as CFTypeRef + return errSecSuccess + } + + return errSecItemNotFound + } +} \ No newline at end of file diff --git a/PlayTools/PlayCover.swift b/PlayTools/PlayCover.swift index 4f15a11a..b8c5fe35 100644 --- a/PlayTools/PlayCover.swift +++ b/PlayTools/PlayCover.swift @@ -16,6 +16,11 @@ public class PlayCover: NSObject { AKInterface.initialize() PlayInput.shared.initialize() DiscordIPC.shared.initialize() + + if PlaySettings.shared.rootWorkDir { + // Change the working directory to / just like iOS + FileManager.default.changeCurrentDirectoryPath("/") + } } @objc static public func initMenu(menu: NSObject) { diff --git a/PlayTools/PlayLoader.m b/PlayTools/PlayLoader.m index 1994e1a2..b80ba2ec 100644 --- a/PlayTools/PlayLoader.m +++ b/PlayTools/PlayLoader.m @@ -12,8 +12,9 @@ #import "NSObject+Swizzle.h" // Get device model from playcover .plist -#define DEVICE_MODEL ([[[PlaySettings shared] deviceModel] UTF8String]) -#define OEM_ID ([[[PlaySettings shared] oemID] UTF8String]) +// With a null terminator +#define DEVICE_MODEL [[[PlaySettings shared] deviceModel] cStringUsingEncoding:NSUTF8StringEncoding] +#define OEM_ID [[[PlaySettings shared] oemID] cStringUsingEncoding:NSUTF8StringEncoding] #define PLATFORM_IOS 2 // Define dyld_get_active_platform function for interpose @@ -60,22 +61,37 @@ static int pt_sysctl(int *name, u_int types, void *buf, size_t *size, void *arg0 static int pt_sysctlbyname(const char *name, void *oldp, size_t *oldlenp, void *newp, size_t newlen) { if ((strcmp(name, "hw.machine") == 0) || (strcmp(name, "hw.product") == 0) || (strcmp(name, "hw.model") == 0)) { - if (oldp != NULL) { + if (oldp == NULL) { + int ret = sysctlbyname(name, oldp, oldlenp, newp, newlen); + // We don't want to accidentally decrease it because the real sysctl call will ENOMEM + // as model are much longer on Macs (eg. MacBookAir10,1) + if (*oldlenp < strlen(DEVICE_MODEL) + 1) { + *oldlenp = strlen(DEVICE_MODEL) + 1; + } + return ret; + } + else if (oldp != NULL) { int ret = sysctlbyname(name, oldp, oldlenp, newp, newlen); const char *machine = DEVICE_MODEL; strncpy((char *)oldp, machine, strlen(machine)); - *oldlenp = strlen(machine); + *oldlenp = strlen(machine) + 1; return ret; } else { int ret = sysctlbyname(name, oldp, oldlenp, newp, newlen); return ret; } } else if ((strcmp(name, "hw.target") == 0)) { - if (oldp != NULL) { + if (oldp == NULL) { + int ret = sysctlbyname(name, oldp, oldlenp, newp, newlen); + if (*oldlenp < strlen(OEM_ID) + 1) { + *oldlenp = strlen(OEM_ID) + 1; + } + return ret; + } else if (oldp != NULL) { int ret = sysctlbyname(name, oldp, oldlenp, newp, newlen); const char *machine = OEM_ID; strncpy((char *)oldp, machine, strlen(machine)); - *oldlenp = strlen(machine); + *oldlenp = strlen(machine) + 1; return ret; } else { int ret = sysctlbyname(name, oldp, oldlenp, newp, newlen); @@ -92,6 +108,77 @@ static int pt_sysctlbyname(const char *name, void *oldp, size_t *oldlenp, void * DYLD_INTERPOSE(pt_sysctlbyname, sysctlbyname) DYLD_INTERPOSE(pt_sysctl, sysctl) +// Interpose Apple Keychain functions (SecItemCopyMatching, SecItemAdd, SecItemUpdate, SecItemDelete) +// This allows us to intercept keychain requests and return our own data + +// Use the implementations from PlayKeychain +static OSStatus pt_SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result) { + OSStatus retval; + if ([[PlaySettings shared] playChain]) { + retval = [PlayKeychain copyMatching:(__bridge NSDictionary * _Nonnull)(query) result:result]; + } else { + retval = SecItemCopyMatching(query, result); + } + if (result != NULL) { + if ([[PlaySettings shared] playChainDebugging]) { + [PlayKeychain debugLogger:[NSString stringWithFormat:@"SecItemCopyMatching: %@", query]]; + [PlayKeychain debugLogger:[NSString stringWithFormat:@"SecItemCopyMatching result: %@", *result]]; + } + } + return retval; +} + +static OSStatus pt_SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result) { + OSStatus retval; + if ([[PlaySettings shared] playChain]) { + retval = [PlayKeychain add:(__bridge NSDictionary * _Nonnull)(attributes) result:result]; + } else { + retval = SecItemAdd(attributes, result); + } + if (result != NULL) { + if ([[PlaySettings shared] playChainDebugging]) { + [PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemAdd: %@", attributes]]; + [PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemAdd result: %@", *result]]; + } + } + return retval; +} + +static OSStatus pt_SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate) { + OSStatus retval; + if ([[PlaySettings shared] playChain]) { + retval = [PlayKeychain update:(__bridge NSDictionary * _Nonnull)(query) attributesToUpdate:(__bridge NSDictionary * _Nonnull)(attributesToUpdate)]; + } else { + retval = SecItemUpdate(query, attributesToUpdate); + } + if (attributesToUpdate != NULL) { + if ([[PlaySettings shared] playChainDebugging]) { + [PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemUpdate: %@", query]]; + [PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemUpdate attributesToUpdate: %@", attributesToUpdate]]; + } + } + return retval; + +} + +static OSStatus pt_SecItemDelete(CFDictionaryRef query) { + OSStatus retval; + if ([[PlaySettings shared] playChain]) { + retval = [PlayKeychain delete:(__bridge NSDictionary * _Nonnull)(query)]; + } else { + retval = SecItemDelete(query); + } + if ([[PlaySettings shared] playChainDebugging]) { + [PlayKeychain debugLogger: [NSString stringWithFormat:@"SecItemDelete: %@", query]]; + } + return retval; +} + +DYLD_INTERPOSE(pt_SecItemCopyMatching, SecItemCopyMatching) +DYLD_INTERPOSE(pt_SecItemAdd, SecItemAdd) +DYLD_INTERPOSE(pt_SecItemUpdate, SecItemUpdate) +DYLD_INTERPOSE(pt_SecItemDelete, SecItemDelete) + @implementation PlayLoader static void __attribute__((constructor)) initialize(void) { diff --git a/PlayTools/PlayScreen.swift b/PlayTools/PlayScreen.swift index 47bc4ff6..d443a57f 100644 --- a/PlayTools/PlayScreen.swift +++ b/PlayTools/PlayScreen.swift @@ -9,6 +9,7 @@ let screen = PlayScreen.shared let isInvertFixEnabled = PlaySettings.shared.inverseScreenValues && PlaySettings.shared.adaptiveDisplay let mainScreenWidth = !isInvertFixEnabled ? PlaySettings.shared.windowSizeWidth : PlaySettings.shared.windowSizeHeight let mainScreenHeight = !isInvertFixEnabled ? PlaySettings.shared.windowSizeHeight : PlaySettings.shared.windowSizeWidth +let customScaler = PlaySettings.shared.customScaler extension CGSize { func aspectRatio() -> CGFloat { @@ -93,7 +94,7 @@ public class PlayScreen: NSObject { } @objc public static func nativeBounds(_ rect: CGRect) -> CGRect { - return rect.toAspectRatio(2) + return rect.toAspectRatio(CGFloat((customScaler))) } @objc public static func width(_ size: Int) -> Int { @@ -172,7 +173,7 @@ public class PlayScreen: NSObject { } @objc public static func nativeBoundsDefault(_ rect: CGRect) -> CGRect { - return rect.toAspectRatioDefault(2) + return rect.toAspectRatioDefault(CGFloat((customScaler))) } @objc public static func sizeAspectRatioDefault(_ size: CGSize) -> CGSize { diff --git a/PlayTools/PlaySettings.swift b/PlayTools/PlaySettings.swift index 38b98510..0fab777e 100644 --- a/PlayTools/PlaySettings.swift +++ b/PlayTools/PlaySettings.swift @@ -27,12 +27,12 @@ let settings = PlaySettings.shared lazy var keymapping = settingsData.keymapping - lazy var mouseMapping = settingsData.mouseMapping - lazy var notch = settingsData.notch lazy var sensitivity = settingsData.sensitivity / 100 + @objc lazy var bypass = settingsData.bypass + @objc lazy var windowSizeHeight = CGFloat(settingsData.windowHeight) @objc lazy var windowSizeWidth = CGFloat(settingsData.windowWidth) @@ -61,22 +61,36 @@ let settings = PlaySettings.shared return "J320xAP" } }() + + @objc lazy var playChain = settingsData.playChain + + @objc lazy var playChainDebugging = settingsData.playChainDebugging + + @objc lazy var windowFixMethod = settingsData.windowFixMethod + + @objc lazy var customScaler = settingsData.customScaler + + @objc lazy var rootWorkDir = settingsData.rootWorkDir } struct AppSettingsData: Codable { var keymapping = true - var mouseMapping = true var sensitivity: Float = 50 var disableTimeout = false var iosDeviceModel = "iPad13,8" var windowWidth = 1920 var windowHeight = 1080 + var customScaler = 2.0 var resolution = 2 var aspectRatio = 1 var notch = false var bypass = false var discordActivity = DiscordActivity() var version = "2.0.0" + var playChain = false + var playChainDebugging = false var inverseScreenValues = false + var windowFixMethod = 0 + var rootWorkDir = true } diff --git a/PlayTools/Utils/Toast.swift b/PlayTools/Utils/Toast.swift index 4bf90244..e84bfaa4 100644 --- a/PlayTools/Utils/Toast.swift +++ b/PlayTools/Utils/Toast.swift @@ -12,7 +12,166 @@ class Toast { Toast.show(message: msg, parent: parent) } } + static var hintView: [UIView] = [] + private static let gap: CGFloat = 40 + + public static func hideHint(hint: UIView) { + guard let id = hintView.firstIndex(of: hint) else {return} + for index in 0.. id { + hintView[index-1] = hintView[index] + } + } + hintView.removeLast() + UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut, animations: { + hint.alpha = 0.0 + }, completion: {_ in + hint.removeFromSuperview() + }) + } + + private static func getAttributedString(title: String, text: [String]) -> NSMutableAttributedString { + var heading = title + if !text.isEmpty { + heading += "\n" + } + let txt = NSMutableAttributedString(string: text.reduce(into: heading, { result, string in + result += string + })) + var messageLength = 0 + var highlight = false + for msg in text { + txt.addAttribute(.foregroundColor, value: highlight ? UIColor.cyan: UIColor.white, + range: NSRange(location: heading.count + messageLength, length: msg.count)) + highlight = !highlight + messageLength += msg.count + } + let style = NSMutableParagraphStyle() + style.alignment = .center + txt.addAttribute(.paragraphStyle, value: style, + range: NSRange(location: 0, length: heading.count + messageLength)) + txt.addAttribute(.font, value: UIFont.systemFont(ofSize: 28, weight: .bold), + range: NSRange(location: 0, length: heading.count)) + txt.addAttribute(.foregroundColor, value: UIColor.white, + range: NSRange(location: 0, length: heading.count)) + txt.addAttribute(.font, value: UIFont.systemFont(ofSize: 28), + range: NSRange(location: heading.count, length: messageLength)) + return txt + } + + public static func showHint(title: String, text: [String] = [], timeout: Double = -3, + notification: NSNotification.Name? = nil) { + let parent = screen.keyWindow! + + // Width and height here serve as an upper limit. + // Text would fill width first, then wrap, then fill height, then scroll + let messageLabel = UITextView(frame: CGRect(x: 0, y: 0, width: 800, height: 800)) + messageLabel.attributedText = getAttributedString(title: title, text: text) + messageLabel.backgroundColor = UIColor.black.withAlphaComponent(0.5) + messageLabel.alpha = 1.0 + messageLabel.clipsToBounds = true + messageLabel.isUserInteractionEnabled = false + messageLabel.frame.size = messageLabel.sizeThatFits(messageLabel.frame.size) + messageLabel.layer.cornerCurve = CALayerCornerCurve.continuous + messageLabel.layer.cornerRadius = messageLabel.frame.size.height / 4 + messageLabel.frame.size.width += messageLabel.layer.cornerRadius * 2 + messageLabel.center.x = parent.center.x + messageLabel.center.y = -messageLabel.frame.size.height / 2 + + // Disable editing + messageLabel.isEditable = false + messageLabel.isSelectable = false + + hintView.append(messageLabel) + parent.addSubview(messageLabel) + + if hintView.count > 4 { + hideHint(hint: hintView.first!) + } + var life = timeout + if let note = notification { + let center = NotificationCenter.default + var token: NSObjectProtocol? + token = center.addObserver(forName: note, object: nil, queue: OperationQueue.main) { _ in + center.removeObserver(token!) + hideHint(hint: messageLabel) + } + } else if life < 0 { + life = 3 + } + if life >= 0 { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5 + life, qos: .background) { + hideHint(hint: messageLabel) + } + } + for view in hintView { + UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn, animations: { + view.layer.position.y += messageLabel.frame.size.height + gap + }) + } + } + + static func syncUserDefaults() -> Float { + let persistenceKeyname = "playtoolsKeymappingDisabledAt" + let lastUse = UserDefaults.standard.float(forKey: persistenceKeyname) + var thisUse = lastUse + if lastUse < 1 { + thisUse = 2 + } else { + thisUse = Float(Date.timeIntervalSinceReferenceDate) + } + var token2: NSObjectProtocol? + let center = NotificationCenter.default + token2 = center.addObserver(forName: NSNotification.Name.playtoolsCursorWillShow, + object: nil, queue: OperationQueue.main) { _ in + center.removeObserver(token2!) + UserDefaults.standard.set(thisUse, forKey: persistenceKeyname) + } + return lastUse + } + + public static func initialize() { + let lastUse = syncUserDefaults() + if lastUse > Float(Date.now.addingTimeInterval(-86400*14).timeIntervalSinceReferenceDate) { + return + } + Toast.showHint(title: NSLocalizedString("hint.mouseMapping.title", + tableName: "Playtools", + value: "Mouse mapping disabled", comment: ""), + text: [NSLocalizedString("hint.mouseMapping.content.before", + tableName: "Playtools", + value: "Press", comment: ""), + " option ⌥ ", + NSLocalizedString("hint.mouseMapping.content.after", + tableName: "Playtools", + value: "to enable mouse mapping", comment: "")], + timeout: 10, + notification: NSNotification.Name.playtoolsCursorWillHide) + let center = NotificationCenter.default + var token: NSObjectProtocol? + token = center.addObserver(forName: NSNotification.Name.playtoolsCursorWillHide, + object: nil, queue: OperationQueue.main) { _ in + center.removeObserver(token!) + Toast.showHint(title: NSLocalizedString("hint.showCursor.title", + tableName: "Playtools", + value: "Cursor locked", comment: ""), + text: [NSLocalizedString("hint.showCursor.content.before", + tableName: "Playtools", + value: "Press", comment: ""), + " option ⌥ ", + NSLocalizedString("hint.showCursor.content.after", + tableName: "Playtools", + value: "to unlock cursor", comment: "")], + timeout: 10, + notification: NSNotification.Name.playtoolsCursorWillShow) + } + } + // swiftlint:disable function_body_length private static func show(message: String, parent: UIView) { diff --git a/PlayTools/en.lproj/Playtools.strings b/PlayTools/en.lproj/Playtools.strings new file mode 100644 index 00000000..0311d2a5 Binary files /dev/null and b/PlayTools/en.lproj/Playtools.strings differ diff --git a/PlayTools/zh-Hans.lproj/Playtools.strings b/PlayTools/zh-Hans.lproj/Playtools.strings new file mode 100644 index 00000000..11b93063 Binary files /dev/null and b/PlayTools/zh-Hans.lproj/Playtools.strings differ diff --git a/Plugin.swift b/Plugin.swift index 6c2a7066..56bbe84d 100644 --- a/Plugin.swift +++ b/Plugin.swift @@ -17,12 +17,16 @@ public protocol Plugin: NSObjectProtocol { var mainScreenFrame: CGRect { get } var isMainScreenEqualToFirst: Bool { get } var isFullscreen: Bool { get } + var cmdPressed: Bool { get } func hideCursor() + func warpCursor() func unhideCursor() func terminateApplication() - func eliminateRedundantKeyPressEvents(_ dontIgnore: @escaping() -> Bool) - func setupMouseButton(_ _up: Int, _ _down: Int, _ dontIgnore: @escaping(Int, Bool, Bool) -> Bool) + func setupKeyboard(keyboard: @escaping(UInt16, Bool, Bool) -> Bool, + swapMode: @escaping() -> Bool) + func setupMouseMoved(mouseMoved: @escaping(CGFloat, CGFloat) -> Bool) + func setupMouseButton(left: Bool, right: Bool, _ dontIgnore: @escaping(Bool) -> Bool) func setupScrollWheel(_ onMoved: @escaping(CGFloat, CGFloat) -> Bool) func urlForApplicationWithBundleIdentifier(_ value: String) -> URL? func setMenuBarVisible(_ value: Bool)