Skip to content

Commit 0c14d21

Browse files
authored
[+] WIP exclusive touch (#102)
* [+] Add branch to build script * 独占触摸基础 * 多点修复 * [RF] Extract TouchStatusProvider * 支持 xy 翻转 * mml touch * pdx * 线程安全 * 防滞留,防吃键 * 移除 Linux 支持 * 移除 Notifier * 移除 Linux * [F] PracticeMode 在 1.50 以下版本的兼容性 * default hide for not ready
1 parent 23c312f commit 0c14d21

File tree

116 files changed

+9108
-128
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+9108
-128
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using HarmonyLib;
4+
using IO;
5+
using Manager;
6+
using MelonLoader;
7+
8+
namespace AquaMai.Core.Helpers;
9+
10+
public class TouchStatusProvider
11+
{
12+
public delegate ulong TouchStatusProviderDelegate(int playerNo);
13+
14+
private static readonly List<TouchStatusProviderDelegate>[] _touchStatusProviders = [new List<TouchStatusProviderDelegate>(), new List<TouchStatusProviderDelegate>()];
15+
16+
public static void RegisterTouchStatusProvider(int playerNo, TouchStatusProviderDelegate touchStatusProvider)
17+
{
18+
if (playerNo is < 0 or > 1) throw new ArgumentException("Invalid player");
19+
EnsurePatched();
20+
_touchStatusProviders[playerNo].Add(touchStatusProvider);
21+
}
22+
private static bool isPatched = false;
23+
private static bool EnsurePatched()
24+
{
25+
if (isPatched) return false;
26+
isPatched = true;
27+
Startup.ApplyPatch(typeof(TouchStatusProvider));
28+
return true;
29+
}
30+
31+
private static bool ShouldEnableForPlayer(int playerNo) => playerNo switch
32+
{
33+
0 => _touchStatusProviders[0].Count > 0,
34+
1 => _touchStatusProviders[1].Count > 0,
35+
_ => false,
36+
};
37+
38+
[HarmonyPrefix]
39+
[HarmonyPatch(typeof(NewTouchPanel), "Start")]
40+
public static bool PreNewTouchPanelStart(uint ____monitorIndex, ref NewTouchPanel.StatusEnum ___Status, ref bool ____isRunning)
41+
{
42+
if (!ShouldEnableForPlayer((int)____monitorIndex)) return true;
43+
___Status = NewTouchPanel.StatusEnum.Drive;
44+
____isRunning = true;
45+
MelonLogger.Msg($"[TouchStatusProvider] NewTouchPanel Start {____monitorIndex + 1}P");
46+
return false;
47+
}
48+
49+
[HarmonyPrefix]
50+
[HarmonyPatch(typeof(NewTouchPanel), "Execute")]
51+
public static bool PreNewTouchPanelExecute(uint ____monitorIndex, ref uint ____dataCounter)
52+
{
53+
if (!ShouldEnableForPlayer((int)____monitorIndex)) return true;
54+
ulong currentTouchData = 0;
55+
foreach (var touchStatusProvider in _touchStatusProviders[(int)____monitorIndex])
56+
{
57+
currentTouchData |= touchStatusProvider((int)____monitorIndex);
58+
}
59+
60+
InputManager.SetNewTouchPanel(____monitorIndex, currentTouchData, ++____dataCounter);
61+
return false;
62+
}
63+
}
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
using System;
2+
using AquaMai.Config.Attributes;
3+
using LibUsbDotNet.Main;
4+
using LibUsbDotNet;
5+
using MelonLoader;
6+
using UnityEngine;
7+
using AquaMai.Core.Helpers;
8+
using System.Threading;
9+
10+
namespace AquaMai.Mods.GameSystem.ExclusiveTouch;
11+
12+
[ConfigCollapseNamespace]
13+
[ConfigSection(exampleHidden: true)]
14+
public class ExclusiveTouch
15+
{
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];
51+
52+
private class TouchPoint
53+
{
54+
public ulong Mask;
55+
public DateTime LastUpdate;
56+
public bool IsActive;
57+
}
58+
59+
// [玩家][手指ID]
60+
private static readonly TouchPoint[][] allFingerPoints = new TouchPoint[2][];
61+
62+
// 防吃键
63+
private static readonly ulong[] frameAccumulators = new ulong[2];
64+
private static readonly object[] touchLocks = [new object(), new object()];
65+
66+
private const int TouchTimeoutMs = 20;
67+
68+
public static void OnBeforePatch()
69+
{
70+
if (enable1p)
71+
{
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)
77+
{
78+
MelonLogger.Msg("[ExclusiveTouch] Cannot connect 1P");
79+
}
80+
else
81+
{
82+
IUsbDevice wholeDevice = device as IUsbDevice;
83+
if (wholeDevice != null)
84+
{
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();
103+
}
104+
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)
123+
{
124+
if (ec == ErrorCode.IoTimedOut) continue; // 超时就继续等
125+
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 读取错误: {ec}");
126+
break;
127+
}
128+
129+
if (bytesRead > 0)
130+
{
131+
OnTouchData(playerNo, buffer);
132+
}
133+
}
134+
}
135+
136+
private static void OnTouchData(int playerNo, byte[] data)
137+
{
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)
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)
184+
{
185+
// 安全检查,防止越界
186+
if (fingerId < 0 || fingerId >= 256) return;
187+
188+
lock (touchLocks[playerNo])
189+
{
190+
var point = allFingerPoints[playerNo][fingerId];
191+
192+
if (isPressed)
193+
{
194+
ulong touchMask = touchSensorMappers[playerNo].ParseTouchPoint(x, y);
195+
196+
if (!point.IsActive)
197+
{
198+
point.IsActive = true;
199+
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{fingerId} 按下 at ({x}, {y}) -> 0x{touchMask:X}");
200+
}
201+
202+
point.Mask = touchMask;
203+
point.LastUpdate = DateTime.Now;
204+
205+
frameAccumulators[playerNo] |= touchMask;
206+
}
207+
else
208+
{
209+
if (point.IsActive)
210+
{
211+
point.IsActive = false;
212+
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{fingerId} 松开");
213+
}
214+
}
215+
}
216+
}
217+
218+
public static ulong GetTouchState(int playerNo)
219+
{
220+
lock (touchLocks[playerNo])
221+
{
222+
ulong currentTouchData = 0;
223+
var now = DateTime.Now;
224+
var points = allFingerPoints[playerNo];
225+
226+
for (int i = 0; i < points.Length; i++)
227+
{
228+
var point = points[i];
229+
if (point.IsActive)
230+
{
231+
if ((now - point.LastUpdate).TotalMilliseconds > TouchTimeoutMs)
232+
{
233+
point.IsActive = false;
234+
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 手指{i} 超时自动释放");
235+
}
236+
else
237+
{
238+
currentTouchData |= point.Mask;
239+
}
240+
}
241+
}
242+
243+
ulong finalResult = currentTouchData | frameAccumulators[playerNo];
244+
frameAccumulators[playerNo] = 0;
245+
246+
return finalResult;
247+
}
248+
}
249+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
namespace LibUsbDotNet.Descriptors;
4+
5+
[Flags]
6+
public enum ClassCodeType : byte
7+
{
8+
PerInterface = 0,
9+
Audio = 1,
10+
Comm = 2,
11+
Hid = 3,
12+
Printer = 7,
13+
Ptp = 6,
14+
MassStorage = 8,
15+
Hub = 9,
16+
Data = 0xA,
17+
VendorSpec = byte.MaxValue
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
3+
namespace LibUsbDotNet.Descriptors;
4+
5+
[Flags]
6+
public enum DescriptorType : byte
7+
{
8+
Device = 1,
9+
Configuration = 2,
10+
String = 3,
11+
Interface = 4,
12+
Endpoint = 5,
13+
DeviceQualifier = 6,
14+
OtherSpeedConfiguration = 7,
15+
InterfacePower = 8,
16+
OTG = 9,
17+
Debug = 0xA,
18+
InterfaceAssociation = 0xB,
19+
Hid = 0x21,
20+
HidReport = 0x22,
21+
Physical = 0x23,
22+
Hub = 0x29
23+
}

0 commit comments

Comments
 (0)