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

LogiBoardToolBox

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

公开
关注 0 Fork 0 Star 0
UTF-8
using LogiBoardToolBox.Core.Macros;

namespace LogiBoardToolBox.Core.Onboard;

public sealed record MacroAssignment(int ButtonIndex, bool GShift, CompiledMacro Macro);
public sealed record MacroLocation(ushort Sector, ushort Offset);
public sealed record MacroAllocation(Dictionary<int, byte[]> Sectors, Dictionary<(int Button, bool GShift), MacroLocation> Bindings);

public static class MacroAllocator
{
    /// <summary>Follows both explicit jumps and implicit continuation, including disabled profiles.</summary>
    public static HashSet<int> FindReferencedSectors(MemorySnapshot snapshot)
    {
        var used = new HashSet<int>();
        foreach (var profile in snapshot.GetProfiles())
        {
            // All records are reserved, including fields outside the advertised button count.
            for (var layer = 0; layer < 2; layer++)
            for (var i = 0; i < snapshot.Capabilities.BindingRecordCount; i++)
            {
                var binding = profile.GetBinding(i, layer == 1);
                if (!ButtonBinding.IsMacro(binding)) continue;
                var sector = (int)ButtonBinding.MacroSector(binding);
                var offset = (int)ButtonBinding.MacroOffset(binding);
                var visited = new HashSet<(int, int)>();
                var terminated = false;
                while (visited.Add((sector, offset)))
                {
                    if (sector < 0x100) used.Add(sector);
                    if (!snapshot.Sectors.TryGetValue(sector, out var data) || !SectorChecksum.IsValid(data))
                        throw new InvalidDataException($"现有宏引用了无法校验的扇区 {sector},无法分配新宏空间。");
                    if (offset < 0 || offset >= data.Length - 2) throw new InvalidDataException("现有宏的地址超出扇区。");
                    var opcode = data[offset];
                    if (opcode == 0xFF) { terminated = true; break; }
                    var length = MacroCompiler.InstructionSize(opcode);
                    if (offset + length > data.Length - 2) { sector++; offset = 0; continue; }
                    if (opcode == 0x60) { var nextOffset = data[offset + 1]; sector = data[offset + 2]; offset = nextOffset; continue; }
                    offset += length;
                    if (offset == data.Length - 2) { sector++; offset = 0; }
                    if (visited.Count > snapshot.Capabilities.SectorCount * snapshot.Capabilities.SectorSize)
                        throw new InvalidDataException("现有宏跳转链过长。");
                }
                if (!terminated) throw new InvalidDataException("现有宏存在循环跳转,无法安全分配新空间。");
            }
        }
        return used;
    }

