XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

LogiBoardToolBox

【WinUI】罗技鼠标板载驱动程序

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
using LogiBoardToolBox.Core.Devices;

namespace LogiBoardToolBox.Core.Macros;

public sealed record GHubMacroBinding(string SlotId, int ButtonIndex, bool GShift, string CardId,
    string Name, string Script, MacroRepeat Repeat, string HoldScript, string ReleaseScript,
    string Color, IReadOnlyList<int> RecordedDelays, CompiledMacro Compiled);

public sealed record GHubMacroImport(string ProfileId, string ProfileName, string DeviceKey,
    IReadOnlyList<GHubMacroBinding> Bindings)
{
    /// <summary>Imports assigned sequence macros only. Other profile fields remain on the mouse.</summary>
    public static GHubMacroImport Read(string json, string deviceKey = "g502x-plus")
    {
        if (json.Length > 8 * 1024 * 1024) throw new InvalidDataException("G HUB 配置文件过大。");
        var model = MouseCatalog.BySlotPrefix(deviceKey) ?? throw new NotSupportedException("此型号尚未核对 G HUB 按键槽位,不能直接导入。");
        using var document = JsonDocument.Parse(json);
        var root = document.RootElement;
        var last = root.GetProperty("profiles/auto_switching/s_last_used_profile").GetString();
        var candidates = root.GetProperty("profiles").GetProperty("profiles").EnumerateArray()
            .Where(p => Text(p, "id") == last || Text(p, "applicationId") == last && Flag(p, "activeForApplication")).ToArray();
        if (candidates.Length != 1) throw new InvalidDataException("无法唯一确定 G HUB 最后使用的配置。");
        var profile = candidates[0];
        var cards = root.GetProperty("cards").GetProperty("cards").EnumerateArray().ToDictionary(c => Text(c, "id"));
        var bindings = new List<GHubMacroBinding>();
        var seen = new HashSet<string>();
        foreach (var assignment in profile.GetProperty("assignments").EnumerateArray())
        {
            var slot = Text(assignment, "slotId");
            if (!slot.StartsWith(deviceKey + "_", StringComparison.Ordinal)) continue;
            if (!seen.Add(slot)) throw new InvalidDataException($"G HUB 按键 {slot} 有重复分配。");
            var match = Regex.Match(slot, "^" + Regex.Escape(deviceKey) + @"_(g[1-9][0-9]?|scroll[12])_m1(_shifted)?$");
            if (!match.Success) continue; // Sensitivity and lighting cards are outside this import.
            var cardId = Text(assignment, "cardId");
            if (!cards.TryGetValue(cardId, out var card))
            {
                if (!cardId.StartsWith("0f82f693-5b78-4cf5-867e-", StringComparison.Ordinal))
                    throw new InvalidDataException($"按键 {slot} 引用的 G HUB 卡片不存在。");
                continue; // Built-in mouse functions already present on the device are preserved.
            }
            if (Text(card, "attribute") != "MACRO_PLAYBACK") continue;
            if (!model.TryGetButtonIndex(match.Groups[1].Value, out var index))
                throw new NotSupportedException($"{model.DisplayName} 的宏槽位 {slot} 尚未核对,停止导入以免绑定到其他实体键。");
            var name = Text(card, "name");
            var macro = card.GetProperty("macro");
            if (Text(macro, "type") != "SEQUENCE") throw new NotSupportedException($"宏“{name}”不是可导入的录制序列。");
            var sequence = macro.GetProperty("sequence");
            var flags = new[] { Flag(sequence, "useSimpleActions"), Flag(sequence, "useRepeatActions"), Flag(sequence, "useToggleActions") };
            if (flags.Count(f => f) > 1) throw new InvalidDataException($"宏“{name}”有冲突的重复模式。");
            var repeat = flags[0] ? MacroRepeat.Once : flags[1] ? MacroRepeat.WhileHeld : flags[2] ? MacroRepeat.Toggle : MacroRepeat.Sequence;
            var delays = new List<int>();
            var press = ReadPhase(sequence, repeat switch { MacroRepeat.Once => "simpleSequence", MacroRepeat.WhileHeld => "heldSequence", MacroRepeat.Toggle => "toggleSequence", _ => "pressSequence" }, delays);
            var hold = repeat == MacroRepeat.Sequence ? ReadPhase(sequence, "heldSequence", delays) : "";
            var release = repeat == MacroRepeat.Sequence ? ReadPhase(sequence, "releaseSequence", delays) : "";
            if (repeat != MacroRepeat.Sequence && (HasSteps(sequence, "pressSequence") || HasSteps(sequence, "releaseSequence")))
                throw new NotSupportedException($"宏“{name}”包含额外的按下/松开片段,不能丢弃片段后导入。");
            // G HUB serializes actual delay components. defaultDelay is editor metadata;
            // adding a uniform delay would change recorded down/up and inter-click timings.
            var compiled = MacroCompiler.CompileProgram(press, repeat, hold, release).ToOnboard();
            var color = macro.TryGetProperty("color", out var c) ? Text(c, "hex") : "#00A5DB";
            bindings.Add(new(slot, index, match.Groups[2].Success, cardId, name, press, repeat, hold, release,
                color, delays.ToArray(), compiled));
        }
        if (bindings.Count == 0) throw new InvalidDataException("当前配置没有为此鼠标分配录制宏。");
        return new(Text(profile, "id"), Text(profile, "name"), deviceKey, bindings);
    }

    private static bool HasSteps(JsonElement sequence, string key) => sequence.TryGetProperty(key, out var phase)
        && phase.TryGetProperty("components", out var components) && components.GetArrayLength() != 0;

    private static string ReadPhase(JsonElement sequence, string key, List<int> delays)
    {
        if (!sequence.TryGetProperty(key, out var phase) || !phase.TryGetProperty("components", out var components)) return "";
        var lines = new List<string>();
        foreach (var component in components.EnumerateArray())
        {
            var fields = component.EnumerateObject().ToArray();
            if (fields.Length != 1) throw new NotSupportedException("宏步骤必须包含一种明确的操作。");
            var field = fields[0];
            switch (field.Name)
            {
                case "delay":
                    var duration = field.Value.TryGetProperty("durationMs", out var ms) ? ms.GetInt32() : 0;
                    if (duration < 0) throw new InvalidDataException("录制延迟不能为负数。");
                    delays.Add(duration);
                    for (var remaining = duration; remaining > 0;)
                    {
                        var part = Math.Min(remaining, ushort.MaxValue); lines.Add($"delay {part}"); remaining -= part;
                        if (lines.Count > 10000) throw new InvalidDataException("宏指令数超过限制。");
                    }
                    break;
                case "keyboard":
                    var usage = Usage(field.Value, 255);
                    lines.Add($"key 0x{usage:X2} {(Flag(field.Value, "isDown") ? "down" : "up")}");
                    break;
                case "mouse":
                    if (!field.Value.TryGetProperty("button", out var button)) throw new NotSupportedException("此鼠标操作尚不能保持原样导入。");
                    lines.Add($"mouse {Usage(button, 16)} {(Flag(button, "isDown") ? "down" : "up")}");
                    break;
                default: throw new NotSupportedException($"宏步骤 {field.Name} 尚不能保持原样导入。");
            }
            if (lines.Count > 10000) throw new InvalidDataException("宏指令数超过限制。");
        }
        return string.Join('\n', lines);
    }

    private static int Usage(JsonElement element, int maximum)
    {
        var property = element.GetProperty("hidUsage");
        var value = property.ValueKind == JsonValueKind.String ? int.Parse(property.GetString()!, CultureInfo.InvariantCulture) : property.GetInt32();
        if (value < 1 || value > maximum) throw new NotSupportedException($"不支持的 HID usage {value}。");
        return value;
    }
    private static bool Flag(JsonElement element, string key) => element.TryGetProperty(key, out var value) && value.ValueKind == JsonValueKind.True;
    private static string Text(JsonElement element, string key) => element.TryGetProperty(key, out var value) ? value.GetString() ?? "" : "";
}