-
Notifications
You must be signed in to change notification settings - Fork 51
[+] PDX ExclusiveTouch #110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,53 +1,18 @@ | ||
| using System; | ||
| using AquaMai.Config.Attributes; | ||
| using LibUsbDotNet.Main; | ||
| using LibUsbDotNet; | ||
| using MelonLoader; | ||
| using UnityEngine; | ||
| using AquaMai.Core.Helpers; | ||
| using System.Threading; | ||
| using JetBrains.Annotations; | ||
|
|
||
| namespace AquaMai.Mods.GameSystem.ExclusiveTouch; | ||
|
|
||
| [ConfigCollapseNamespace] | ||
| [ConfigSection(exampleHidden: true)] | ||
| public class ExclusiveTouch | ||
| public abstract class ExclusiveTouchBase(int playerNo, int vid, int pid, [CanBeNull] string serialNumber, byte configuration, int interfaceNumber, ReadEndpointID endpoint, int packetSize, int minX, int minY, int maxX, int maxY, bool flip, int radius) | ||
| { | ||
| [ConfigEntry] | ||
| public static readonly bool enable1p; | ||
|
|
||
| [ConfigEntry] | ||
| public static readonly int vid1p; | ||
| [ConfigEntry] | ||
| public static readonly int pid1p; | ||
| [ConfigEntry] | ||
| public static readonly string serialNumber1p = ""; | ||
| [ConfigEntry] | ||
| public static readonly byte configuration1p = 1; | ||
| [ConfigEntry] | ||
| public static readonly int interfaceNumber1p = 0; | ||
| [ConfigEntry] | ||
| public static readonly int reportId1p; | ||
| [ConfigEntry] | ||
| public static readonly ReadEndpointID endpoint1p = ReadEndpointID.Ep01; | ||
| [ConfigEntry] | ||
| public static readonly int packetSize1p = 64; | ||
| [ConfigEntry] | ||
| public static readonly int minX1p; | ||
| [ConfigEntry] | ||
| public static readonly int minY1p; | ||
| [ConfigEntry] | ||
| public static readonly int maxX1p; | ||
| [ConfigEntry] | ||
| public static readonly int maxY1p; | ||
| [ConfigEntry] | ||
| public static readonly bool flip1p; | ||
|
|
||
| [ConfigEntry("触摸体积半径", zh: "基准是 1440x1440")] | ||
| public static readonly int radius1p; | ||
|
|
||
| private static UsbDevice[] devices = new UsbDevice[2]; | ||
| private static TouchSensorMapper[] touchSensorMappers = new TouchSensorMapper[2]; | ||
| private UsbDevice device; | ||
| private TouchSensorMapper touchSensorMapper; | ||
|
|
||
| private class TouchPoint | ||
| { | ||
|
|
@@ -56,65 +21,60 @@ private class TouchPoint | |
| public bool IsActive; | ||
| } | ||
|
|
||
| // [玩家][手指ID] | ||
| private static readonly TouchPoint[][] allFingerPoints = new TouchPoint[2][]; | ||
| // [手指ID] | ||
| private readonly TouchPoint[] allFingerPoints = new TouchPoint[256]; | ||
|
|
||
| // 防吃键 | ||
| private static readonly ulong[] frameAccumulators = new ulong[2]; | ||
| private static readonly object[] touchLocks = [new object(), new object()]; | ||
| private ulong frameAccumulators; | ||
| private readonly object touchLock = new(); | ||
|
|
||
| private const int TouchTimeoutMs = 20; | ||
|
|
||
| public static void OnBeforePatch() | ||
| public void Start() | ||
| { | ||
| if (enable1p) | ||
| // 方便组 2P | ||
| var finder = new UsbDeviceFinder(vid, pid, string.IsNullOrWhiteSpace(serialNumber) ? null : serialNumber); | ||
| device = UsbDevice.OpenUsbDevice(finder); | ||
| if (device == null) | ||
| { | ||
| // 方便组 2P | ||
| var serialNumber = string.IsNullOrWhiteSpace(serialNumber1p) ? null : serialNumber1p; | ||
| var finder = new UsbDeviceFinder(vid1p, pid1p, serialNumber); | ||
| var device = UsbDevice.OpenUsbDevice(finder); | ||
| if (device == null) | ||
| MelonLogger.Msg("[ExclusiveTouch] Cannot connect 1P"); | ||
| } | ||
| else | ||
| { | ||
| IUsbDevice wholeDevice = device as IUsbDevice; | ||
| if (wholeDevice != null) | ||
| { | ||
| MelonLogger.Msg("[ExclusiveTouch] Cannot connect 1P"); | ||
| wholeDevice.SetConfiguration(configuration); | ||
| wholeDevice.ClaimInterface(interfaceNumber); | ||
| } | ||
| else | ||
| touchSensorMapper = new TouchSensorMapper(minX, minY, maxX, maxY, radius, flip); | ||
| Application.quitting += () => | ||
| { | ||
| IUsbDevice wholeDevice = device as IUsbDevice; | ||
| var tmpDevice = device; | ||
| device = null; | ||
| if (wholeDevice != null) | ||
| { | ||
| wholeDevice.SetConfiguration(configuration1p); | ||
| wholeDevice.ClaimInterface(interfaceNumber1p); | ||
| } | ||
| touchSensorMappers[0] = new TouchSensorMapper(minX1p, minY1p, maxX1p, maxY1p, radius1p, flip1p); | ||
| Application.quitting += () => | ||
| { | ||
| devices[0] = null; | ||
| if (wholeDevice != null) | ||
| { | ||
| wholeDevice.ReleaseInterface(interfaceNumber1p); | ||
| } | ||
| device.Close(); | ||
| }; | ||
|
|
||
| allFingerPoints[0] = new TouchPoint[256]; | ||
| for (int i = 0; i < 256; i++) | ||
| { | ||
| allFingerPoints[0][i] = new TouchPoint(); | ||
| wholeDevice.ReleaseInterface(interfaceNumber); | ||
| } | ||
| tmpDevice.Close(); | ||
| }; | ||
|
Comment on lines
+68
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the Application.quitting += () =>
{
var tmpDevice = device;
device = null;
try
{
if (wholeDevice != null)
{
wholeDevice.ReleaseInterface(interfaceNumber);
}
}
finally
{
tmpDevice.Close();
}
}; |
||
|
|
||
| devices[0] = device; | ||
| Thread readThread = new Thread(() => ReadThread(0)); | ||
| readThread.Start(); | ||
| TouchStatusProvider.RegisterTouchStatusProvider(0, GetTouchState); | ||
| for (int i = 0; i < 256; i++) | ||
| { | ||
| allFingerPoints[i] = new TouchPoint(); | ||
| } | ||
|
|
||
| Thread readThread = new(ReadThread); | ||
| readThread.Start(); | ||
| TouchStatusProvider.RegisterTouchStatusProvider(playerNo, GetTouchState); | ||
| } | ||
| } | ||
|
|
||
| private static void ReadThread(int playerNo) | ||
| private void ReadThread() | ||
| { | ||
| byte[] buffer = new byte[packetSize1p]; | ||
| var reader = devices[playerNo].OpenEndpointReader(endpoint1p); | ||
| while (devices[playerNo] != null) | ||
| byte[] buffer = new byte[packetSize]; | ||
| var reader = device.OpenEndpointReader(endpoint); | ||
| while (device != null) | ||
| { | ||
| int bytesRead; | ||
| ErrorCode ec = reader.Read(buffer, 100, out bytesRead); // 100ms 超时 | ||
|
|
@@ -128,120 +88,72 @@ private static void ReadThread(int playerNo) | |
|
|
||
| if (bytesRead > 0) | ||
| { | ||
| OnTouchData(playerNo, buffer); | ||
| OnTouchData(buffer); | ||
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| private static void OnTouchData(int playerNo, byte[] data) | ||
| { | ||
| byte reportId = data[0]; | ||
| if (reportId != reportId1p) return; | ||
| protected abstract void OnTouchData(byte[] data); | ||
|
|
||
| #if true // PDX | ||
| for (int i = 0; i < 10; i++) | ||
| { | ||
| var index = i * 6 + 1; | ||
| if (data[index] == 0) continue; | ||
| bool isPressed = (data[index] & 0x01) == 1; | ||
| var fingerId = data[index + 1]; | ||
| ushort x = BitConverter.ToUInt16(data, index + 2); | ||
| ushort y = BitConverter.ToUInt16(data, index + 4); | ||
| HandleFinger(x, y, fingerId, isPressed, playerNo); | ||
| } | ||
| #else // 凌莞的便携屏 | ||
| // 解析第一根手指 | ||
| if (data.Length >= 7) | ||
| { | ||
| byte status1 = data[1]; | ||
| int fingerId1 = (status1 >> 4) & 0x0F; // 高4位:手指ID | ||
| bool isPressed1 = (status1 & 0x01) == 1; // 低位:按下状态 | ||
| ushort x1 = BitConverter.ToUInt16(data, 2); | ||
| ushort y1 = BitConverter.ToUInt16(data, 4); | ||
|
|
||
| HandleFinger(x1, y1, fingerId1, isPressed1, playerNo); | ||
| } | ||
|
|
||
| // 解析第二根手指 | ||
| if (data.Length >= 14) | ||
| { | ||
| byte status2 = data[6]; | ||
| int fingerId2 = (status2 >> 4) & 0x0F; | ||
| bool isPressed2 = (status2 & 0x01) == 1; | ||
| ushort x2 = BitConverter.ToUInt16(data, 7); | ||
| ushort y2 = BitConverter.ToUInt16(data, 9); | ||
|
|
||
| // 只有坐标非零才处理第二根手指 | ||
| if (x2 != 0 || y2 != 0) | ||
| { | ||
| HandleFinger(x2, y2, fingerId2, isPressed2, playerNo); | ||
| } | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| private static void HandleFinger(ushort x, ushort y, int fingerId, bool isPressed, int playerNo) | ||
| protected void HandleFinger(ushort x, ushort y, int fingerId, bool isPressed) | ||
| { | ||
| // 安全检查,防止越界 | ||
| if (fingerId < 0 || fingerId >= 256) return; | ||
|
|
||
| lock (touchLocks[playerNo]) | ||
| lock (touchLock) | ||
| { | ||
| var point = allFingerPoints[playerNo][fingerId]; | ||
| var point = allFingerPoints[fingerId]; | ||
|
|
||
| if (isPressed) | ||
| { | ||
| ulong touchMask = touchSensorMappers[playerNo].ParseTouchPoint(x, y); | ||
| ulong touchMask = touchSensorMapper.ParseTouchPoint(x, y); | ||
|
|
||
| if (!point.IsActive) | ||
| { | ||
| point.IsActive = true; | ||
| MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{fingerId} 按下 at ({x}, {y}) -> 0x{touchMask:X}"); | ||
| } | ||
|
|
||
| point.Mask = touchMask; | ||
| point.LastUpdate = DateTime.Now; | ||
| frameAccumulators[playerNo] |= touchMask; | ||
|
|
||
| frameAccumulators |= touchMask; | ||
| } | ||
| else | ||
| { | ||
| if (point.IsActive) | ||
| { | ||
| point.IsActive = false; | ||
| MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{fingerId} 松开"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static ulong GetTouchState(int playerNo) | ||
| private ulong GetTouchState(int player) | ||
| { | ||
| lock (touchLocks[playerNo]) | ||
| if (player != playerNo) return 0; | ||
| lock (touchLock) | ||
| { | ||
| ulong currentTouchData = 0; | ||
| var now = DateTime.Now; | ||
| var points = allFingerPoints[playerNo]; | ||
|
|
||
| for (int i = 0; i < points.Length; i++) | ||
| for (int i = 0; i < allFingerPoints.Length; i++) | ||
| { | ||
| var point = points[i]; | ||
| var point = allFingerPoints[i]; | ||
| if (point.IsActive) | ||
| { | ||
| if ((now - point.LastUpdate).TotalMilliseconds > TouchTimeoutMs) | ||
| { | ||
| point.IsActive = false; | ||
| MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{i} 超时自动释放"); | ||
| } | ||
| else | ||
| { | ||
| currentTouchData |= point.Mask; | ||
| } | ||
| } | ||
| } | ||
| ulong finalResult = currentTouchData | frameAccumulators[playerNo]; | ||
| frameAccumulators[playerNo] = 0; | ||
|
|
||
| ulong finalResult = currentTouchData | frameAccumulators; | ||
| frameAccumulators = 0; | ||
|
|
||
| return finalResult; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| using System; | ||
| using AquaMai.Config.Attributes; | ||
| using AquaMai.Mods.GameSystem.ExclusiveTouch; | ||
| using LibUsbDotNet.Main; | ||
|
|
||
| namespace AquaMai.Mods.GameSystem; | ||
|
|
||
| [ConfigSection("PDX 独占触摸")] | ||
| public class PdxTouch | ||
| { | ||
| [ConfigEntry("触摸体积半径", zh: "基准是 1440x1440")] | ||
| public static readonly int radius = 30; | ||
|
|
||
| [ConfigEntry("1P 序列号", zh: "如果要组 2P,请在这里指定对应的序列号。否则将自动使用第一个检测到的设备作为 1P")] | ||
| public static readonly string serial1p; | ||
| [ConfigEntry("2P 序列号")] | ||
| public static readonly string serial2p; | ||
|
|
||
| private static readonly PdxTouchDevice[] devices = new PdxTouchDevice[2]; | ||
|
|
||
| public static void OnBeforePatch() | ||
| { | ||
| if (string.IsNullOrWhiteSpace(serial1p) && string.IsNullOrWhiteSpace(serial2p)) | ||
| { | ||
| devices[0] = new PdxTouchDevice(0, null); | ||
| devices[0].Start(); | ||
| } | ||
| if (!string.IsNullOrWhiteSpace(serial1p)) | ||
| { | ||
| devices[0] = new PdxTouchDevice(0, serial1p); | ||
| devices[0].Start(); | ||
| } | ||
| if (!string.IsNullOrWhiteSpace(serial2p)) | ||
| { | ||
| devices[1] = new PdxTouchDevice(1, serial2p); | ||
| devices[1].Start(); | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
Comment on lines
22
to
44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for initializing devices in public static void OnBeforePatch()
{
// Initialize 1P device if serial1p is provided, or if no serials are provided at all (auto-detect mode).
// This logic does not initialize 1P if only 2P serial is provided, matching original behavior.
if (!string.IsNullOrWhiteSpace(serial1p) || string.IsNullOrWhiteSpace(serial2p))
{
devices[0] = new PdxTouchDevice(0, serial1p);
devices[0].Start();
}
// Initialize 2P device if serial2p is provided.
if (!string.IsNullOrWhiteSpace(serial2p))
{
devices[1] = new PdxTouchDevice(1, serial2p);
devices[1].Start();
}
}
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这样岂不是更绕 |
||
|
|
||
| private class PdxTouchDevice(int playerNo, string serialNumber) : ExclusiveTouchBase(playerNo, vid: 0x3356, pid: 0x3003, serialNumber, configuration: 1, interfaceNumber: 1, ReadEndpointID.Ep02, packetSize: 64, minX: 18432, minY: 0, maxX: 0, maxY: 32767, flip: true, radius) | ||
|
||
| { | ||
| private const byte ReportId = 2; | ||
| protected override void OnTouchData(byte[] data) | ||
| { | ||
| byte reportId = data[0]; | ||
| if (reportId != ReportId) return; | ||
|
|
||
| for (int i = 0; i < 10; i++) | ||
| { | ||
| var index = i * 6 + 1; | ||
| if (data[index] == 0) continue; | ||
| bool isPressed = (data[index] & 0x01) == 1; | ||
| var fingerId = data[index + 1]; | ||
| ushort x = BitConverter.ToUInt16(data, index + 2); | ||
| ushort y = BitConverter.ToUInt16(data, index + 4); | ||
| HandleFinger(x, y, fingerId, isPressed); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.