    public static MacroAllocation Allocate(MemorySnapshot snapshot, IReadOnlyList<MacroAssignment> assignments)
    {
        if (assignments.Count == 0) return new([], []);
        var caps = snapshot.Capabilities;
        if (!caps.CanWriteMacros) throw new NotSupportedException("设备没有可用的格式 1 板载宏空间。");
        if (assignments.Any(a => a.ButtonIndex < 0 || a.ButtonIndex >= caps.AssignableButtonCount || a.GShift && !caps.HasGShift))
            throw new ArgumentException("宏绑定超出此鼠标实际提供的按键或 G-Shift 层。");
        if (assignments.Select(a => (a.ButtonIndex, a.GShift)).Distinct().Count() != assignments.Count)
            throw new ArgumentException("同一实体键和层不能分配多个宏。");
        var used = FindReferencedSectors(snapshot);
        var free = new Queue<int>(Enumerable.Range(caps.ProfileCount + 1, caps.SectorCount - caps.ProfileCount - 1)
            .Where(i => !used.Contains(i) && snapshot.Sectors.TryGetValue(i, out var data) && (SectorChecksum.IsValid(data) || data.All(b => b == 0xFF))));
        var sectors = new Dictionary<int, byte[]>();
        var locations = new Dictionary<(int, bool), MacroLocation>();
        var dedup = new Dictionary<string, MacroLocation>();
        // Reuse identical bytecode already referenced on the device. Automatic saves of a
        // DPI/name edit must not move every unchanged macro or rewrite a macro sector.
        foreach (var profile in snapshot.GetProfiles())
        for (var layer = 0; layer < 2; layer++)
        for (var button = 0; button < caps.BindingRecordCount; button++)
        {
            var binding = profile.GetBinding(button, layer == 1);
            if (!ButtonBinding.IsMacro(binding)) continue;
            var location = new MacroLocation(ButtonBinding.MacroSector(binding), ButtonBinding.MacroOffset(binding));
            var bytes = ReadMacroBytes(snapshot, location);
            // G HUB rounds each macro allocation up to a two-byte boundary. Old
            // versions packed odd-length programs without padding; retaining those
            // addresses would defeat the corrected allocator during a re-import.
            if ((location.Offset & 1) == 0) dedup.TryAdd(Convert.ToHexString(bytes), location);
        }
        var usable = caps.SectorSize - 2;
        var currentSector = -1;
        var currentOffset = 0;

        int NextSector()
        {
            if (!free.TryDequeue(out var sector))
                throw new InvalidOperationException("剩余板载宏空间不足。现有宏会被保留;可先移除旧宏的按键绑定并写入,再保存新宏。");
            sectors[sector] = Enumerable.Repeat((byte)0xFF, caps.SectorSize).ToArray();
            return sector;
        }

        foreach (var assignment in assignments)
        {
            if (assignment.ButtonIndex < 0 || assignment.ButtonIndex >= caps.AssignableButtonCount || assignment.GShift && !caps.HasGShift)
                throw new ArgumentOutOfRangeException(nameof(assignments));
            var instructions = assignment.Macro.Instructions;
            var key = Convert.ToHexString(assignment.Macro.Bytes);
            if (!dedup.TryGetValue(key, out var location))
            {
                if (assignment.Macro.Length <= usable)
                {
                    currentOffset = (currentOffset + 1) & ~1;
                    if (currentSector < 0 || usable - currentOffset < assignment.Macro.Length)
                    { currentSector = NextSector(); currentOffset = 0; }
                    location = new((ushort)currentSector, (ushort)currentOffset);
                    assignment.Macro.Bytes.CopyTo(sectors[currentSector], currentOffset);
                    currentOffset += assignment.Macro.Length;
                }
                else
                {
                    var sector = NextSector();
                    var offset = 0;
                    location = new((ushort)sector, 0);
                    for (var i = 0; i < instructions.Count; i++)
                    {
                        var remaining = instructions.Skip(i).Sum(x => x.Length);
                        if (remaining > usable - offset && instructions[i].Length + 3 > usable - offset)
                        {
                            var next = NextSector();
                            new byte[] { 0x60, 0, (byte)next }.CopyTo(sectors[sector], offset);
                            sector = next;
                            offset = 0;
                        }
                        instructions[i].CopyTo(sectors[sector], offset);
                        offset += instructions[i].Length;
                    }
                }
                dedup[key] = location;
            }
            if (!locations.TryAdd((assignment.ButtonIndex, assignment.GShift), location))
                throw new InvalidOperationException("同一个按键不能重复分配宏。");
        }
        foreach (var data in sectors.Values) SectorChecksum.Seal(data);
        return new(sectors, locations);
    }

    public static byte[] ReadMacroBytes(MemorySnapshot snapshot, MacroLocation location)
    {
        var bytes = new List<byte>(); var sector = (int)location.Sector; var offset = (int)location.Offset;
        var visited = new HashSet<(int, int)>();
        while (visited.Add((sector, offset)))
        {
            if (!snapshot.Sectors.TryGetValue(sector, out var data) || !SectorChecksum.IsValid(data)
                || offset < 0 || offset >= data.Length - 2)
                throw new InvalidDataException("宏地址无效或扇区校验失败。");
            var opcode = data[offset];
            if (opcode == 0xFF) { bytes.Add(0xFF); return bytes.ToArray(); }
            var size = MacroCompiler.InstructionSize(opcode);
            if (offset + size > data.Length - 2) { sector++; offset = 0; continue; }
            if (opcode == 0x60) { var next = data[offset + 2]; offset = data[offset + 1]; sector = next; continue; }
            bytes.AddRange(data.AsSpan(offset, size).ToArray()); offset += size;
            if (offset == data.Length - 2) { sector++; offset = 0; }
        }
        throw new InvalidDataException("现有宏跳转链存在循环。");
    }
}