AquaDX/AquaMai/MaimaiDX2077/SlideCodeParser.cs

260 lines
9.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using MelonLoader;
namespace AquaMai.MaimaiDX2077;
public static class SlideCodeParser
{
public enum CommandType
{
Invalid = -1,
NodeA = 0,
NodeB = 1,
NodeC = 2,
OrbitCCW = 3,
OrbitCW = 4,
NodeEnd = 5
}
public struct Command(CommandType type, int value)
{
public CommandType Type = type;
public int Value = value;
public static bool IsSame(Command a, Command b)
{
return a.Type == b.Type && a.Value == b.Value;
}
}
public static readonly char[] CommandChars =
[
'A', 'B', 'C', 'P', 'Q', 'K'
];
public static int TryParseDigit(char c)
{
if (c >= '0' && c <= '9') return c - '0';
return -1;
}
public static List<Command> ParseCommands(string code)
{
if (!CommandChars.Contains(code[1]))
{
throw new ArgumentException($"the 2nd char should be a command");
}
if (code[code.Length - 2] != 'K')
{
throw new ArgumentException($"should end with 'K' command");
}
var commands = new List<Command>();
var currentType = CommandType.NodeA;
var value = TryParseDigit(code[0]);
if (value < 0) throw new ArgumentException($"invalid char '{code[0]}'");
commands.Add(new Command(currentType, value));
for (var ptr = 1; ptr < code.Length; ptr++)
{
var ch = code[ptr];
if (CommandChars.Contains(ch))
{
currentType = (CommandType) Array.IndexOf(CommandChars, ch);
if (currentType == CommandType.NodeC)
{
commands.Add(new Command(CommandType.NodeC, 0));
}
}
else
{
value = TryParseDigit(ch);
if (value < 0) throw new ArgumentException($"invalid char '{ch}'");
if (currentType == CommandType.NodeC)
{
throw new ArgumentException($"digit should not follow 'C'");
}
commands.Add(new Command(currentType, value));
}
}
return commands;
}
public static Complex GetNodePosition(Command cmd)
{
switch (cmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeEnd:
return MaiGeometry.PointGroupA(cmd.Value);
case CommandType.NodeB:
return MaiGeometry.PointGroupB(cmd.Value);
case CommandType.NodeC:
return MaiGeometry.Center();
default:
throw new ArgumentException($"invalid type for node: {cmd.Type}");
}
}
public static void NodeToNode(SlidePathGenerator generator, Command last, Command current)
{
if (Command.IsSame(last, current)) return;
generator.LineToPoint(GetNodePosition(current));
}
public static void NodeToOrbit(SlidePathGenerator generator, Command last, Command current)
{
var isCcw = (current.Type == CommandType.OrbitCCW);
var node = GetNodePosition(last);
var orbit = MaiGeometry.GetCircle(current.Value);
var diff = node - orbit.Center;
if (Math.Abs(diff.Magnitude - orbit.Radius) < 0.1)
{
if (last.Type == CommandType.NodeA && current.Value == 9)
{
generator.TrySetLastParseMarker(ParametricSlidePath.ParseMarker.ForceAlign);
}
return; // node on circle, do nothing
}
if (diff.Magnitude < orbit.Radius)
throw new ArgumentException($"impossible: {last.Type}{last.Value} -> Orbit{current.Value}");
generator.TangentToCircle(orbit, isCcw);
}
public static void OrbitToNode(SlidePathGenerator generator, Command last, Command current)
{
var isCcw = (last.Type == CommandType.OrbitCCW);
var node = GetNodePosition(current);
var orbit = MaiGeometry.GetCircle(last.Value);
var diff = node - orbit.Center;
if (Math.Abs(diff.Magnitude - orbit.Radius) < 0.1)
{
generator.ArcToAngle(orbit.Center, diff.Phase, isCcw, false);
return;
}
if (diff.Magnitude < orbit.Radius)
throw new ArgumentException($"impossible: Orbit{last.Value} -> {current.Type}{current.Value}");
generator.ArcToTangentTowards(node, orbit.Center, isCcw);
generator.LineToPoint(node);
}
public static void OrbitToOrbit(SlidePathGenerator generator, Command last, Command current)
{
if (current.Type != last.Type) throw new ArgumentException($"orbit type mismatch");
var isCcw = (last.Type == CommandType.OrbitCCW);
var lastOrbit = MaiGeometry.GetCircle(last.Value);
var currentOrbit = MaiGeometry.GetCircle(current.Value);
if (current.Value == last.Value)
{
generator.FullCircle(lastOrbit.Center, isCcw);
return;
}
if (last.Value == 0 && current.Value == 9 || last.Value == 9 && current.Value == 0)
throw new ArgumentException($"impossible: Orbit{last.Value} -> Orbit{current.Value}");
if (current.Value == 9)
{
var data = MaiGeometry.TransferOutData(last.Value, isCcw);
generator.ArcToAngle(lastOrbit.Center, data.Item2, isCcw, false);
generator.ArcToAngle(data.Item1.Center, data.Item3, isCcw, false);
generator.TrySetLastParseMarker(ParametricSlidePath.ParseMarker.SmoothAlign);
return;
}
if (last.Value == 9)
{
var data = MaiGeometry.TransferOutData(current.Value, !isCcw);
generator.ArcToAngle(lastOrbit.Center, data.Item3, isCcw, true);
generator.ArcToAngle(data.Item1.Center, data.Item2, isCcw, false);
return;
}
generator.ExternTangentTransfer(lastOrbit.Center, currentOrbit, isCcw);
}
public static ParametricSlidePath Parse(string code)
{
try
{
var commands = ParseCommands(code);
var lastCmd = commands[0];
// The first command is guarantee to be 'A'
var generator = SlidePathGenerator.BeginAt(MaiGeometry.PointGroupA(lastCmd.Value));
for (var i = 1; i < commands.Count; i++)
{
var cmd = commands[i];
switch (cmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
case CommandType.NodeEnd:
switch (lastCmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
NodeToNode(generator, lastCmd, cmd);
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
OrbitToNode(generator, lastCmd, cmd);
break;
case CommandType.NodeEnd:
throw new ArgumentException($"'K' should be the last command");
default:
throw new ArgumentOutOfRangeException();
}
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
switch (lastCmd.Type)
{
case CommandType.NodeA:
case CommandType.NodeB:
case CommandType.NodeC:
NodeToOrbit(generator, lastCmd, cmd);
break;
case CommandType.OrbitCCW:
case CommandType.OrbitCW:
OrbitToOrbit(generator, lastCmd, cmd);
break;
case CommandType.NodeEnd:
throw new ArgumentException($"'K' should be the last command");
default:
throw new ArgumentOutOfRangeException();
}
break;
default:
throw new ArgumentOutOfRangeException();
}
lastCmd = cmd;
}
return generator.GeneratePath();
}
catch (ArgumentException e)
{
var msg = $"Invalid code: {code}";
if (e.Message != "")
{
msg += $", {e.Message}";
}
MelonLogger.Error(msg);
return null;
}
}
}