Skip to content

Commit e931346

Browse files
committed
[+] PDX ExclusiveTouch
1 parent 92dfbde commit e931346

File tree

3 files changed

+131
-147
lines changed

3 files changed

+131
-147
lines changed

AquaMai.Mods/GameSystem/ExclusiveTouch/ExclusiveTouch.cs

Lines changed: 55 additions & 143 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, 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,65 +21,60 @@ 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+
var finder = new UsbDeviceFinder(vid, pid, string.IsNullOrWhiteSpace(serialNumber) ? null : serialNumber);
37+
device = UsbDevice.OpenUsbDevice(finder);
38+
if (device == null)
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+
MelonLogger.Msg("[ExclusiveTouch] Cannot connect 1P");
41+
}
42+
else
43+
{
44+
IUsbDevice wholeDevice = device as IUsbDevice;
45+
if (wholeDevice != null)
7746
{
78-
MelonLogger.Msg("[ExclusiveTouch] Cannot connect 1P");
47+
wholeDevice.SetConfiguration(configuration);
48+
wholeDevice.ClaimInterface(interfaceNumber);
7949
}
80-
else
50+
touchSensorMapper = new TouchSensorMapper(minX, minY, maxX, maxY, radius, flip);
51+
Application.quitting += () =>
8152
{
82-
IUsbDevice wholeDevice = device as IUsbDevice;
53+
var tmpDevice = device;
54+
device = null;
8355
if (wholeDevice != null)
8456
{
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();
57+
wholeDevice.ReleaseInterface(interfaceNumber);
10358
}
59+
tmpDevice.Close();
60+
};
10461

105-
devices[0] = device;
106-
Thread readThread = new Thread(() => ReadThread(0));
107-
readThread.Start();
108-
TouchStatusProvider.RegisterTouchStatusProvider(0, GetTouchState);
62+
for (int i = 0; i < 256; i++)
63+
{
64+
allFingerPoints[i] = new TouchPoint();
10965
}
66+
67+
Thread readThread = new(ReadThread);
68+
readThread.Start();
69+
TouchStatusProvider.RegisterTouchStatusProvider(playerNo, GetTouchState);
11070
}
11171
}
11272

113-
private static void ReadThread(int playerNo)
73+
private void ReadThread()
11474
{
115-
byte[] buffer = new byte[packetSize1p];
116-
var reader = devices[playerNo].OpenEndpointReader(endpoint1p);
117-
while (devices[playerNo] != null)
75+
byte[] buffer = new byte[packetSize];
76+
var reader = device.OpenEndpointReader(endpoint);
77+
while (device != null)
11878
{
11979
int bytesRead;
12080
ErrorCode ec = reader.Read(buffer, 100, out bytesRead); // 100ms 超时
@@ -128,120 +88,72 @@ private static void ReadThread(int playerNo)
12888

12989
if (bytesRead > 0)
13090
{
131-
OnTouchData(playerNo, buffer);
91+
OnTouchData(buffer);
13292
}
13393
}
13494
}
13595

136-
private static void OnTouchData(int playerNo, byte[] data)
137-
{
138-
byte reportId = data[0];
139-
if (reportId != reportId1p) return;
96+
protected abstract void OnTouchData(byte[] data);
14097

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)
155-
{
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);
161-
162-
HandleFinger(x1, y1, fingerId1, isPressed1, playerNo);
163-
}
164-
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);
178-
}
179-
}
180-
#endif
181-
}
182-
183-
private static void HandleFinger(ushort x, ushort y, int fingerId, bool isPressed, int playerNo)
98+
protected void HandleFinger(ushort x, ushort y, int fingerId, bool isPressed)
18499
{
185100
// 安全检查,防止越界
186101
if (fingerId < 0 || fingerId >= 256) return;
187102

188-
lock (touchLocks[playerNo])
103+
lock (touchLock)
189104
{
190-
var point = allFingerPoints[playerNo][fingerId];
105+
var point = allFingerPoints[fingerId];
191106

192107
if (isPressed)
193108
{
194-
ulong touchMask = touchSensorMappers[playerNo].ParseTouchPoint(x, y);
109+
ulong touchMask = touchSensorMapper.ParseTouchPoint(x, y);
195110

196111
if (!point.IsActive)
197112
{
198113
point.IsActive = true;
199-
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{fingerId} 按下 at ({x}, {y}) -> 0x{touchMask:X}");
200114
}
201115

202116
point.Mask = touchMask;
203117
point.LastUpdate = DateTime.Now;
204-
205-
frameAccumulators[playerNo] |= touchMask;
118+
119+
frameAccumulators |= touchMask;
206120
}
207121
else
208122
{
209123
if (point.IsActive)
210124
{
211125
point.IsActive = false;
212-
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{fingerId} 松开");
213126
}
214127
}
215128
}
216129
}
217130

