From c52f909378d84eb276fb7325803a0a591e8e05d8 Mon Sep 17 00:00:00 2001 From: Xyct <87l46110@gmail.com> Date: Sun, 26 May 2024 04:11:04 +0800 Subject: [PATCH 1/9] release on option --- PlayTools/Controls/ActionDispatcher.swift | 7 +++++++ PlayTools/Controls/Backend/Action/PlayAction.swift | 1 + PlayTools/Controls/Frontend/ControlMode.swift | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/PlayTools/Controls/ActionDispatcher.swift b/PlayTools/Controls/ActionDispatcher.swift index 8071172e..de128686 100644 --- a/PlayTools/Controls/ActionDispatcher.swift +++ b/PlayTools/Controls/ActionDispatcher.swift @@ -149,6 +149,13 @@ public class ActionDispatcher { } } + static public func invalidateNonButtonActions() { + for action in actions + where !(action as? ButtonAction != nil || action is JoystickAction){ + action.invalidate() + } + } + static public func getDispatchPriority(key: String) -> ActionDispatchPriority? { if let priority = directionPadHandlers.firstIndex(where: { handlers in handlers.contains(where: { handler in diff --git a/PlayTools/Controls/Backend/Action/PlayAction.swift b/PlayTools/Controls/Backend/Action/PlayAction.swift index 4f024362..bc60ddac 100644 --- a/PlayTools/Controls/Backend/Action/PlayAction.swift +++ b/PlayTools/Controls/Backend/Action/PlayAction.swift @@ -418,6 +418,7 @@ class FakeMouseAction: Action { } func invalidate() { + ActionDispatcher.unregister(key: KeyCodeNames.fakeMouse) Toucher.touchcam(point: pos ?? CGPoint(x: 10, y: 10), phase: UITouch.Phase.ended, tid: &self.id) } diff --git a/PlayTools/Controls/Frontend/ControlMode.swift b/PlayTools/Controls/Frontend/ControlMode.swift index cf6eb9a2..3f16b62c 100644 --- a/PlayTools/Controls/Frontend/ControlMode.swift +++ b/PlayTools/Controls/Frontend/ControlMode.swift @@ -111,6 +111,11 @@ public class ControlMode: Equatable { if mode == .OFF || mode == .EDITOR { ActionDispatcher.invalidateActions() + } else { + // In case any touch point failed to release + // (might because of system glitch) + // Work around random zoom in zoom out + ActionDispatcher.invalidateNonButtonActions() } AKInterface.shared!.unhideCursor() @@ -118,6 +123,11 @@ public class ControlMode: Equatable { NotificationCenter.default.post(name: NSNotification.Name.playtoolsCursorWillHide, object: nil, userInfo: [:]) AKInterface.shared!.hideCursor() + + // Fix when people hold fake mouse while pressing option + // and it becomes random zoom in zoom out + ActionDispatcher.invalidateNonButtonActions() + if screen.fullscreen { screen.switchDock(false) } From f73c5688cf1bb5921109681c06a53a3259826404 Mon Sep 17 00:00:00 2001 From: Xyct <87l46110@gmail.com> Date: Sun, 26 May 2024 17:48:37 +0800 Subject: [PATCH 2/9] comment on invalidateNonButtonActions --- PlayTools/Controls/ActionDispatcher.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/PlayTools/Controls/ActionDispatcher.swift b/PlayTools/Controls/ActionDispatcher.swift index de128686..bb785ce9 100644 --- a/PlayTools/Controls/ActionDispatcher.swift +++ b/PlayTools/Controls/ActionDispatcher.swift @@ -141,6 +141,10 @@ public class ActionDispatcher { static public var cursorHideNecessary = true + /** + Lift off (release) all actions' touch points. + Would be called during mode switching where target mode shouldn't have any touch remain. + */ static public func invalidateActions() { for action in actions { // This is just a rescue feature, in case any key stuck pressed for any reason @@ -149,6 +153,15 @@ public class ActionDispatcher { } } + /** + Lift off (release) touch points of actions that moves freely under control of the user. (e.g. camera control action) + Would be called during every mode switching where `invalidateActions` is not called. + In such scenario button-type actions are not released, because users may continue using them across different modes. + (e.g. holding W while unhiding cursor to click something) + + But non-button-type actions (e.g. camera control action, fake mouse action) are unlikely used across modes. + If they're not released, they would interfere with and ruin the game's camera control (becomes random zoom in zoom out) + */ static public func invalidateNonButtonActions() { for action in actions where !(action as? ButtonAction != nil || action is JoystickAction){ From 89bfa2483711d3c2e7e74148de5e66d03024e54e Mon Sep 17 00:00:00 2001 From: Xyct <87l46110@gmail.com> Date: Mon, 27 May 2024 00:37:57 +0800 Subject: [PATCH 3/9] limit touch point inside window & improve swipe begin/end behavior --- .../Controls/Backend/Action/PlayAction.swift | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/PlayTools/Controls/Backend/Action/PlayAction.swift b/PlayTools/Controls/Backend/Action/PlayAction.swift index bc60ddac..a11d0076 100644 --- a/PlayTools/Controls/Backend/Action/PlayAction.swift +++ b/PlayTools/Controls/Backend/Action/PlayAction.swift @@ -355,12 +355,41 @@ class SwipeAction: Action { counter = 0 Toucher.touchcam(point: location, phase: UITouch.Phase.began, tid: &id) timer.resume() + } else { + // 1. Put location update after touch action, so that final `end` touch has different location + // 2. If `began` touched, do not `move` touch at the same time, otherwise the two may conflict + Toucher.touchcam(point: self.location, phase: UITouch.Phase.moved, tid: &id) } // count touch duration counter += 1 - self.location.x += deltaX - self.location.y -= deltaY - Toucher.touchcam(point: self.location, phase: UITouch.Phase.moved, tid: &id) + let newX = self.location.x + deltaX + let newY = self.location.y - deltaY + // Check if new location is out of window (position overflows) + // May cause resource leak in some games if point moves out of window + // If this is out of window then lift off instead + if newX < 0 || newY < 0 || newX > screen.width || newY > screen.height { +// Toast.showHint(title: newX.description + newY.description) + // Move the point as far as possible, in case it always overflows and gets stuck + if newX < 0 { + self.location.x = 0 + } else if newX > screen.width { + self.location.x = screen.width + } else { + self.location.x = newX + } + + if newY < 0 { + self.location.y = 0 + } else if newY > screen.height { + self.location.y = screen.height + } else { + self.location.y = newY + } + doLiftOff() + return + } + self.location.x = newX + self.location.y = newY } public func doLiftOff() { From 4dc3209386f1d0561684bab74d3709f6f55cd4c4 Mon Sep 17 00:00:00 2001 From: Xyct <87l46110@gmail.com> Date: Wed, 29 May 2024 17:28:37 +0800 Subject: [PATCH 4/9] tolerate touch end failure --- .../Controls/Backend/Action/PlayAction.swift | 23 ++++++++++++------- .../Controls/PTFakeTouch/PTFakeMetaTouch.m | 3 ++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/PlayTools/Controls/Backend/Action/PlayAction.swift b/PlayTools/Controls/Backend/Action/PlayAction.swift index a11d0076..d52a02e4 100644 --- a/PlayTools/Controls/Backend/Action/PlayAction.swift +++ b/PlayTools/Controls/Backend/Action/PlayAction.swift @@ -68,10 +68,12 @@ class DraggableButtonAction: ButtonAction { AKInterface.shared!.hideCursor() } } else { - ActionDispatcher.unregister(key: KeyCodeNames.mouseMove) Toucher.touchcam(point: releasePoint, phase: UITouch.Phase.ended, tid: &id) - if !mode.cursorHidden() { - AKInterface.shared!.unhideCursor() + if id == nil { + ActionDispatcher.unregister(key: KeyCodeNames.mouseMove) + if !mode.cursorHidden() { + AKInterface.shared!.unhideCursor() + } } } } @@ -397,11 +399,14 @@ class SwipeAction: Action { return } Toucher.touchcam(point: self.location, phase: UITouch.Phase.ended, tid: &id) - timer.suspend() - delay(0.02) { - self.cooldown = false + // Touch might somehow fail to end + if id == nil { + timer.suspend() + delay(0.02) { + self.cooldown = false + } + cooldown = true } - cooldown = true } func invalidate() { @@ -436,8 +441,10 @@ class FakeMouseAction: Action { // DispatchQueue.main.async { // Toast.showHint(title: " lift Fake mouse", text: ["\(self.pos)"]) // } - ActionDispatcher.unregister(key: KeyCodeNames.fakeMouse) Toucher.touchcam(point: pos, phase: UITouch.Phase.ended, tid: &id) + if id == nil { + ActionDispatcher.unregister(key: KeyCodeNames.fakeMouse) + } } func movementHandler(xValue: CGFloat, yValue: CGFloat) { diff --git a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m index b3e76150..003f3c07 100644 --- a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m +++ b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m @@ -94,7 +94,8 @@ + (NSInteger)fakeTouchId: (NSInteger)pointId AtPoint: (CGPoint)point withTouchPh [touch setPhaseAndUpdateTimestamp:phase]; } CFRunLoopSourceSignal(source); - if(phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled) { + // Set phase might somehow fail + if([touch phase] == UITouchPhaseEnded || [touch phase] == UITouchPhaseCancelled) { pointId = -1; } return pointId; From 588c9821334deacd78e701ad86cd707a5be7207e Mon Sep 17 00:00:00 2001 From: Xyct <87l46110@gmail.com> Date: Thu, 30 May 2024 20:50:43 +0800 Subject: [PATCH 5/9] fix data race in PTFakeTouch --- .../Controls/PTFakeTouch/PTFakeMetaTouch.m | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m index 003f3c07..7ea9469f 100644 --- a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m +++ b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m @@ -11,29 +11,31 @@ #import "UIApplication+Private.h" #import "UIEvent+Private.h" #import "CoreFoundation/CFRunLoop.h" +#import #include #include static NSMutableArray *livingTouchAry; -uint64_t reusageMask = 0; +atomic_ullong reusageMask = ATOMIC_VAR_INIT(0); static CFRunLoopSourceRef source; -static UITouch *toStationarify = NULL; NSLock *lock; void eventSendCallback(void* info) { UIEvent *event = [[UIApplication sharedApplication] _touchesEvent]; [event _clearTouches]; + // Step1: copy touches and record began touches and mark recyclable touches + NSMutableArray *begunTouchAry = [[NSMutableArray alloc] init]; [lock lock]; [livingTouchAry enumerateObjectsUsingBlock:^(UITouch *aTouch, NSUInteger idx, BOOL *stop) { switch (aTouch.phase) { case UITouchPhaseEnded: case UITouchPhaseCancelled: - // set this bit to 0 - reusageMask |= 1ull< Date: Fri, 31 May 2024 00:54:22 +0800 Subject: [PATCH 6/9] fix synchronize region mistake --- PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m index 7ea9469f..c65498ff 100644 --- a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m +++ b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m @@ -51,8 +51,10 @@ void eventSendCallback(void* info) { // Do not let a "began" appear twice on a point for (UITouch *touch in begunTouchAry) { // Double check "began", because phase may have changed - if ([touch phase] == UITouchPhaseBegan) { - @synchronized (touch) { + @synchronized (touch) { + // Check condition needs to be synchronized too, + // otherwise phase might also change after condition met + if ([touch phase] == UITouchPhaseBegan) { [touch setPhaseAndUpdateTimestamp:UITouchPhaseMoved]; } } From 244f503a57f4f8d4b4b0858b49b8fae6a75a8831 Mon Sep 17 00:00:00 2001 From: Xyct <87l46110@gmail.com> Date: Fri, 31 May 2024 14:35:15 +0800 Subject: [PATCH 7/9] lock id occupy and object replace together --- PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m index c65498ff..d4e04233 100644 --- a/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m +++ b/PlayTools/Controls/PTFakeTouch/PTFakeMetaTouch.m @@ -83,6 +83,7 @@ + (NSInteger)fakeTouchId: (NSInteger)pointId AtPoint: (CGPoint)point withTouchPh // Find and clear any 1 bit if possible if(atomic_load(&reusageMask) == 0){ pointId = [livingTouchAry count]; + [lock lock]; }else{ // reuse previous ID pointId = 0; @@ -96,9 +97,12 @@ + (NSInteger)fakeTouchId: (NSInteger)pointId AtPoint: (CGPoint)point withTouchPh // 1. Other thread read // 2. This thread read and write // 3. Other thread write + [lock lock]; atomic_fetch_and(&reusageMask, ~(1ull< Date: Sun, 2 Jun 2024 22:29:00 +0800 Subject: [PATCH 8/9] revert limiting touch inside window --- .../Controls/Backend/Action/PlayAction.swift | 30 ++----------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/PlayTools/Controls/Backend/Action/PlayAction.swift b/PlayTools/Controls/Backend/Action/PlayAction.swift index d52a02e4..e7b05d11 100644 --- a/PlayTools/Controls/Backend/Action/PlayAction.swift +++ b/PlayTools/Controls/Backend/Action/PlayAction.swift @@ -364,34 +364,8 @@ class SwipeAction: Action { } // count touch duration counter += 1 - let newX = self.location.x + deltaX - let newY = self.location.y - deltaY - // Check if new location is out of window (position overflows) - // May cause resource leak in some games if point moves out of window - // If this is out of window then lift off instead - if newX < 0 || newY < 0 || newX > screen.width || newY > screen.height { -// Toast.showHint(title: newX.description + newY.description) - // Move the point as far as possible, in case it always overflows and gets stuck - if newX < 0 { - self.location.x = 0 - } else if newX > screen.width { - self.location.x = screen.width - } else { - self.location.x = newX - } - - if newY < 0 { - self.location.y = 0 - } else if newY > screen.height { - self.location.y = screen.height - } else { - self.location.y = newY - } - doLiftOff() - return - } - self.location.x = newX - self.location.y = newY + self.location.x += deltaX + self.location.y -= deltaY } public func doLiftOff() { From f0b110cf6e831ea7b02bcb4315ed3c47722b5519 Mon Sep 17 00:00:00 2001 From: Xyct <87l46110@gmail.com> Date: Tue, 4 Jun 2024 04:17:54 +0800 Subject: [PATCH 9/9] limit min event count before edge reset --- .../Controls/Backend/Action/PlayAction.swift | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/PlayTools/Controls/Backend/Action/PlayAction.swift b/PlayTools/Controls/Backend/Action/PlayAction.swift index e7b05d11..81ac0e69 100644 --- a/PlayTools/Controls/Backend/Action/PlayAction.swift +++ b/PlayTools/Controls/Backend/Action/PlayAction.swift @@ -335,6 +335,7 @@ class SwipeAction: Action { // if should wait before beginning next touch var cooldown = false var lastCounter = 0 + var shouldEdgeReset = false func checkEnded() { if self.counter == self.lastCounter { @@ -347,6 +348,34 @@ class SwipeAction: Action { self.lastCounter = self.counter } + private func checkXYOutOfWindow(coordX: CGFloat, coordY: CGFloat) -> Bool { + return coordX < 0 || coordY < 0 || coordX > screen.width || coordY > screen.height + } + + /** + get a multiplier to current velocity, so as to make the predicted coordinate inside window + */ + private func getVelocityScaler(predictX: CGFloat, predictY: CGFloat, + nowX: CGFloat, nowY: CGFloat) -> CGFloat { + var scaler = 1.0 + if predictX < 0 { + let scale = (0 - nowX) / (predictX - nowX) + scaler = min(scaler, scale) + } else if predictX > screen.width { + let scale = (screen.width - nowX) / (predictX - nowX) + scaler = min(scaler, scale) + } + + if predictY < 0 { + let scale = (0 - nowY) / (predictY - nowY) + scaler = min(scaler, scale) + } else if predictY > screen.height { + let scale = (screen.height - nowY) / (predictY - nowY) + scaler = min(scaler, scale) + } + return scaler + } + public func move(from: () -> CGPoint?, deltaX: CGFloat, deltaY: CGFloat) { if id == nil { if cooldown { @@ -358,14 +387,43 @@ class SwipeAction: Action { Toucher.touchcam(point: location, phase: UITouch.Phase.began, tid: &id) timer.resume() } else { + if shouldEdgeReset { + doLiftOff() + return + } // 1. Put location update after touch action, so that final `end` touch has different location // 2. If `began` touched, do not `move` touch at the same time, otherwise the two may conflict Toucher.touchcam(point: self.location, phase: UITouch.Phase.moved, tid: &id) } + // Scale movement down, so that an edge reset won't cause a too short touch sequence + var scaledDeltaX = deltaX + var scaledDeltaY = deltaY + // A scroll must have this number of touch events to get inertia + let minTotalCounter = 16 + if counter < minTotalCounter { + // Suppose the touch velocity doesn't change + let predictX = self.location.x + CGFloat((minTotalCounter - counter)) * deltaX + let predictY = self.location.y - CGFloat((minTotalCounter - counter)) * deltaY + if checkXYOutOfWindow(coordX: predictX, coordY: predictY) { + // Velocity needs scale down + let scaler = getVelocityScaler(predictX: predictX, predictY: predictY, + nowX: self.location.x, nowY: self.location.y) + scaledDeltaX *= scaler + scaledDeltaY *= scaler + } + } // count touch duration counter += 1 - self.location.x += deltaX - self.location.y -= deltaY + self.location.x += scaledDeltaX + self.location.y -= scaledDeltaY + // Check if new location is out of window (position overflows) + // May fail in some games if point moves out of window + // If next touch is predicted out of window then this lift off instead + if checkXYOutOfWindow(coordX: self.location.x + scaledDeltaX, + coordY: self.location.y - scaledDeltaY) { + // Wait until next event to lift off, so as to maintain smooth scrolling speed + shouldEdgeReset = true + } } public func doLiftOff() { @@ -380,6 +438,7 @@ class SwipeAction: Action { self.cooldown = false } cooldown = true + shouldEdgeReset = false } }