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 BuildArrowData(ParametricSlidePath path) { var result = new List(); 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; } /// /// Convert arrow data to sinmai format (Vector4) /// /// arrow data generated by BuildArrowData() /// button index of slide-star /// mirror mode in user option /// sinmai format arrow data, referenced to slide-star public static List ConvertAndRotateArrowData(IEnumerable 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(); 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 HitAreasLookup = new Dictionary(); 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 BuildHitAreas(ParametricSlidePath path) { var nodeList = new List>(); 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(lastNode.Value, (length + enterLength) / 2.0)); if (node != null) { enterLength = length; } } } lastNode = node; } nodeList.Add(new Tuple(lastNode!.Value, totalLength)); nodeList[0] = new Tuple(nodeList[0].Item1, 0.0); var result = new List(); 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; } /// /// Convert hit area data to sinmai format (Vector4) /// /// hit area data generated by BuildHitAreas() /// button index of slide-star /// mirror mode in user option /// sinmai format arrow data, referenced to slide-star public static List ConvertAndRotateHitAreas(IEnumerable data, int starButton, OptionMirrorID mirrorMode) { var hitAreaList = new List(); 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; } }