AquaDX/AquaMai/MaimaiDX2077/SlideDataBuilder.cs

475 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using DB;
using Manager;
using MelonLoader;
using Vector4 = UnityEngine.Vector4;
namespace AquaMai.MaimaiDX2077;
public static class SlideDataBuilder
{
public readonly struct ArrowData(Complex point, Complex tangent, double length)
{
public readonly Complex Point = point;
public readonly Complex Tangent = tangent;
public readonly double Length = length;
}
public static List<ArrowData> BuildArrowData(ParametricSlidePath path)
{
var result = new List<ArrowData>();
var totalLength = path.GetPathLength();
var totalSegCount = path.Segments.Length;
var length = 0.0;
var segIdx = 0;
var isSwitching = false;
while (length < totalLength)
{
var t = length / totalLength;
var pt = path.GetPointAt(t);
var tg = path.GetTangentAt(t);
var t2 = (length + 10.0) / totalLength;
if ((path.GetTangentAt(t2) - tg).Magnitude < 0.2) // 0.2 -> ~ 11.48 deg apart
{
// use secant instead of tangent (for better visual quality)
tg = path.GetPointAt(t2) - pt;
tg /= tg.Magnitude;
}
if (isSwitching)
{
// around connecting point of 2 segments, smoothing the transition
var last = result[result.Count - 1];
var vec = pt - last.Point;
vec /= vec.Magnitude;
if ((tg - last.Tangent).Magnitude < 0.2)
{
var x = 0.5 * (last.Tangent + vec);
x /= x.Magnitude;
result[result.Count - 1] = new ArrowData(last.Point, x, last.Length);
tg = 0.5 * (tg + vec);
tg /= tg.Magnitude;
}
}
result.Add(new ArrowData(pt, tg, length));
isSwitching = false;
var nextLength = length + path.Segments[segIdx].ArrowDistance;
if (segIdx < totalSegCount - 1 && nextLength >= path.AccumulatedLengths[segIdx])
{
isSwitching = true;
if (path.Segments[segIdx].ParseMarker == ParametricSlidePath.ParseMarker.ForceAlign)
{
// in this case the next point is forced to be 1 unit after the connecting point
nextLength = path.AccumulatedLengths[segIdx] + path.Segments[segIdx + 1].ArrowDistance;
// P.S. 这种情况一般是出现在一条直线连接到外圈, 这个处理是为了让外圈的箭头对齐
}
if (path.Segments[segIdx + 1].ParseMarker == ParametricSlidePath.ParseMarker.SmoothAlign)
{
// arrow distance of the next segment is tempered in order to align arrow
var delta = path.AccumulatedLengths[segIdx + 1] - length;
var n = Math.Round(delta / MaiGeometry.DefaultDistance);
path.Segments[segIdx + 1].SetArrowDistance(delta / n);
nextLength = length + delta / n;
// P.S. 这种情况出现在 ppqq 圈进入外圈, 可以把转移轨道的箭头间距微调一下, 也是让外圈对齐
}
segIdx++;
}
length = nextLength;
}
// 把路径终点补上
result.Add(new ArrowData(path.GetPointAt(1.0), path.GetTangentAt(1.0), totalLength));
return result;
}
/// <summary>
/// Convert arrow data to sinmai format (Vector4)
/// </summary>
/// <param name="data">arrow data generated by BuildArrowData()</param>
/// <param name="starButton">button index of slide-star</param>
/// <param name="mirrorMode">mirror mode in user option</param>
/// <returns>sinmai format arrow data, referenced to slide-star</returns>
public static List<Vector4> ConvertAndRotateArrowData(IEnumerable<ArrowData> data, int starButton,
OptionMirrorID mirrorMode)
{
// SBGA 用 Vector4 存储了 slide 箭头的坐标与取向
// x, y 是平面坐标, z 是从起点到此处的路径长度 (px), w 是旋转的角度 (0 ~ 360 deg) (注意与切线方向差了 180 度)
// 坐标原点是屏幕中心, x 轴向右, y 轴向上
// w 的零点是朝向正右 (对应于箭头朝向正左), 逆时针为正方向
// 此外, sinmai 实际上是把所有 slide 路径相对于星星头存储的, 再在 SlideRoot 里通过 transform 转到合适的位置
// 判定区也是相对于星星头存储, 用 InputManager.ConvertTouchPanelRotatePush() 执行旋转
// 但是 slide code 定义的是绝对位置, 所以要逆向转回去, 以保证无论星星头在哪个键获取到的路径在处理过后都是一样的
// 然后还需要处理镜像的问题
var arrowList = new List<Vector4>();
var rotor = Complex.FromPolarCoordinates(1.0, Math.PI / 4.0 * starButton);
foreach (var arrow in data)
{
var pos = arrow.Point;
var tangent = arrow.Tangent;
switch (mirrorMode)
{
case OptionMirrorID.Normal:
break;
case OptionMirrorID.LR:
pos = Complex.Conjugate(pos) * -1.0;
tangent = Complex.Conjugate(tangent) * -1.0;
break;
case OptionMirrorID.UD:
pos = Complex.Conjugate(pos);
tangent = Complex.Conjugate(tangent);
break;
case OptionMirrorID.UDLR:
pos *= -1.0;
tangent *= -1.0;
break;
default:
break;
}
pos *= rotor;
tangent *= rotor;
var angle = tangent.Phase * 180.0 / Math.PI + 180.0; // Phase is in [-PI, PI]
arrowList.Add(new Vector4((float) pos.Real, (float) pos.Imaginary, (float) arrow.Length, (float) angle));
}
return arrowList;
}
public readonly struct HitAreaData(double push, double release, int[] areas)
{
public readonly double PushDistance = push;
public readonly double ReleaseDistance = release;
public readonly int[] PanelAreas = areas;
}
public static readonly Dictionary<int, HitAreaData[]> HitAreasLookup = new Dictionary<int, HitAreaData[]>();
public static void InitializeHitAreasLookup()
{
for (var i = 0; i < 8; i++)
{
for (var j = 0; j < 8; j++)
{
var diff = (j - i) & 7; // you know this is actually % 8 ... for same negative number compat
int tmp, tmp2;
// Ai -> Aj
var key = (i << 5) | j;
switch (diff)
{
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.32, 0.68, [i]),
new HitAreaData(1.00, 1.00, [j])
];
break;
case 2:
tmp = (i + 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.20, 0.38, [i]),
new HitAreaData(0.62, 0.80, [tmp, tmp | 8]),
new HitAreaData(1.00, 1.00, [j])
];
break;
case 6:
tmp = (i - 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.20, 0.38, [i]),
new HitAreaData(0.62, 0.80, [tmp, tmp | 8]),
new HitAreaData(1.00, 1.00, [j])
];
break;
default:
break;
}
// Bi -> Bj
key = ((i | 8) << 5) | (j | 8);
switch (diff)
{
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.44, 0.56, [i | 8]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 2:
tmp = (i + 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.22, 0.35, [i | 8]),
new HitAreaData(0.65, 0.78, [tmp | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 6:
tmp = (i - 1) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.22, 0.35, [i | 8]),
new HitAreaData(0.65, 0.78, [tmp | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 3:
tmp = (i + 1) & 7;
tmp2 = (i + 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.15, 0.28, [i | 8]),
new HitAreaData(0.48, 0.52, [tmp | 8, 16]),
new HitAreaData(0.72, 0.85, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
case 5:
tmp = (i - 1) & 7;
tmp2 = (i - 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.15, 0.28, [i | 8]),
new HitAreaData(0.48, 0.52, [tmp | 8, 16]),
new HitAreaData(0.72, 0.85, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
break;
default:
break;
}
// Ai <-> Bj
key = (i << 5) | (j | 8);
var key2 = ((j | 8) << 5) | i;
switch (diff)
{
case 0:
HitAreasLookup[key] =
[
new HitAreaData(0.60, 0.75, [i]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.25, 0.40, [j | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 1:
case 7:
HitAreasLookup[key] =
[
new HitAreaData(0.45, 0.77, [i]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.23, 0.55, [j | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 3:
tmp = (i + 1) & 7;
tmp2 = (i + 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.25, 0.34, [i]),
new HitAreaData(0.54, 0.68, [i | 8, tmp | 8]),
new HitAreaData(0.85, 0.90, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.10, 0.15, [j | 8]),
new HitAreaData(0.32, 0.46, [tmp2 | 8, 16]),
new HitAreaData(0.66, 0.75, [i | 8, tmp | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
case 5:
tmp = (i - 1) & 7;
tmp2 = (i - 2) & 7;
HitAreasLookup[key] =
[
new HitAreaData(0.25, 0.34, [i]),
new HitAreaData(0.54, 0.68, [i | 8, tmp | 8]),
new HitAreaData(0.85, 0.90, [tmp2 | 8, 16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.10, 0.15, [j | 8]),
new HitAreaData(0.32, 0.46, [tmp2 | 8, 16]),
new HitAreaData(0.66, 0.75, [i | 8, tmp | 8]),
new HitAreaData(1.00, 1.00, [i])
];
break;
default:
break;
}
// C <-> Bj
key = (16 << 5) | (j | 8);
key2 = ((j | 8) << 5) | 16;
HitAreasLookup[key] =
[
new HitAreaData(0.50, 0.70, [16]),
new HitAreaData(1.00, 1.00, [j | 8])
];
HitAreasLookup[key2] =
[
new HitAreaData(0.30, 0.50, [j | 8]),
new HitAreaData(1.00, 1.00, [16])
];
}
}
}
public static List<HitAreaData> BuildHitAreas(ParametricSlidePath path)
{
var nodeList = new List<Tuple<int, double>>();
var totalLength = path.GetPathLength();
var count = (int)Math.Round(totalLength / 10.0);
int? lastNode = null;
var enterLength = 0.0;
for (var i = 0; i < count; i++)
{
var t = (double)i / count;
var pt = path.GetPointAt(t);
int? node = null;
if (pt.Magnitude < 55.0)
{
node = 16;
}
else for (var j = 0; j < 8; j++)
{
var phi = Math.PI * (3.0 / 8.0 - j / 4.0);
if ((pt - Complex.FromPolarCoordinates(440.0, phi)).Magnitude < 80.0)
{
node = j;
break;
}
if ((pt - Complex.FromPolarCoordinates(210.0, phi)).Magnitude < 45.0)
{
node = j | 8;
break;
}
}
if (lastNode != node)
{
var length = t * totalLength;
if (lastNode == null)
{
enterLength = length;
}
else
{
nodeList.Add(new Tuple<int, double>(lastNode.Value, (length + enterLength) / 2.0));
if (node != null)
{
enterLength = length;
}
}
}
lastNode = node;
}
nodeList.Add(new Tuple<int, double>(lastNode!.Value, totalLength));
nodeList[0] = new Tuple<int, double>(nodeList[0].Item1, 0.0);
var result = new List<HitAreaData>();
result.Add(new HitAreaData(0.0, 0.0, [nodeList[0].Item1]));
for (var i = 1; i < nodeList.Count; i++)
{
var key = (nodeList[i - 1].Item1 << 5) | nodeList[i].Item1;
var segmentLength = nodeList[i].Item2 - nodeList[i - 1].Item2;
var data = HitAreasLookup[key];
var area = result[result.Count - 1];
result[result.Count - 1] = new HitAreaData(
area.PushDistance + segmentLength * data[0].PushDistance,
area.ReleaseDistance + segmentLength * data[0].ReleaseDistance,
area.PanelAreas
);
for (var j = 1; j < data.Length; j++)
{
result.Add(new HitAreaData(
segmentLength * (data[j].PushDistance - data[j - 1].ReleaseDistance),
segmentLength * (data[j].ReleaseDistance - data[j].PushDistance),
data[j].PanelAreas
));
}
}
double lastPushDistance = 0.0;
if (path.GetEndType(OptionMirrorID.Normal) == SlideType.Slide_Straight)
{
var diff = nodeList[nodeList.Count - 1].Item1 - nodeList[nodeList.Count - 2].Item1;
diff %= 8;
lastPushDistance = diff switch
{
1 or 2 or 6 or 7 => 130.0,
_ => 159.0
};
}
else
{
lastPushDistance = 175.0;
}
var last2ndArea = result[result.Count - 2];
var lastArea = result[result.Count - 1];
var distance = last2ndArea.ReleaseDistance + lastArea.PushDistance + lastArea.ReleaseDistance;
result[result.Count - 2] = new HitAreaData(last2ndArea.PushDistance, distance - lastPushDistance, last2ndArea.PanelAreas);
result[result.Count - 1] = new HitAreaData(lastPushDistance, 0.0, lastArea.PanelAreas);
return result;
}
/// <summary>
/// Convert hit area data to sinmai format (Vector4)
/// </summary>
/// <param name="data">hit area data generated by BuildHitAreas()</param>
/// <param name="starButton">button index of slide-star</param>
/// <param name="mirrorMode">mirror mode in user option</param>
/// <returns>sinmai format arrow data, referenced to slide-star</returns>
public static List<SlideManager.HitArea> ConvertAndRotateHitAreas(IEnumerable<HitAreaData> data, int starButton,
OptionMirrorID mirrorMode)
{
var hitAreaList = new List<SlideManager.HitArea>();
foreach (var hitAreaData in data)
{
var hitArea = new SlideManager.HitArea();
hitArea.PushDistance = hitAreaData.PushDistance;
hitArea.ReleaseDistance = hitAreaData.ReleaseDistance;
foreach (var pad in hitAreaData.PanelAreas)
{
var converted = MaiGeometry.MirrorInfo[(int) mirrorMode, pad];
converted = converted == 16 ? 16 : (converted - starButton) & 0b111 | converted & 0b1000;
hitArea.HitPoints.Add((InputManager.TouchPanelArea) converted);
}
hitAreaList.Add(hitArea);
}
return hitAreaList;
}
}