Skip to content

Commit 3c40b0c

Browse files
authored
[+] PDX ExclusiveTouch (#110)
* [+] PDX ExclusiveTouch * fix build * [F] null default value * 设备路径查找 * fix agent warning
1 parent 92dfbde commit 3c40b0c

File tree

4 files changed

+341
-156
lines changed

4 files changed

+341
-156
lines changed

AquaMai.Mods/GameSystem/ExclusiveTouch/ExclusiveTouch.cs

Lines changed: 90 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,18 @@
11
using System;
2-
using AquaMai.Config.Attributes;
32
using LibUsbDotNet.Main;
43
using LibUsbDotNet;
54
using MelonLoader;
65
using UnityEngine;
76
using AquaMai.Core.Helpers;
87
using System.Threading;
8+
using JetBrains.Annotations;
99

1010
namespace AquaMai.Mods.GameSystem.ExclusiveTouch;
1111

12-
[ConfigCollapseNamespace]
13-
[ConfigSection(exampleHidden: true)]
14-
public class ExclusiveTouch
12+
public abstract class ExclusiveTouchBase(int playerNo, int vid, int pid, [CanBeNull] string serialNumber, [CanBeNull] string locationPath, byte configuration, int interfaceNumber, ReadEndpointID endpoint, int packetSize, int minX, int minY, int maxX, int maxY, bool flip, int radius)
1513
{
16-
[ConfigEntry]
17-
public static readonly bool enable1p;
18-
19-
[ConfigEntry]
20-
public static readonly int vid1p;
21-
[ConfigEntry]
22-
public static readonly int pid1p;
23-
[ConfigEntry]
24-
public static readonly string serialNumber1p = "";
25-
[ConfigEntry]
26-
public static readonly byte configuration1p = 1;
27-
[ConfigEntry]
28-
public static readonly int interfaceNumber1p = 0;
29-
[ConfigEntry]
30-
public static readonly int reportId1p;
31-
[ConfigEntry]
32-
public static readonly ReadEndpointID endpoint1p = ReadEndpointID.Ep01;
33-
[ConfigEntry]
34-
public static readonly int packetSize1p = 64;
35-
[ConfigEntry]
36-
public static readonly int minX1p;
37-
[ConfigEntry]
38-
public static readonly int minY1p;
39-
[ConfigEntry]
40-
public static readonly int maxX1p;
41-
[ConfigEntry]
42-
public static readonly int maxY1p;
43-
[ConfigEntry]
44-
public static readonly bool flip1p;
45-
46-
[ConfigEntry("触摸体积半径", zh: "基准是 1440x1440")]
47-
public static readonly int radius1p;
48-
49-
private static UsbDevice[] devices = new UsbDevice[2];
50-
private static TouchSensorMapper[] touchSensorMappers = new TouchSensorMapper[2];
14+
private UsbDevice device;
15+
private TouchSensorMapper touchSensorMapper;
5116

5217
private class TouchPoint
5318
{
@@ -56,192 +21,165 @@ private class TouchPoint
5621
public bool IsActive;
5722
}
5823

59-
// [玩家][手指ID]
60-
private static readonly TouchPoint[][] allFingerPoints = new TouchPoint[2][];
24+
// [手指ID]
25+
private readonly TouchPoint[] allFingerPoints = new TouchPoint[256];
6126

6227
// 防吃键
63-
private static readonly ulong[] frameAccumulators = new ulong[2];
64-
private static readonly object[] touchLocks = [new object(), new object()];
28+
private ulong frameAccumulators;
29+
private readonly object touchLock = new();
6530

6631
private const int TouchTimeoutMs = 20;
6732

68-
public static void OnBeforePatch()
33+
public void Start()
6934
{
70-
if (enable1p)
35+
// 方便组 2P
36+
UsbDeviceFinder finder;
37+
38+
if (!string.IsNullOrWhiteSpace(serialNumber))
7139
{
72-
// 方便组 2P
73-
var serialNumber = string.IsNullOrWhiteSpace(serialNumber1p) ? null : serialNumber1p;
74-
var finder = new UsbDeviceFinder(vid1p, pid1p, serialNumber);
75-
var device = UsbDevice.OpenUsbDevice(finder);
76-
if (device == null)
40+
// 优先使用序列号
41+
finder = new UsbDeviceFinder(vid, pid, serialNumber);
42+
}
43+
else if (!string.IsNullOrWhiteSpace(locationPath))
44+
{
45+
// 使用位置路径匹配
46+
finder = new UsbDeviceLocationFinder(vid, pid, locationPath);
47+
}
48+
else
49+
{
50+
// 使用第一个匹配的设备
51+
finder = new UsbDeviceFinder(vid, pid);
52+
}
53+
54+
device = UsbDevice.OpenUsbDevice(finder);
55+
if (device == null)
56+
{
57+
MelonLogger.Msg($"[ExclusiveTouch] Cannot connect {playerNo + 1}P");
58+
}
59+
else
60+
{
61+
IUsbDevice wholeDevice = device as IUsbDevice;
62+
if (wholeDevice != null)
7763
{
78-
MelonLogger.Msg("[ExclusiveTouch] Cannot connect 1P");
64+
wholeDevice.SetConfiguration(configuration);
65+
wholeDevice.ClaimInterface(interfaceNumber);
7966
}
80-
else
67+
touchSensorMapper = new TouchSensorMapper(minX, minY, maxX, maxY, radius, flip);
68+
Application.quitting += () =>
8169
{
82-
IUsbDevice wholeDevice = device as IUsbDevice;
70+
var tmpDevice = device;
71+
device = null;
8372
if (wholeDevice != null)
8473
{
85-
wholeDevice.SetConfiguration(configuration1p);
86-
wholeDevice.ClaimInterface(interfaceNumber1p);
87-
}
88-
touchSensorMappers[0] = new TouchSensorMapper(minX1p, minY1p, maxX1p, maxY1p, radius1p, flip1p);
89-
Application.quitting += () =>
90-
{
91-
devices[0] = null;
92-
if (wholeDevice != null)
93-
{
94-
wholeDevice.ReleaseInterface(interfaceNumber1p);
95-
}
96-
device.Close();
97-
};
98-
99-
allFingerPoints[0] = new TouchPoint[256];
100-
for (int i = 0; i < 256; i++)
101-
{
102-
allFingerPoints[0][i] = new TouchPoint();
74+
wholeDevice.ReleaseInterface(interfaceNumber);
10375
}
76+
tmpDevice.Close();
77+
};
10478

105-
devices[0] = device;
106-
Thread readThread = new Thread(() => ReadThread(0));
107-
readThread.Start();
108-
TouchStatusProvider.RegisterTouchStatusProvider(0, GetTouchState);
109-
}
110-
}
111-
}
112-
113-
private static void ReadThread(int playerNo)
114-
{
115-
byte[] buffer = new byte[packetSize1p];
116-
var reader = devices[playerNo].OpenEndpointReader(endpoint1p);
117-
while (devices[playerNo] != null)
118-
{
119-
int bytesRead;
120-
ErrorCode ec = reader.Read(buffer, 100, out bytesRead); // 100ms 超时
121-
122-
if (ec != ErrorCode.None)
79+
for (int i = 0; i < 256; i++)
12380
{
124-
if (ec == ErrorCode.IoTimedOut) continue; // 超时就继续等
125-
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 读取错误: {ec}");
126-
break;
81+
allFingerPoints[i] = new TouchPoint();
12782
}
12883

129-
if (bytesRead > 0)
130-
{
131-
OnTouchData(playerNo, buffer);
132-
}
84+
Thread readThread = new(ReadThread);
85+
readThread.Start();
86+
TouchStatusProvider.RegisterTouchStatusProvider(playerNo, GetTouchState);
13387
}
13488
}
13589

136-
private static void OnTouchData(int playerNo, byte[] data)
90+
private void ReadThread()
13791
{
138-
byte reportId = data[0];
139-
if (reportId != reportId1p) return;
140-
141-
#if true // PDX
142-
for (int i = 0; i < 10; i++)
143-
{
144-
var index = i * 6 + 1;
145-
if (data[index] == 0) continue;
146-
bool isPressed = (data[index] & 0x01) == 1;
147-
var fingerId = data[index + 1];
148-
ushort x = BitConverter.ToUInt16(data, index + 2);
149-
ushort y = BitConverter.ToUInt16(data, index + 4);
150-
HandleFinger(x, y, fingerId, isPressed, playerNo);
151-
}
152-
#else // 凌莞的便携屏
153-
// 解析第一根手指
154-
if (data.Length >= 7)
92+
byte[] buffer = new byte[packetSize];
93+
var reader = device.OpenEndpointReader(endpoint);
94+
95+
try
15596
{
156-
byte status1 = data[1];
157-
int fingerId1 = (status1 >> 4) & 0x0F; // 高4位:手指ID
158-
bool isPressed1 = (status1 & 0x01) == 1; // 低位:按下状态
159-
ushort x1 = BitConverter.ToUInt16(data, 2);
160-
ushort y1 = BitConverter.ToUInt16(data, 4);
97+
while (device != null)
98+
{
99+
int bytesRead;
100+
ErrorCode ec = reader.Read(buffer, 100, out bytesRead); // 100ms 超时
161101

162-
HandleFinger(x1, y1, fingerId1, isPressed1, playerNo);
163-
}
102+
if (ec != ErrorCode.None)
103+
{
104+
if (ec == ErrorCode.IoTimedOut) continue; // 超时就继续等
105+
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 读取错误: {ec}");
106+
break;
107+
}
164108

165-
// 解析第二根手指
166-
if (data.Length >= 14)
167-
{
168-
byte status2 = data[6];
169-
int fingerId2 = (status2 >> 4) & 0x0F;
170-
bool isPressed2 = (status2 & 0x01) == 1;
171-
ushort x2 = BitConverter.ToUInt16(data, 7);
172-
ushort y2 = BitConverter.ToUInt16(data, 9);
173-
174-
// 只有坐标非零才处理第二根手指
175-
if (x2 != 0 || y2 != 0)
176-
{
177-
HandleFinger(x2, y2, fingerId2, isPressed2, playerNo);
109+
if (bytesRead > 0)
110+
{
111+
OnTouchData(buffer);
112+
}
178113
}
179114
}
180-
#endif
115+
finally
116+
{
117+
// 确保 reader 被正确释放
118+
reader?.Dispose();
119+
}
181120
}
182121

183-
private static void HandleFinger(ushort x, ushort y, int fingerId, bool isPressed, int playerNo)
122+
protected abstract void OnTouchData(byte[] data);
123+
124+
protected void HandleFinger(ushort x, ushort y, int fingerId, bool isPressed)
184125
{
185126
// 安全检查,防止越界
186127
if (fingerId < 0 || fingerId >= 256) return;
187128

188-
lock (touchLocks[playerNo])
129+
lock (touchLock)
189130
{
190-
var point = allFingerPoints[playerNo][fingerId];
131+
var point = allFingerPoints[fingerId];
191132

192133
if (isPressed)
193134
{
194-
ulong touchMask = touchSensorMappers[playerNo].ParseTouchPoint(x, y);
135+
ulong touchMask = touchSensorMapper.ParseTouchPoint(x, y);
195136

196137
if (!point.IsActive)
197138
{
198139
point.IsActive = true;
199-
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{fingerId} 按下 at ({x}, {y}) -> 0x{touchMask:X}");
200140
}
201141

202142
point.Mask = touchMask;
203143
point.LastUpdate = DateTime.Now;
204-
205-
frameAccumulators[playerNo] |= touchMask;
144+
145+
frameAccumulators |= touchMask;
206146
}
207147
else
208148
{
209149
if (point.IsActive)
210150
{
211151
point.IsActive = false;
212-
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{fingerId} 松开");
213152
}
214153
}
215154
}
216155
}
217156

218-
public static ulong GetTouchState(int playerNo)
157+
private ulong GetTouchState(int player)
219158
{
220-
lock (touchLocks[playerNo])
159+
if (player != playerNo) return 0;
160+
lock (touchLock)
221161
{
222162
ulong currentTouchData = 0;
223163
var now = DateTime.Now;
224-
var points = allFingerPoints[playerNo];
225164

226-
for (int i = 0; i < points.Length; i++)
165+
for (int i = 0; i < allFingerPoints.Length; i++)
227166
{
228-
var point = points[i];
167+
var point = allFingerPoints[i];
229168
if (point.IsActive)
230169
{
231170
if ((now - point.LastUpdate).TotalMilliseconds > TouchTimeoutMs)
232171
{
233172
point.IsActive = false;
234-
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{i} 超时自动释放");
235173
}
236174
else
237175
{
238176
currentTouchData |= point.Mask;
239177
}
240178
}
241179
}
242-
243-
ulong finalResult = currentTouchData | frameAccumulators[playerNo];
244-
frameAccumulators[playerNo] = 0;
180+
181+
ulong finalResult = currentTouchData | frameAccumulators;
182+
frameAccumulators = 0;
245183

246184
return finalResult;
247185
}

AquaMai.Mods/GameSystem/ExclusiveTouch/TouchSensorMapper.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,15 +231,27 @@ public ulong ParseTouchPoint(float x, float y)
231231
// 检查所有传感器
232232
for (int i = 0; i < 34; i++)
233233
{
234-
bool isInsidePolygon = PolygonRaycasting.IsVertDistance(_sensors[i], canvasPoint, radius);
235-
if (!isInsidePolygon)
234+
bool isInsidePolygon;
235+
236+
if (radius > 0)
236237
{
237-
isInsidePolygon = PolygonRaycasting.IsCircleIntersectingPolygonEdges(_sensors[i], canvasPoint, radius);
238+
// 当有半径时,需要检查圆与多边形的关系
239+
isInsidePolygon = PolygonRaycasting.IsVertDistance(_sensors[i], canvasPoint, radius);
240+
if (!isInsidePolygon)
241+
{
242+
isInsidePolygon = PolygonRaycasting.IsCircleIntersectingPolygonEdges(_sensors[i], canvasPoint, radius);
243+
}
244+
if (!isInsidePolygon)
245+
{
246+
isInsidePolygon = PolygonRaycasting.InPointInInternal(_sensors[i], canvasPoint);
247+
}
238248
}
239-
if (!isInsidePolygon)
249+
else
240250
{
251+
// 当半径为0时,只需要检查点是否在多边形内部
241252
isInsidePolygon = PolygonRaycasting.InPointInInternal(_sensors[i], canvasPoint);
242253
}
254+
243255
if (isInsidePolygon)
244256
{
245257
res |= 1ul << i;

0 commit comments

Comments
 (0)