Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 90 additions & 152 deletions AquaMai.Mods/GameSystem/ExclusiveTouch/ExclusiveTouch.cs
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, [CanBeNull] string locationPath, 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
{
Expand All @@ -56,192 +21,165 @@ 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
UsbDeviceFinder finder;

if (!string.IsNullOrWhiteSpace(serialNumber))
{
// 方便组 2P
var serialNumber = string.IsNullOrWhiteSpace(serialNumber1p) ? null : serialNumber1p;
var finder = new UsbDeviceFinder(vid1p, pid1p, serialNumber);
var device = UsbDevice.OpenUsbDevice(finder);
if (device == null)
// 优先使用序列号
finder = new UsbDeviceFinder(vid, pid, serialNumber);
}
else if (!string.IsNullOrWhiteSpace(locationPath))
{
// 使用位置路径匹配
finder = new UsbDeviceLocationFinder(vid, pid, locationPath);
}
else
{
// 使用第一个匹配的设备
finder = new UsbDeviceFinder(vid, pid);
}

device = UsbDevice.OpenUsbDevice(finder);
if (device == null)
{
MelonLogger.Msg($"[ExclusiveTouch] Cannot connect {playerNo + 1}P");
}
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the Application.quitting event handler, if wholeDevice.ReleaseInterface(interfaceNumber) throws an exception, tmpDevice.Close() will not be called, which could lead to an unclosed resource. It's safer to wrap the resource release logic in a try...finally block to guarantee that Close() is always called.

            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);
}
}
}

private static void ReadThread(int playerNo)
{
byte[] buffer = new byte[packetSize1p];
var reader = devices[playerNo].OpenEndpointReader(endpoint1p);
while (devices[playerNo] != null)
{
int bytesRead;
ErrorCode ec = reader.Read(buffer, 100, out bytesRead); // 100ms 超时

if (ec != ErrorCode.None)
for (int i = 0; i < 256; i++)
{
if (ec == ErrorCode.IoTimedOut) continue; // 超时就继续等
MelonLogger.Msg($"[ExclusiveTouch] {playerNo + 1}P: 读取错误: {ec}");
break;
allFingerPoints[i] = new TouchPoint();
}

if (bytesRead > 0)
{
OnTouchData(playerNo, buffer);
}
Thread readThread = new(ReadThread);
readThread.Start();
TouchStatusProvider.RegisterTouchStatusProvider(playerNo, GetTouchState);
}
}

private static void OnTouchData(int playerNo, byte[] data)
private void ReadThread()
{
byte reportId = data[0];
if (reportId != reportId1p) return;

#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[] buffer = new byte[packetSize];
var reader = device.OpenEndpointReader(endpoint);

try
{
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);
while (device != null)
{
int bytesRead;
ErrorCode ec = reader.Read(buffer, 100, out bytesRead); // 100ms 超时

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

// 解析第二根手指
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);
if (bytesRead > 0)
{
OnTouchData(buffer);
}
}
}
#endif
finally
{
// 确保 reader 被正确释放
reader?.Dispose();
}
}

private static void HandleFinger(ushort x, ushort y, int fingerId, bool isPressed, int playerNo)
protected abstract void OnTouchData(byte[] data);

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;
}
Expand Down
20 changes: 16 additions & 4 deletions AquaMai.Mods/GameSystem/ExclusiveTouch/TouchSensorMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,27 @@ public ulong ParseTouchPoint(float x, float y)
// 检查所有传感器
for (int i = 0; i < 34; i++)
{
bool isInsidePolygon = PolygonRaycasting.IsVertDistance(_sensors[i], canvasPoint, radius);
if (!isInsidePolygon)
bool isInsidePolygon;

if (radius > 0)
{
isInsidePolygon = PolygonRaycasting.IsCircleIntersectingPolygonEdges(_sensors[i], canvasPoint, radius);
// 当有半径时,需要检查圆与多边形的关系
isInsidePolygon = PolygonRaycasting.IsVertDistance(_sensors[i], canvasPoint, radius);
if (!isInsidePolygon)
{
isInsidePolygon = PolygonRaycasting.IsCircleIntersectingPolygonEdges(_sensors[i], canvasPoint, radius);
}
if (!isInsidePolygon)
{
isInsidePolygon = PolygonRaycasting.InPointInInternal(_sensors[i], canvasPoint);
}
}
if (!isInsidePolygon)
else
{
// 当半径为0时,只需要检查点是否在多边形内部
isInsidePolygon = PolygonRaycasting.InPointInInternal(_sensors[i], canvasPoint);
}

if (isInsidePolygon)
{
res |= 1ul << i;
Expand Down
Loading