163 lines
6.3 KiB
C#
163 lines
6.3 KiB
C#
|
|
using System.Net.Http;
|
||
|
|
using System.Text;
|
||
|
|
using System.Text.Json;
|
||
|
|
using System.Text.RegularExpressions;
|
||
|
|
|
||
|
|
namespace Substation;
|
||
|
|
|
||
|
|
public record XToysPattern(string Name, List<WaveFrame> ChannelA, List<WaveFrame> ChannelB);
|
||
|
|
|
||
|
|
public static class XToysPatternImporter
|
||
|
|
{
|
||
|
|
static readonly HttpClient Http = new();
|
||
|
|
|
||
|
|
public static async Task<XToysPattern> ImportAsync(string url)
|
||
|
|
{
|
||
|
|
var patternId = ExtractPatternId(url);
|
||
|
|
if (patternId == null)
|
||
|
|
throw new ArgumentException("Could not extract pattern ID from URL. Expected format: https://xtoys.app/patterns/<ID>");
|
||
|
|
|
||
|
|
var body = JsonSerializer.Serialize(new { data = new { patternID = patternId } });
|
||
|
|
var content = new StringContent(body, Encoding.UTF8, "application/json");
|
||
|
|
var response = await Http.PostAsync("https://xtoys.app/api/getPatternv2", content);
|
||
|
|
response.EnsureSuccessStatusCode();
|
||
|
|
|
||
|
|
var json = await response.Content.ReadAsStringAsync();
|
||
|
|
return Parse(json);
|
||
|
|
}
|
||
|
|
|
||
|
|
static int FreqPctToMs(byte freqPct)
|
||
|
|
{
|
||
|
|
// xToys frequency: 0% = deep (1000ms), 100% = buzzy (10ms)
|
||
|
|
// freqPct is the raw 0-100 value from the pattern (already clamped in EvaluatePattern)
|
||
|
|
return (int)Math.Round(1000 - 9.9 * freqPct);
|
||
|
|
}
|
||
|
|
|
||
|
|
static string? ExtractPatternId(string url)
|
||
|
|
{
|
||
|
|
var match = Regex.Match(url, @"/patterns/([A-Za-z0-9_-]+)");
|
||
|
|
return match.Success ? match.Groups[1].Value : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
static XToysPattern Parse(string json)
|
||
|
|
{
|
||
|
|
using var doc = JsonDocument.Parse(json);
|
||
|
|
|
||
|
|
var pattern = doc.RootElement.GetProperty("result").GetProperty("pattern");
|
||
|
|
var name = pattern.GetProperty("name").GetString() ?? "Unknown";
|
||
|
|
var patternData = pattern.GetProperty("patternData");
|
||
|
|
|
||
|
|
// Resolve slider defaults
|
||
|
|
var sliders = new Dictionary<string, double>();
|
||
|
|
if (patternData.TryGetProperty("ma", out var ma))
|
||
|
|
{
|
||
|
|
foreach (var slider in ma.EnumerateArray())
|
||
|
|
{
|
||
|
|
var key = slider.GetProperty("key").GetString()!;
|
||
|
|
var value = slider.GetProperty("value").GetDouble();
|
||
|
|
sliders[key] = value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Evaluate patterns to per-tick value arrays
|
||
|
|
var int1 = EvaluatePattern(patternData, "Int1", sliders);
|
||
|
|
var int2 = EvaluatePattern(patternData, "Int2", sliders);
|
||
|
|
var freq1 = EvaluatePattern(patternData, "Freq1", sliders);
|
||
|
|
var freq2 = EvaluatePattern(patternData, "Freq2", sliders);
|
||
|
|
|
||
|
|
// All patterns in a group should have the same total duration.
|
||
|
|
// Use the max length to be safe.
|
||
|
|
int tickCount = Math.Max(int1.Length, Math.Max(int2.Length, Math.Max(freq1.Length, freq2.Length)));
|
||
|
|
|
||
|
|
var channelA = new List<WaveFrame>(tickCount);
|
||
|
|
var channelB = new List<WaveFrame>(tickCount);
|
||
|
|
|
||
|
|
for (int i = 0; i < tickCount; i++)
|
||
|
|
{
|
||
|
|
byte intA = i < int1.Length ? int1[i] : (byte)0;
|
||
|
|
byte intB = i < int2.Length ? int2[i] : (byte)0;
|
||
|
|
// Freq patterns output 0-100 (percentage). Map: 0% = 1000ms deep, 100% = 10ms buzzy.
|
||
|
|
byte freqA = i < freq1.Length ? Freq.Compress(FreqPctToMs(freq1[i])) : Freq.Compress(10);
|
||
|
|
byte freqB = i < freq2.Length ? Freq.Compress(FreqPctToMs(freq2[i])) : Freq.Compress(10);
|
||
|
|
|
||
|
|
channelA.Add(new WaveFrame(
|
||
|
|
new[] { freqA, freqA, freqA, freqA },
|
||
|
|
new[] { intA, intA, intA, intA }));
|
||
|
|
|
||
|
|
channelB.Add(new WaveFrame(
|
||
|
|
new[] { freqB, freqB, freqB, freqB },
|
||
|
|
new[] { intB, intB, intB, intB }));
|
||
|
|
}
|
||
|
|
|
||
|
|
return new XToysPattern(name, channelA, channelB);
|
||
|
|
}
|
||
|
|
|
||
|
|
static byte[] EvaluatePattern(JsonElement patternData, string patternName, Dictionary<string, double> sliders)
|
||
|
|
{
|
||
|
|
if (!patternData.TryGetProperty("patterns", out var patterns))
|
||
|
|
return Array.Empty<byte>();
|
||
|
|
if (!patterns.TryGetProperty(patternName, out var patternArr))
|
||
|
|
return Array.Empty<byte>();
|
||
|
|
|
||
|
|
var values = new List<byte>();
|
||
|
|
double lastEnd = 0;
|
||
|
|
|
||
|
|
foreach (var loop in patternArr.EnumerateArray())
|
||
|
|
{
|
||
|
|
var steps = loop.GetProperty("steps");
|
||
|
|
foreach (var step in steps.EnumerateArray())
|
||
|
|
{
|
||
|
|
var type = step.GetProperty("type").GetString() ?? "straight";
|
||
|
|
var time = ResolveExpr(GetStepValue(step, "time"), sliders);
|
||
|
|
var startVal = step.TryGetProperty("start", out var s)
|
||
|
|
? (s.ValueKind == JsonValueKind.Null ? lastEnd : ResolveExpr(GetStepValue(step, "start"), sliders))
|
||
|
|
: lastEnd;
|
||
|
|
var endVal = ResolveExpr(GetStepValue(step, "end"), sliders);
|
||
|
|
|
||
|
|
int ticks = Math.Max(1, (int)Math.Round(time / 0.1));
|
||
|
|
|
||
|
|
for (int t = 0; t < ticks; t++)
|
||
|
|
{
|
||
|
|
double frac = (double)t / ticks;
|
||
|
|
double v;
|
||
|
|
if (type == "sine")
|
||
|
|
v = startVal + (endVal - startVal) * (1 - Math.Cos(frac * Math.PI)) / 2;
|
||
|
|
else
|
||
|
|
v = startVal + (endVal - startVal) * frac;
|
||
|
|
|
||
|
|
values.Add((byte)Math.Clamp((int)Math.Round(v), 0, 100));
|
||
|
|
}
|
||
|
|
|
||
|
|
lastEnd = endVal;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return values.ToArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
static string GetStepValue(JsonElement step, string propName)
|
||
|
|
{
|
||
|
|
if (!step.TryGetProperty(propName, out var el))
|
||
|
|
return "0";
|
||
|
|
return el.ValueKind switch
|
||
|
|
{
|
||
|
|
JsonValueKind.String => el.GetString() ?? "0",
|
||
|
|
JsonValueKind.Number => el.GetDouble().ToString(),
|
||
|
|
_ => "0"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
static double ResolveExpr(string expr, Dictionary<string, double> sliders)
|
||
|
|
{
|
||
|
|
// Replace {var} references, then evaluate simple expressions like "ramp/2"
|
||
|
|
var resolved = Regex.Replace(expr, @"\{(\w+)\}", m =>
|
||
|
|
sliders.TryGetValue(m.Groups[1].Value, out var val) ? val.ToString() : "0");
|
||
|
|
|
||
|
|
// Parse simple arithmetic: number, or number/number
|
||
|
|
var parts = resolved.Split('/');
|
||
|
|
if (parts.Length == 2 && double.TryParse(parts[0], out var a) && double.TryParse(parts[1], out var b))
|
||
|
|
return a / b;
|
||
|
|
return double.TryParse(resolved, out var result) ? result : 0;
|
||
|
|
}
|
||
|
|
}
|