using System.Net.Http; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; namespace Substation; public record XToysPattern(string Name, List ChannelA, List ChannelB); public static class XToysPatternImporter { static readonly HttpClient Http = new(); public static async Task 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/"); 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(); 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(tickCount); var channelB = new List(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 sliders) { if (!patternData.TryGetProperty("patterns", out var patterns)) return Array.Empty(); if (!patterns.TryGetProperty(patternName, out var patternArr)) return Array.Empty(); var values = new List(); 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 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; } }