218-
public static ulong GetTouchState(int playerNo)
131+
private ulong GetTouchState(int player)
219132
{
220-
lock (touchLocks[playerNo])
133+
if (player != playerNo) return 0;
134+
lock (touchLock)
221135
{
222136
ulong currentTouchData = 0;
223137
var now = DateTime.Now;
224-
var points = allFingerPoints[playerNo];
225138

226-
for (int i = 0; i < points.Length; i++)
139+
for (int i = 0; i < allFingerPoints.Length; i++)
227140
{
228-
var point = points[i];
141+
var point = allFingerPoints[i];
229142
if (point.IsActive)
230143
{
231144
if ((now - point.LastUpdate).TotalMilliseconds > TouchTimeoutMs)
232145
{
233146
point.IsActive = false;
234-
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{i} 超时自动释放");
235147
}
236148
else
237149
{
238150
currentTouchData |= point.Mask;
239151
}
240152
}
241153
}
242-
243-
ulong finalResult = currentTouchData | frameAccumulators[playerNo];
244-
frameAccumulators[playerNo] = 0;
154+
155+
ulong finalResult = currentTouchData | frameAccumulators;
156+
frameAccumulators = 0;
245157

246158
return finalResult;
247159
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using AquaMai.Config.Attributes;
3+
using LibUsbDotNet.Main;
4+
5+
namespace AquaMai.Mods.GameSystem.ExclusiveTouch;
6+
7+
[ConfigCollapseNamespace]
8+
[ConfigSection("PDX 独占触摸")]
9+
public class PdxTouch
10+
{
11+
[ConfigEntry("触摸体积半径", zh: "基准是 1440x1440")]
12+
public static readonly int radius;
13+
14+
[ConfigEntry("1P 序列号", zh: "如果要组 2P,请在这里指定对应的序列号。否则将自动使用第一个检测到的设备作为 1P")]
15+
public static readonly string serial1p;
16+
[ConfigEntry("2P 序列号")]
17+
public static readonly string serial2p;
18+
19+
private static readonly PdxTouchDevice[] devices = new PdxTouchDevice[2];
20+
21+
public static void OnBeforePatch()
22+
{
23+
if (string.IsNullOrWhiteSpace(serial1p) && string.IsNullOrWhiteSpace(serial2p))
24+
{
25+
devices[0] = new PdxTouchDevice(0, null);
26+
devices[0].Start();
27+
}
28+
if (!string.IsNullOrWhiteSpace(serial1p))
29+
{
30+
devices[0] = new PdxTouchDevice(0, serial1p);
31+
devices[0].Start();
32+
}
33+
if (!string.IsNullOrWhiteSpace(serial2p))
34+
{
35+
devices[1] = new PdxTouchDevice(1, serial2p);
36+
devices[1].Start();
37+
}
38+
}
39+
40+
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)
41+
{
42+
private const byte ReportId = 2;
43+
protected override void OnTouchData(byte[] data)
44+
{
45+
byte reportId = data[0];
46+
if (reportId != ReportId) return;
47+
48+
for (int i = 0; i < 10; i++)
49+
{
50+
var index = i * 6 + 1;
51+
if (data[index] == 0) continue;
52+
bool isPressed = (data[index] & 0x01) == 1;
53+
var fingerId = data[index + 1];
54+
ushort x = BitConverter.ToUInt16(data, index + 2);
55+
ushort y = BitConverter.ToUInt16(data, index + 4);
56+
HandleFinger(x, y, fingerId, isPressed);
57+
}
58+
}
59+
}
60+
}

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)