From 702829bb2052ceacf9172a399664b936e0fb079d Mon Sep 17 00:00:00 2001 From: Kurt Date: Wed, 29 Oct 2025 16:34:10 -0500 Subject: [PATCH] Add more event block repo types Allow bigger window --- .../Saves/Access/SaveBlockAccessor9ZA.cs | 19 +- .../Gen9/ZA/EventWorkStorage64.cs | 340 ++++++-- .../Save Editors/Gen9/EventWorkGrid64.cs | 745 ++++++++++++++---- .../Gen9/SAV_FlagWork9a.Designer.cs | 19 +- .../Save Editors/Gen9/SAV_FlagWork9a.cs | 33 +- 5 files changed, 915 insertions(+), 241 deletions(-) diff --git a/PKHeX.Core/Saves/Access/SaveBlockAccessor9ZA.cs b/PKHeX.Core/Saves/Access/SaveBlockAccessor9ZA.cs index f44556396..c4baa9f8b 100644 --- a/PKHeX.Core/Saves/Access/SaveBlockAccessor9ZA.cs +++ b/PKHeX.Core/Saves/Access/SaveBlockAccessor9ZA.cs @@ -34,6 +34,14 @@ public sealed class SaveBlockAccessor9ZA(SAV9ZA sav) : SCBlockAccessor public EventWorkValueStorage WorkSpawn { get; } = new(sav, Block(sav, KEventWorkSpawn)); public EventWorkFlagStorage Flags { get; } = new(sav, Block(sav, KEventFlagsOther)); + public EventWorkValueStorageKey128 Report { get; } = new(sav, Block(sav, KEventReport)); + public EventWorkValueStorageKey128 Obstruction { get; } = new(sav, Block(sav, KObstruction)); + public EventWorkValueStorageKey192 FieldObjectInteractable { get; } = new(sav, Block(sav, KFieldObjectInteractable)); + + public EventWorkValueStorageKey128 Spawner2 { get; } = new(sav, Block(sav, KEventSpawner2)); // (u64-key, u64-bool, u64-struct) + public EventWorkValueStorage InfiniteRank { get; } = new(sav, Block(sav, KEventInfiniteRank)); // (u64-key, u64-struct) + public EventWorkValueStorageKey128 Spawner4 { get; } = new(sav, Block(sav, KEventSpawner4)); // (u64-key, u64-hash, u64-struct) + public MableStatus9a Mable { get; } = new(sav, Block(sav, KStatusMable)); private const uint KBox = 0x0d66012c; // Box Data @@ -64,12 +72,15 @@ public sealed class SaveBlockAccessor9ZA(SAV9ZA sav) : SCBlockAccessor private const uint KEventCountTitle = 0x2C2C6964; // title_count (u64,u64)[64] - Player earned display titles private const uint KEventWorkSpawn = 0x53FD0223; // Overworld Spawner 0x46500 small values (u64,u64)[18000] - // 7C896A83 0x2000 unused - // B25E7EE5 0x400 unused - // AF2165F0 0x3000 (u64,u64,value) + private const uint KEventSpawner2 = 0x79ABCB0B; // (u64-key, u64-bool, u64-struct) + private const uint KEventInfiniteRank = 0x7C896A83; // (u64-key, u64-struct) + private const uint KEventSpawner4 = 0xD1A3FF7B; // (u64-key, u64-hash, u64-struct) + // B25E7EE5 0x400 unused + + private const uint KEventReport = 0xAF2165F0; // 0x3000 (u64,(s64,u64) value) + private const uint KObstruction = 0x4C26C29B; // (u64, u64-state, u64-unused)[2000] private const uint KFieldObjectInteractable = 0x7147C953; // (u64,u64,u64,value)[5000] (mega crystal, prize medals) - private const uint KObstruction = 0x4C26C29B; // (u64, u64-state, u64-unused)[2000] public const uint KTicketPointsZARoyale = 0x9A730DE1; // u32 public const uint KTicketPointsZARoyaleInfinite = 0x1D7EE369; // u32 diff --git a/PKHeX.Core/Saves/Substructures/Gen9/ZA/EventWorkStorage64.cs b/PKHeX.Core/Saves/Substructures/Gen9/ZA/EventWorkStorage64.cs index 25008127f..fa1b6bba9 100644 --- a/PKHeX.Core/Saves/Substructures/Gen9/ZA/EventWorkStorage64.cs +++ b/PKHeX.Core/Saves/Substructures/Gen9/ZA/EventWorkStorage64.cs @@ -5,29 +5,41 @@ namespace PKHeX.Core; /// -/// Storage for event work values keyed by a 64-bit FNV-1a hash. +/// Generic storage for event work values keyed by a fixed-size key and backed by a 64-bit value field. /// -/// Unmanaged value type stored alongside the hash. Backed by a 64-bit field. +/// Key type. Implementations serialize/deserialize to a fixed-size number of 64-bit chunks. +/// Unmanaged value type stored alongside the key. Backed by a 64-bit field. /// Owning save file. /// Underlying block providing the backing bytes. -public abstract class EventWorkStorage64(SaveFile sav, SCBlock block) : SaveBlock(sav, block.Raw), - IEventValueStorage where T : struct, IEquatable +public abstract class EventWorkStorage(SaveFile sav, SCBlock block) : SaveBlock(sav, block.Raw) + where TKey : struct, IEquatable + where TValue : struct, IEquatable { // Structure: - // entry[] of u64 hash, T value - // T is backed by a separate u64 - // Essentially, it is - // (u64, u64)[Count]; - // Empty entries use an empty hash value instead of 0. + // entry[] of key(u64 * N), value(u64) + // Empty entries use an implementation-defined empty key value. // Default state is all entries initialized to "empty". - /// - /// Size in bytes of a single entry: 8 bytes for the key hash + 8 bytes for the value. - /// - public const int ENTRY_SIZE = 16; - private const ulong HashEmpty = FnvHash.HashEmpty; + /// Number of 64-bit chunks used to serialize a key. + protected abstract int KeyU64Count { get; } + + /// Gets the empty key sentinel. + protected abstract TKey EmptyKey { get; } + + /// Reads the key at the specified index. + protected abstract TKey ReadKey(int index); + + /// Writes the key at the specified index. + protected abstract void WriteKey(int index, TKey key); + + /// Compares two keys for sorting. Return same semantics as . + protected abstract int CompareKeys(TKey a, TKey b); + + /// Size in bytes of a single entry: key bytes (8 * KeyU64Count) + 8 bytes for the value. + protected int EntrySize => (KeyU64Count * sizeof(ulong)) + sizeof(ulong); + + public int Count => Data.Length / EntrySize; - public int Count => Data.Length / ENTRY_SIZE; public int CountUsed => ScanCountUsed(Data); private int ScanCountUsed(ReadOnlySpan data) @@ -35,61 +47,44 @@ private int ScanCountUsed(ReadOnlySpan data) int count = 0; for (int i = 0; i < Count; i++) { - var hash = ReadUInt64LittleEndian(data[GetOffset(i)..]); - if (hash == 0) + var key = ReadKey(i); + if (IsEmpty(key)) break; count++; } return count; } - private static ulong Hash(ReadOnlySpan name) => FnvHash.HashFnv1a_64(name); - - public bool HasSpace(out int index) => (index = CountUsed) < Count; - public bool HasSpace() => HasSpace(out _); - public bool TryGetValue(ReadOnlySpan name, out T value) => TryGetValue(Hash(name), out value); - - public bool TryGetValue(ulong hash, out T value) - { - var index = GetIndex(hash); - if (index == -1) - { - value = default; - return false; - } - value = GetValue(index); - return true; - } - protected int GetOffset(int index) { if ((uint)index > Count) throw new ArgumentOutOfRangeException(nameof(index), index, null); - return index * ENTRY_SIZE; + return index * EntrySize; } - public int GetIndex(ulong hash) + protected bool IsEmpty(TKey key) => EqualityComparer.Default.Equals(key, EmptyKey); + + public bool HasSpace(out int index) => (index = CountUsed) < Count; + public bool HasSpace() => HasSpace(out _); + + public int GetIndex(TKey key) { for (int i = 0; i < Count; i++) { - var ofs = i * ENTRY_SIZE; - var entryHash = ReadUInt64LittleEndian(Data.Slice(ofs, 8)); - if (entryHash == hash) + var entryKey = ReadKey(i); + if (entryKey.Equals(key)) return i; - if (entryHash == HashEmpty) + if (IsEmpty(entryKey)) break; } return -1; } - public void Clear(int index) => WriteEntry(Data[GetOffset(index)..], HashEmpty, 0); - - private void WriteEntry(int index, ulong hash, ulong value) => WriteEntry(Data[GetOffset(index)..], hash, value); - - private static void WriteEntry(Span data, ulong hash, ulong value) + public void Clear(int index) { - WriteUInt64LittleEndian(data, hash); - WriteUInt64LittleEndian(data[8..], value); + // Write empty key and zero value + WriteKey(index, EmptyKey); + WriteUInt64LittleEndian(Data.Slice(GetOffset(index) + (KeyU64Count * sizeof(ulong)), sizeof(ulong)), 0); } public void Compress() @@ -98,16 +93,16 @@ public void Compress() int writePtr = 0; for (int readPtr = 0; readPtr < count; readPtr++) { - var readOfs = GetOffset(readPtr); - var entryHash = ReadUInt64LittleEndian(Data.Slice(readOfs, 8)); - if (entryHash == HashEmpty) + var entryKey = ReadKey(readPtr); + if (IsEmpty(entryKey)) continue; // skip empty if (writePtr != readPtr) { var writeOfs = GetOffset(writePtr); + var readOfs = GetOffset(readPtr); // Move entry - var dest = Data.Slice(writeOfs, ENTRY_SIZE); - var src = Data.Slice(readOfs, ENTRY_SIZE); + var dest = Data.Slice(writeOfs, EntrySize); + var src = Data.Slice(readOfs, EntrySize); src.CopyTo(dest); // Clear old entry Clear(readPtr); @@ -119,50 +114,203 @@ public void Compress() public void Sort() { Compress(); - var entries = new (ulong Hash, ulong Value)[CountUsed]; + var used = CountUsed; + var entries = new (TKey Key, ulong Value)[used]; // Gather used entries. for (int i = 0; i < entries.Length; i++) { var data = Data[GetOffset(i)..]; - var entryHash = ReadUInt64LittleEndian(data); - var entryValue = ReadUInt64LittleEndian(data[8..]); - entries[i] = (entryHash, entryValue); + var key = ReadKey(i); + var value = ReadUInt64LittleEndian(data.Slice(KeyU64Count * sizeof(ulong), sizeof(ulong))); + entries[i] = (key, value); } - Array.Sort(entries, Comparer<(ulong Hash, ulong Value)>.Create((a, b) => a.Hash.CompareTo(b.Hash))); + Array.Sort(entries, Comparer<(TKey Key, ulong Value)>.Create((a, b) => CompareKeys(a.Key, b.Key))); // Write back for (int i = 0; i < entries.Length; i++) - WriteEntry(i, entries[i].Hash, entries[i].Value); + { + WriteKey(i, entries[i].Key); + WriteUInt64LittleEndian(Data.Slice(GetOffset(i) + (KeyU64Count * sizeof(ulong)), sizeof(ulong)), entries[i].Value); + } } - public ulong GetKey(int index) => ReadUInt64LittleEndian(Data.Slice(GetOffset(index), 8)); - public void SetKey(int index, ulong key) => WriteUInt64LittleEndian(Data.Slice(GetOffset(index), 8), key); - public abstract T GetValue(int index); - public abstract void SetValue(int index, T value); + public abstract TValue GetValue(int index); + public abstract void SetValue(int index, TValue value); - public T this[ReadOnlySpan name] + public bool TryGetValue(TKey key, out TValue value) + { + var index = GetIndex(key); + if (index == -1) + { + value = default; + return false; + } + value = GetValue(index); + return true; + } + + public void SetValue(TKey key, TValue value) + { + var index = GetIndex(key); + if (index == -1 && !HasSpace(out index)) + throw new InvalidOperationException("No space available to add new entry."); + // Only ensure the key exists; value writing is done via SetValue(index, ...) + WriteKey(index, key); + } +} + +/// +/// Storage for event work values keyed by a 64-bit FNV-1a hash. +/// +/// Unmanaged value type stored alongside the hash. Backed by a 64-bit field. +/// Owning save file. +/// Underlying block providing the backing bytes. +public abstract class EventWorkStorage64(SaveFile sav, SCBlock block) : EventWorkStorage(sav, block), + IEventValueStorage where TValue : struct, IEquatable +{ + /// + /// Size in bytes of a single entry: 8 bytes for the key hash + 8 bytes for the value. + /// + public const int ENTRY_SIZE = 16; + private const ulong HashEmpty = FnvHash.HashEmpty; + + protected override int KeyU64Count => 1; + protected override ulong EmptyKey => HashEmpty; + + protected override int CompareKeys(ulong a, ulong b) => a.CompareTo(b); + + protected override ulong ReadKey(int index) + { + var ofs = GetOffset(index); + return ReadUInt64LittleEndian(Data.Slice(ofs, sizeof(ulong))); + } + + protected override void WriteKey(int index, ulong key) + { + var ofs = GetOffset(index); + WriteUInt64LittleEndian(Data.Slice(ofs, sizeof(ulong)), key); + } + + private static ulong Hash(ReadOnlySpan name) => FnvHash.HashFnv1a_64(name); + + public bool TryGetValue(ReadOnlySpan name, out TValue value) => TryGetValue(Hash(name), out value); + + public ulong GetKey(int index) => ReadKey(index); + public void SetKey(int index, ulong key) => WriteKey(index, key); + + public TValue this[ReadOnlySpan name] { get => TryGetValue(name, out var val) ? val : throw new KeyNotFoundException($"No entry found for name '{name.ToString()}'"); set => SetValue(name, value); } - public T this[ulong hash] + public TValue this[ulong hash] { get => TryGetValue(hash, out var val) ? val : throw new KeyNotFoundException($"No entry found for hash '{hash:X16}'"); set => SetValue(hash, value); } - public void SetValue(ulong hash, T value) + public void SetValue(ReadOnlySpan name, TValue value) => SetValue(Hash(name), value); +} + +/// +/// Storage for event work values keyed by a 128-bit composite FNV-1a hash (two 64-bit parts). +/// +/// Unmanaged value type stored alongside the key. Backed by a 64-bit field. +/// Owning save file. +/// Underlying block providing the backing bytes. +public abstract class EventWorkStorageKey128(SaveFile sav, SCBlock block) : EventWorkStorage<(ulong A, ulong B), TValue>(sav, block) + where TValue : struct, IEquatable +{ + protected override int KeyU64Count => 2; + protected override (ulong A, ulong B) EmptyKey => (FnvHash.HashEmpty, FnvHash.HashEmpty); + + protected override int CompareKeys((ulong A, ulong B) a, (ulong A, ulong B) b) { - var index = GetIndex(hash); - if (index == -1 && !HasSpace(out index)) - throw new InvalidOperationException("No space available to add new entry."); - WriteEntry(index, hash, 0); + int cmp = a.A.CompareTo(b.A); + return cmp != 0 ? cmp : a.B.CompareTo(b.B); } - public void SetValue(ReadOnlySpan name, T value) => SetValue(Hash(name), value); + protected override (ulong A, ulong B) ReadKey(int index) + { + var ofs = GetOffset(index); + var a = ReadUInt64LittleEndian(Data.Slice(ofs + (0 * sizeof(ulong)), sizeof(ulong))); + var b = ReadUInt64LittleEndian(Data.Slice(ofs + (1 * sizeof(ulong)), sizeof(ulong))); + return (a, b); + } + + protected override void WriteKey(int index, (ulong A, ulong B) key) + { + var ofs = GetOffset(index); + WriteUInt64LittleEndian(Data.Slice(ofs + (0 * sizeof(ulong)), sizeof(ulong)), key.A); + WriteUInt64LittleEndian(Data.Slice(ofs + (1 * sizeof(ulong)), sizeof(ulong)), key.B); + } + + private static ulong Hash(ReadOnlySpan name) => FnvHash.HashFnv1a_64(name); + + // Helper methods for convenience + public bool TryGetValue(ReadOnlySpan a, ReadOnlySpan b, out TValue value) + => TryGetValue((Hash(a), Hash(b)), out value); + + public void SetValue(ReadOnlySpan a, ReadOnlySpan b, TValue value) + => SetValue((Hash(a), Hash(b)), value); + + public (ulong A, ulong B) GetKey(int index) => ReadKey(index); + public void SetKey(int index, (ulong A, ulong B) key) => WriteKey(index, key); + public void SetKey(int index, ulong a, ulong b) => WriteKey(index, (a, b)); +} + +/// +/// Storage for event work values keyed by a 192-bit composite FNV-1a hash (three 64-bit parts). +/// +/// Unmanaged value type stored alongside the key. Backed by a 64-bit field. +/// Owning save file. +/// Underlying block providing the backing bytes. +public abstract class EventWorkStorageKey192(SaveFile sav, SCBlock block) : EventWorkStorage<(ulong A, ulong B, ulong C), TValue>(sav, block) + where TValue : struct, IEquatable +{ + protected override int KeyU64Count => 3; + protected override (ulong A, ulong B, ulong C) EmptyKey => (FnvHash.HashEmpty, FnvHash.HashEmpty, FnvHash.HashEmpty); + + protected override int CompareKeys((ulong A, ulong B, ulong C) a, (ulong A, ulong B, ulong C) b) + { + int cmp = a.A.CompareTo(b.A); + if (cmp != 0) return cmp; + cmp = a.B.CompareTo(b.B); + return cmp != 0 ? cmp : a.C.CompareTo(b.C); + } + + protected override (ulong A, ulong B, ulong C) ReadKey(int index) + { + var ofs = GetOffset(index); + var a = ReadUInt64LittleEndian(Data.Slice(ofs + (0 * sizeof(ulong)), sizeof(ulong))); + var b = ReadUInt64LittleEndian(Data.Slice(ofs + (1 * sizeof(ulong)), sizeof(ulong))); + var c = ReadUInt64LittleEndian(Data.Slice(ofs + (2 * sizeof(ulong)), sizeof(ulong))); + return (a, b, c); + } + + protected override void WriteKey(int index, (ulong A, ulong B, ulong C) key) + { + var ofs = GetOffset(index); + WriteUInt64LittleEndian(Data.Slice(ofs + (0 * sizeof(ulong)), sizeof(ulong)), key.A); + WriteUInt64LittleEndian(Data.Slice(ofs + (1 * sizeof(ulong)), sizeof(ulong)), key.B); + WriteUInt64LittleEndian(Data.Slice(ofs + (2 * sizeof(ulong)), sizeof(ulong)), key.C); + } + + private static ulong Hash(ReadOnlySpan name) => FnvHash.HashFnv1a_64(name); + + // Helper methods for convenience + public bool TryGetValue(ReadOnlySpan a, ReadOnlySpan b, ReadOnlySpan c, out TValue value) + => TryGetValue((Hash(a), Hash(b), Hash(c)), out value); + + public void SetValue(ReadOnlySpan a, ReadOnlySpan b, ReadOnlySpan c, TValue value) + => SetValue((Hash(a), Hash(b), Hash(c)), value); + + public (ulong A, ulong B, ulong C) GetKey(int index) => ReadKey(index); + public void SetKey(int index, (ulong A, ulong B, ulong C) key) => WriteKey(index, key); + public void SetKey(int index, ulong a, ulong b, ulong c) => WriteKey(index, (a, b, c)); } /// @@ -205,3 +353,53 @@ public override void SetValue(int index, ulong value) } } + +/// +/// Event work storage specialized for 64-bit integer values. +/// +/// Owning save file. +/// Underlying block providing the backing bytes. +public abstract class EventWorkTupleStorage(SAV9ZA sav, SCBlock block) : EventWorkStorageKey128<(T1, T2)>(sav, block) +{ +} + +/// +/// Event work storage specialized for 64-bit integer values with 128-bit keys. +/// +/// Owning save file. +/// Underlying block providing the backing bytes. +public sealed class EventWorkValueStorageKey128(SAV9ZA sav, SCBlock block) : EventWorkStorageKey128(sav, block) +{ + public override ulong GetValue(int index) + { + var ofs = GetOffset(index); + return ReadUInt64LittleEndian(Data.Slice(ofs + (KeyU64Count * sizeof(ulong)), sizeof(ulong))); + } + + public override void SetValue(int index, ulong value) + { + var ofs = GetOffset(index); + WriteUInt64LittleEndian(Data.Slice(ofs + (KeyU64Count * sizeof(ulong)), sizeof(ulong)), value); + } +} + +/// +/// Event work storage specialized for 64-bit integer values with 192-bit keys. +/// +/// Owning save file. +/// Underlying block providing the backing bytes. +public sealed class EventWorkValueStorageKey192(SAV9ZA sav, SCBlock block) : EventWorkStorageKey192(sav, block) +{ + public override ulong GetValue(int index) + { + var ofs = GetOffset(index); + return ReadUInt64LittleEndian(Data.Slice(ofs + (KeyU64Count * sizeof(ulong)), sizeof(ulong))); + } + + public override void SetValue(int index, ulong value) + { + var ofs = GetOffset(index); + WriteUInt64LittleEndian(Data.Slice(ofs + (KeyU64Count * sizeof(ulong)), sizeof(ulong)), value); + } +} + diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen9/EventWorkGrid64.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen9/EventWorkGrid64.cs index 20416f8ab..5575c42bf 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen9/EventWorkGrid64.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen9/EventWorkGrid64.cs @@ -15,34 +15,132 @@ public interface IEventWorkGrid void Save(); } +/// +/// Immutable helper for name/hash lookup and reverse lookup. +/// +public sealed class EventWorkLookup(Dictionary forward) +{ + private readonly Dictionary _reverse = forward.ToDictionary(z => z.Value, z => z.Key); + + public string GetName(ulong hash) + { + if (forward.TryGetValue(hash, out var name)) + return name; + return hash.ToString("X16"); + } + + public ulong GetHash(string nameOrHex) + { + if (_reverse.TryGetValue(nameOrHex, out var h)) + return h; + if (ulong.TryParse(nameOrHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed)) + return parsed; + return FnvHash.HashFnv1a_64(nameOrHex); + } +} + +/// +/// Base class with shared UI helpers and wiring for EventWork grids. +/// +public abstract record EventWorkGridBase(SplitContainer Container, DataGridView Grid, TextBox Search) + : IEventWorkGrid +{ + protected static (SplitContainer Container, TextBox Search) CreateContainerCommon() + { + var container = new SplitContainer + { + Dock = DockStyle.Fill, + Orientation = Orientation.Horizontal, + FixedPanel = FixedPanel.Panel1, + IsSplitterFixed = true, + Margin = new Padding(0), + }; + + var search = new TextBox + { + Dock = DockStyle.Fill, + Margin = new Padding(0), + PlaceholderText = "Search...", + }; + container.Panel1.Controls.Add(search); + + var topHeight = Math.Max(search.PreferredHeight + 4, 24); + container.Panel1MinSize = topHeight; + container.SplitterDistance = topHeight; + return (container, search); + } + + protected static DataGridViewTextBoxColumn MakeIndexColumn(Font font) => new() + { + Name = "Index", + HeaderText = "Index", + DefaultCellStyle = new DataGridViewCellStyle { Font = font }, + Width = 40, + ReadOnly = true, + }; + + protected static DataGridViewTextBoxColumn MakeKeyColumn(Font font, string title = "Name") => new() + { + Name = title, + HeaderText = title, + DefaultCellStyle = new DataGridViewCellStyle + { + Font = font, + Alignment = DataGridViewContentAlignment.MiddleLeft, + }, + }; + + protected static DataGridViewTextBoxColumn MakeValueNumberColumn(Font font, string title) => new() + { + Name = title, + HeaderText = title, + DefaultCellStyle = new DataGridViewCellStyle + { + Font = font, + Alignment = DataGridViewContentAlignment.MiddleRight, + NullValue = 0UL, + }, + }; + + protected static DataGridViewCheckBoxColumn MakeValueBoolColumn(Font font, string title) => new() + { + Name = title, + HeaderText = title, + DefaultCellStyle = new DataGridViewCellStyle + { + Font = font, + Alignment = DataGridViewContentAlignment.MiddleCenter, + NullValue = false, + }, + }; + + public abstract void Load(); + public abstract void Save(); +} + /// /// Helper that creates and manages a DataGridView for editing an block. /// /// Value type for the storage. Use bool for flags, ulong for values. -public sealed class EventWorkGrid64 : IEventWorkGrid where T : struct, IEquatable +public sealed record EventWorkGrid64 : EventWorkGridBase where T : struct, IEquatable { private readonly EventWorkStorage64 Storage; - private readonly Dictionary Lookup; - private readonly DoubleBufferedDataGridView Grid; - private readonly SplitContainer Container; - private readonly TextBox SearchBox; + private readonly EventWorkLookup Names; private const int ColumnIndex = 0; private const int ColumnValue = 1; private const int ColumnName = 2; - private EventWorkGrid64(EventWorkStorage64 storage, Dictionary lookup, SplitContainer container, DoubleBufferedDataGridView grid, TextBox search) + private EventWorkGrid64(EventWorkStorage64 storage, EventWorkLookup names, SplitContainer container, DataGridView grid, TextBox search) + : base(container, grid, search) { Storage = storage; - Lookup = lookup; - Container = container; - Grid = grid; - SearchBox = search; + Names = names; if (typeof(T) == typeof(ulong)) Grid.CellValidated += ValidateU64; - SearchBox.TextChanged += (_, _) => ApplyFilter(SearchBox.Text); + Search.TextChanged += (_, _) => ApplyFilter(Search.Text); ToggleAutoSize(true); } @@ -62,68 +160,22 @@ private void ValidateU64(object? sender, DataGridViewCellEventArgs e) var row = Grid.Rows[e.RowIndex]; var cell = row.Cells[e.ColumnIndex]; var text = cell.Value?.ToString(); - if (ulong.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _)) + if (ulong.TryParse(text, CultureInfo.InvariantCulture, out _)) return; WinFormsUtil.Alert("Please enter a valid unsigned integer value."); - // Revert to the original stored value. - // Rather than use RowIndex, which may have changed due to sorting, use the Index cell value. var index = Convert.ToInt32(row.Cells[ColumnIndex].Value); var saved = Storage.GetValue(index); cell.Value = saved.ToString(); } - public static EventWorkGrid64 CreateFlags(TabPage host, EventWorkFlagStorage storage, Dictionary lookup) + public static EventWorkGrid64 CreateFlags(TabPage host, EventWorkFlagStorage storage, EventWorkLookup names) { - var (container, grid, search) = CreateContainer(true); + var (container, search) = CreateContainerCommon(); host.Controls.Clear(); host.Controls.Add(container); - return new EventWorkGrid64(storage, lookup, container, grid, search); - } - public static EventWorkGrid64 CreateValues(TabPage host, EventWorkValueStorage storage, Dictionary lookup) - { - var (container, grid, search) = CreateContainer(false); - host.Controls.Clear(); - host.Controls.Add(container); - return new EventWorkGrid64(storage, lookup, container, grid, search); - } - - private static (SplitContainer Container, DoubleBufferedDataGridView Grid, TextBox Search) CreateContainer(bool isBoolean) - { - var container = new SplitContainer - { - Dock = DockStyle.Fill, - Orientation = Orientation.Horizontal, - FixedPanel = FixedPanel.Panel1, - IsSplitterFixed = true, - Margin = new Padding(0), - }; - - var search = new TextBox - { - Dock = DockStyle.Fill, - Margin = new Padding(0), - PlaceholderText = "Search...", - }; - - // Default row font to Courier New (match designer) var font = new Font("Courier New", 8F, FontStyle.Regular, GraphicsUnit.Point, 0); - var grid = CreateGrid(isBoolean, font); - - container.Panel1.Controls.Add(search); - container.Panel2.Controls.Add(grid); - - // Size the top panel to the search box height, leave rest for grid. - var topHeight = Math.Max(search.PreferredHeight + 4, 24); - container.Panel1MinSize = topHeight; - container.SplitterDistance = topHeight; - - return (container, grid, search); - } - - private static DoubleBufferedDataGridView CreateGrid(bool isBoolean, Font font) - { var dgv = new DoubleBufferedDataGridView { AllowUserToAddRows = false, @@ -138,62 +190,38 @@ private static DoubleBufferedDataGridView CreateGrid(bool isBoolean, Font font) DefaultCellStyle = new DataGridViewCellStyle { Font = font }, AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle { BackColor = SystemColors.ControlLight }, }; - - var colIndex = GetColumnIndex("Index", font); - var colName = GetColumnName("Name", font); - var colValue = GetColumnValue("Value", font, isBoolean); - dgv.Columns.AddRange(colIndex, colValue, colName); - return dgv; + dgv.Columns.AddRange(MakeIndexColumn(font), MakeValueBoolColumn(font, "Value"), MakeKeyColumn(font)); + container.Panel2.Controls.Add(dgv); + return new EventWorkGrid64(storage, names, container, dgv, search); } - private static DataGridViewTextBoxColumn GetColumnIndex(string title, Font font) => new() + public static EventWorkGrid64 CreateValues(TabPage host, EventWorkValueStorage storage, EventWorkLookup names) { - Name = title, - HeaderText = title, - DefaultCellStyle = new DataGridViewCellStyle { Font = font }, - Width = 40, - ReadOnly = true, - }; + var (container, search) = CreateContainerCommon(); + host.Controls.Clear(); + host.Controls.Add(container); - private static DataGridViewTextBoxColumn GetColumnName(string title, Font font) => new() - { - Name = title, - HeaderText = title, - DefaultCellStyle = new DataGridViewCellStyle { Font = font }, - }; - - private static DataGridViewColumn GetColumnValue(string title, Font font, bool isBoolean) - { - if (isBoolean) - return GetValueColumnBool(title, font); - return GetValueColumnNumber(title, font); + var font = new Font("Courier New", 8F, FontStyle.Regular, GraphicsUnit.Point, 0); + var dgv = new DoubleBufferedDataGridView + { + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + AllowUserToOrderColumns = true, + AllowUserToResizeRows = false, + AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells, + AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None, + Dock = DockStyle.Fill, + RowHeadersVisible = false, + Margin = new Padding(0), + DefaultCellStyle = new DataGridViewCellStyle { Font = font }, + AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle { BackColor = SystemColors.ControlLight }, + }; + dgv.Columns.AddRange(MakeIndexColumn(font), MakeValueNumberColumn(font, "Value"), MakeKeyColumn(font)); + container.Panel2.Controls.Add(dgv); + return new EventWorkGrid64(storage, names, container, dgv, search); } - private static DataGridViewTextBoxColumn GetValueColumnNumber(string title, Font font) => new() - { - Name = title, - HeaderText = title, - DefaultCellStyle = new DataGridViewCellStyle - { - Font = font, - Alignment = DataGridViewContentAlignment.MiddleRight, - NullValue = 0, - }, - }; - - private static DataGridViewCheckBoxColumn GetValueColumnBool(string title, Font font) => new() - { - Name = title, - HeaderText = title, - DefaultCellStyle = new DataGridViewCellStyle - { - Font = font, - Alignment = DataGridViewContentAlignment.MiddleCenter, - NullValue = false, - }, - }; - - public void Load() + public override void Load() { var rows = Grid.Rows; rows.Clear(); @@ -203,7 +231,7 @@ public void Load() for (int i = 0; i < count; i++) { var hash = Storage.GetKey(i); - var name = GetNameDisplay(hash); + var name = Names.GetName(hash); var value = Storage.GetValue(i); var row = rows[i]; @@ -214,17 +242,6 @@ public void Load() } } - private string GetNameDisplay(ulong hash) => GetNameDisplay(hash, Lookup); - - public static string GetNameDisplay(ulong hash, IReadOnlyDictionary lookup) - { - if (lookup.TryGetValue(hash, out var name)) - return name; - if (hash == FnvHash.HashEmpty) - return string.Empty; - return hash.ToString("X16"); - } - private void ApplyFilter(string text) { ToggleAutoSize(false); @@ -243,7 +260,7 @@ private void ApplyFilter(string text) ToggleAutoSize(true); } - public void Save() + public override void Save() { var rows = Grid.Rows; var count = Math.Min(Storage.Count, rows.Count); @@ -254,7 +271,7 @@ public void Save() try { var name = cells[ColumnName].Value?.ToString() ?? string.Empty; - var hash = GetHash(name.Trim()); + var hash = Names.GetHash(name.Trim()); var valObj = cells[ColumnValue].Value; if (typeof(T) == typeof(bool)) @@ -269,10 +286,8 @@ public void Save() } else { - // Should not happen for current use cases. Storage.SetValue(i, (T)valObj!); } - // Set hash last so that the entry only updates if value parsing succeeded. Storage.SetKey(i, hash); } catch @@ -280,24 +295,464 @@ public void Save() // Ignore invalid row } } - Storage.Compress(); // Ensure the user didn't sneak in empty rows. There should be no gaps. - } - - private ulong GetHash(string name) - { - // Generally, the Lookup will be empty. The user can enter an arbitrary flag name string or hash, and we should recognize both. - // If a recognized name is entered, use the known hash. - // If an unrecognized name is entered, check if it's a hex number and use that as the hash. - // Otherwise, just consider it as a name and hash it. - - // O(n) reverse lookup is acceptable for a Saving operation. - var match = Lookup.FirstOrDefault(z => z.Value == name); - if (match.Value != null) // default if not found - return match.Key; - - if (ulong.TryParse(name, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hash)) - return hash; - - return FnvHash.HashFnv1a_64(name); + Storage.Compress(); + } +} + +/// +/// Grid for EventWorkStorage128 with one key columns and two values. +/// +public sealed record EventWorkGridTuple : EventWorkGridBase +{ + private readonly EventWorkValueStorageKey128 Storage; + private readonly EventWorkLookup Names; + + private const int ColumnIndex = 0; + private const int ColumnKey = 1; + private const int ColumnValue1 = 2; + private const int ColumnValue2 = 3; + + private EventWorkGridTuple(EventWorkValueStorageKey128 storage, EventWorkLookup names, SplitContainer container, DataGridView grid, TextBox search) + : base(container, grid, search) + { + Storage = storage; + Names = names; + + Grid.CellValueChanged += ValidateCell; + Search.TextChanged += (_, _) => ApplyFilter(Search.Text); + ToggleAutoSize(true); + } + + public static EventWorkGridTuple CreateValues(TabPage host, EventWorkValueStorageKey128 storage, EventWorkLookup names) + { + var (container, search) = CreateContainerCommon(); + host.Controls.Clear(); + host.Controls.Add(container); + + var font = new Font("Courier New", 8F, FontStyle.Regular, GraphicsUnit.Point, 0); + var dgv = new DoubleBufferedDataGridView + { + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + AllowUserToOrderColumns = true, + AllowUserToResizeRows = false, + AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells, + AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None, + Dock = DockStyle.Fill, + RowHeadersVisible = false, + Margin = new Padding(0), + DefaultCellStyle = new DataGridViewCellStyle { Font = font }, + AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle { BackColor = SystemColors.ControlLight }, + }; + var c1 = MakeIndexColumn(font); + var c2 = MakeKeyColumn(font, "Key A"); + var v1 = MakeValueNumberColumn(font, "Value 1"); + var v2 = MakeValueNumberColumn(font, "Value 2"); + + dgv.Columns.AddRange(c1, c2, v1, v2); + container.Panel2.Controls.Add(dgv); + return new EventWorkGridTuple(storage, names, container, dgv, search); + } + + private void ToggleAutoSize(bool state) + { + var columns = Grid.Columns; + columns[ColumnIndex].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.ColumnHeader : DataGridViewAutoSizeColumnMode.None; + columns[ColumnKey].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + columns[ColumnValue1].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + columns[ColumnValue2].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + } + + private void ValidateCell(object? sender, DataGridViewCellEventArgs e) + { + if (e.ColumnIndex is ColumnValue1 or ColumnValue2) + { + var row = Grid.Rows[e.RowIndex]; + var cell = row.Cells[e.ColumnIndex]; + var text = cell.Value?.ToString() ?? string.Empty; + if (long.TryParse(text, CultureInfo.InvariantCulture, out _)) + return; + WinFormsUtil.Alert("Please enter a valid value."); + var i = Convert.ToInt32(row.Cells[ColumnIndex].Value); + var (a, b) = Storage.GetKey(i); + if (e.ColumnIndex == ColumnKey) + cell.Value = Names.GetName(a); + else if (e.ColumnIndex == ColumnValue1) + cell.Value = ((long)b).ToString(); + else + cell.Value = Storage.GetValue(i); + } + } + + private void ApplyFilter(string text) + { + ToggleAutoSize(false); + bool has = text.Length != 0; + foreach (DataGridViewRow r in Grid.Rows) + { + if (!has) + { + r.Visible = true; + continue; + } + var a = r.Cells[ColumnKey].Value?.ToString() ?? string.Empty; + r.Visible = a.Contains(text, StringComparison.OrdinalIgnoreCase); + } + ToggleAutoSize(true); + } + + public override void Load() + { + var rows = Grid.Rows; + rows.Clear(); + + var count = Storage.Count; + rows.Add(count); + for (int i = 0; i < count; i++) + { + var (a, b) = Storage.GetKey(i); + var value = Storage.GetValue(i); + + var row = rows[i]; + var cells = row.Cells; + cells[ColumnIndex].Value = i; + cells[ColumnKey].Value = Names.GetName(a); + cells[ColumnValue1].Value = ((long)b).ToString(); + cells[ColumnValue2].Value = value.ToString(); + } + } + + public override void Save() + { + var rows = Grid.Rows; + var count = Math.Min(Storage.Count, rows.Count); + + for (int i = 0; i < count; i++) + { + var cells = rows[i].Cells; + try + { + var aText = cells[ColumnKey].Value?.ToString() ?? string.Empty; + var vText1 = cells[ColumnValue1].Value?.ToString() ?? "0"; + var vText2 = cells[ColumnValue2].Value?.ToString() ?? "0"; + ulong a = Names.GetHash(aText); + ulong b = long.TryParse(vText1, CultureInfo.InvariantCulture, out var bt) ? (ulong)bt : 0UL; + ulong v = ulong.TryParse(vText2, CultureInfo.InvariantCulture, out var vt) ? vt : 0UL; + + Storage.SetValue(i, v); + Storage.SetKey(i, a, b); + } + catch + { + // Ignore invalid row + } + } + Storage.Compress(); + } +} + +/// +/// Grid for EventWorkStorage192 with separate key columns A, B, and C, and one value. +/// +public sealed record EventWorkGrid128 : EventWorkGridBase +{ + private readonly EventWorkValueStorageKey128 Storage; + private readonly EventWorkLookup Names; + + private const int ColumnIndex = 0; + private const int ColumnKeyA = 1; + private const int ColumnKeyB = 2; + private const int ColumnValue = 3; + + private EventWorkGrid128(EventWorkValueStorageKey128 storage, EventWorkLookup names, SplitContainer container, DataGridView grid, TextBox search) + : base(container, grid, search) + { + Storage = storage; + Names = names; + + Grid.CellValidated += ValidateValue; + Search.TextChanged += (_, _) => ApplyFilter(Search.Text); + ToggleAutoSize(true); + } + + public static EventWorkGrid128 CreateValues(TabPage host, EventWorkValueStorageKey128 storage, EventWorkLookup names) + { + var (container, search) = CreateContainerCommon(); + host.Controls.Clear(); + host.Controls.Add(container); + + var font = new Font("Courier New", 8F, FontStyle.Regular, GraphicsUnit.Point, 0); + var dgv = new DoubleBufferedDataGridView + { + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + AllowUserToOrderColumns = true, + AllowUserToResizeRows = false, + AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells, + AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None, + Dock = DockStyle.Fill, + RowHeadersVisible = false, + Margin = new Padding(0), + DefaultCellStyle = new DataGridViewCellStyle { Font = font }, + AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle { BackColor = SystemColors.ControlLight }, + }; + + var c1 = MakeIndexColumn(font); + var c2 = MakeKeyColumn(font, "Key A"); + var c3 = MakeKeyColumn(font, "Key B"); + var v1 = MakeValueNumberColumn(font, "Value"); + + dgv.Columns.AddRange(c1, c2, c3, v1); + container.Panel2.Controls.Add(dgv); + return new EventWorkGrid128(storage, names, container, dgv, search); + } + + private void ToggleAutoSize(bool state) + { + var columns = Grid.Columns; + columns[ColumnIndex].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.ColumnHeader : DataGridViewAutoSizeColumnMode.None; + columns[ColumnKeyA].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + columns[ColumnKeyB].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + columns[ColumnValue].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + } + + private void ValidateValue(object? sender, DataGridViewCellEventArgs e) + { + if (e.ColumnIndex is ColumnValue) + { + var row = Grid.Rows[e.RowIndex]; + var cell = row.Cells[e.ColumnIndex]; + var text = cell.Value?.ToString() ?? string.Empty; + if (ulong.TryParse(text, CultureInfo.InvariantCulture, out _)) + return; + WinFormsUtil.Alert("Please enter a valid value."); + var i = Convert.ToInt32(row.Cells[ColumnIndex].Value); + cell.Value = Storage.GetValue(i); + } + } + + private void ApplyFilter(string text) + { + ToggleAutoSize(false); + bool has = text.Length != 0; + foreach (DataGridViewRow r in Grid.Rows) + { + if (!has) + { + r.Visible = true; + continue; + } + var a = r.Cells[ColumnKeyA].Value?.ToString() ?? string.Empty; + var b = r.Cells[ColumnKeyB].Value?.ToString() ?? string.Empty; + r.Visible = a.Contains(text, StringComparison.OrdinalIgnoreCase) || + b.Contains(text, StringComparison.OrdinalIgnoreCase); + } + ToggleAutoSize(true); + } + + public override void Load() + { + var rows = Grid.Rows; + rows.Clear(); + + var count = Storage.Count; + rows.Add(count); + for (int i = 0; i < count; i++) + { + var (a, b) = Storage.GetKey(i); + var value = Storage.GetValue(i); + + var row = rows[i]; + var cells = row.Cells; + cells[ColumnIndex].Value = i; + cells[ColumnKeyA].Value = Names.GetName(a); + cells[ColumnKeyB].Value = Names.GetName(b); + cells[ColumnValue].Value = value.ToString(); + } + } + + public override void Save() + { + var rows = Grid.Rows; + var count = Math.Min(Storage.Count, rows.Count); + + for (int i = 0; i < count; i++) + { + var cells = rows[i].Cells; + try + { + var aText = cells[ColumnKeyA].Value?.ToString() ?? string.Empty; + var bText = cells[ColumnKeyB].Value?.ToString() ?? string.Empty; + var vText = cells[ColumnValue].Value?.ToString() ?? "0"; + ulong a = Names.GetHash(aText); + ulong b = Names.GetHash(bText); + ulong v = ulong.TryParse(vText, CultureInfo.InvariantCulture, out var vt) ? vt : 0UL; + + Storage.SetValue(i, v); + Storage.SetKey(i, a, b); + } + catch + { + // Ignore invalid row + } + } + Storage.Compress(); + } +} + +/// +/// Grid for EventWorkStorage192 with separate key columns A, B, and C, and one value. +/// +public sealed record EventWorkGrid192 : EventWorkGridBase +{ + private readonly EventWorkValueStorageKey192 Storage; + private readonly EventWorkLookup Names; + + private const int ColumnIndex = 0; + private const int ColumnKeyA = 1; + private const int ColumnKeyB = 2; + private const int ColumnKeyC = 3; + private const int ColumnValue = 4; + + private EventWorkGrid192(EventWorkValueStorageKey192 storage, EventWorkLookup names, SplitContainer container, DataGridView grid, TextBox search) + : base(container, grid, search) + { + Storage = storage; + Names = names; + + Grid.CellValidated += ValidateValue; + Search.TextChanged += (_, _) => ApplyFilter(Search.Text); + ToggleAutoSize(true); + } + + public static EventWorkGrid192 CreateValues(TabPage host, EventWorkValueStorageKey192 storage, EventWorkLookup names) + { + var (container, search) = CreateContainerCommon(); + host.Controls.Clear(); + host.Controls.Add(container); + + var font = new Font("Courier New", 8F, FontStyle.Regular, GraphicsUnit.Point, 0); + var dgv = new DoubleBufferedDataGridView + { + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + AllowUserToOrderColumns = true, + AllowUserToResizeRows = false, + AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells, + AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None, + Dock = DockStyle.Fill, + RowHeadersVisible = false, + Margin = new Padding(0), + DefaultCellStyle = new DataGridViewCellStyle { Font = font }, + AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle { BackColor = SystemColors.ControlLight }, + }; + + var c1 = MakeIndexColumn(font); + var c2 = MakeKeyColumn(font, "Key A"); + var c3 = MakeKeyColumn(font, "Key B"); + var c4 = MakeKeyColumn(font, "Key C"); + var v1 = MakeValueNumberColumn(font, "Value"); + + dgv.Columns.AddRange(c1, c2, c3, c4, v1); + container.Panel2.Controls.Add(dgv); + return new EventWorkGrid192(storage, names, container, dgv, search); + } + + private void ToggleAutoSize(bool state) + { + var columns = Grid.Columns; + columns[ColumnIndex].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.ColumnHeader : DataGridViewAutoSizeColumnMode.None; + columns[ColumnKeyA].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + columns[ColumnKeyB].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + columns[ColumnKeyC].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + columns[ColumnValue].AutoSizeMode = state ? DataGridViewAutoSizeColumnMode.AllCells : DataGridViewAutoSizeColumnMode.None; + } + + private void ValidateValue(object? sender, DataGridViewCellEventArgs e) + { + if (e.ColumnIndex is ColumnValue) + { + var row = Grid.Rows[e.RowIndex]; + var cell = row.Cells[e.ColumnIndex]; + var text = cell.Value?.ToString() ?? string.Empty; + if (ulong.TryParse(text, CultureInfo.InvariantCulture, out _)) + return; + WinFormsUtil.Alert("Please enter a valid value."); + var i = Convert.ToInt32(row.Cells[ColumnIndex].Value); + cell.Value = Storage.GetValue(i); + } + } + + private void ApplyFilter(string text) + { + ToggleAutoSize(false); + bool has = text.Length != 0; + foreach (DataGridViewRow r in Grid.Rows) + { + if (!has) + { + r.Visible = true; + continue; + } + var a = r.Cells[ColumnKeyA].Value?.ToString() ?? string.Empty; + var b = r.Cells[ColumnKeyB].Value?.ToString() ?? string.Empty; + var c = r.Cells[ColumnKeyC].Value?.ToString() ?? string.Empty; + r.Visible = a.Contains(text, StringComparison.OrdinalIgnoreCase) || + b.Contains(text, StringComparison.OrdinalIgnoreCase) || + c.Contains(text, StringComparison.OrdinalIgnoreCase); + } + ToggleAutoSize(true); + } + + public override void Load() + { + var rows = Grid.Rows; + rows.Clear(); + + var count = Storage.Count; + rows.Add(count); + for (int i = 0; i < count; i++) + { + var (a, b, c) = Storage.GetKey(i); + var value = Storage.GetValue(i); + + var row = rows[i]; + var cells = row.Cells; + cells[ColumnIndex].Value = i; + cells[ColumnKeyA].Value = Names.GetName(a); + cells[ColumnKeyB].Value = Names.GetName(b); + cells[ColumnKeyC].Value = Names.GetName(c); + cells[ColumnValue].Value = value.ToString(); + } + } + + public override void Save() + { + var rows = Grid.Rows; + var count = Math.Min(Storage.Count, rows.Count); + + for (int i = 0; i < count; i++) + { + var cells = rows[i].Cells; + try + { + var aText = cells[ColumnKeyA].Value?.ToString() ?? string.Empty; + var bText = cells[ColumnKeyB].Value?.ToString() ?? string.Empty; + var cText = cells[ColumnKeyC].Value?.ToString() ?? string.Empty; + var vText = cells[ColumnValue].Value?.ToString() ?? "0"; + ulong a = Names.GetHash(aText); + ulong b = Names.GetHash(bText); + ulong c = Names.GetHash(cText); + ulong v = ulong.TryParse(vText, CultureInfo.InvariantCulture, out var vt) ? vt : 0UL; + + Storage.SetValue(i, v); + Storage.SetKey(i, a, b, c); + } + catch + { + // Ignore invalid row + } + } + Storage.Compress(); } } diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_FlagWork9a.Designer.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_FlagWork9a.Designer.cs index 1d767a9d2..2623dafd3 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_FlagWork9a.Designer.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_FlagWork9a.Designer.cs @@ -50,7 +50,7 @@ private void InitializeComponent() // B_Cancel // B_Cancel.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; - B_Cancel.Location = new System.Drawing.Point(427, 401); + B_Cancel.Location = new System.Drawing.Point(507, 401); B_Cancel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); B_Cancel.Name = "B_Cancel"; B_Cancel.Size = new System.Drawing.Size(88, 27); @@ -62,7 +62,7 @@ private void InitializeComponent() // B_Save // B_Save.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; - B_Save.Location = new System.Drawing.Point(523, 401); + B_Save.Location = new System.Drawing.Point(603, 401); B_Save.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); B_Save.Name = "B_Save"; B_Save.Size = new System.Drawing.Size(88, 27); @@ -90,7 +90,7 @@ private void InitializeComponent() GB_Research.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); GB_Research.Name = "GB_Research"; GB_Research.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3); - GB_Research.Size = new System.Drawing.Size(598, 351); + GB_Research.Size = new System.Drawing.Size(678, 351); GB_Research.TabIndex = 2; GB_Research.Text = "Research"; GB_Research.UseVisualStyleBackColor = true; @@ -112,7 +112,7 @@ private void InitializeComponent() // splitContainer1.Panel2 // splitContainer1.Panel2.Controls.Add(RTB_Diff); - splitContainer1.Size = new System.Drawing.Size(590, 345); + splitContainer1.Size = new System.Drawing.Size(670, 345); splitContainer1.SplitterDistance = 60; splitContainer1.TabIndex = 7; // @@ -134,7 +134,7 @@ private void InitializeComponent() TB_NewSAV.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); TB_NewSAV.Name = "TB_NewSAV"; TB_NewSAV.ReadOnly = true; - TB_NewSAV.Size = new System.Drawing.Size(487, 25); + TB_NewSAV.Size = new System.Drawing.Size(567, 25); TB_NewSAV.TabIndex = 5; // // B_LoadNew @@ -155,7 +155,7 @@ private void InitializeComponent() TB_OldSAV.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); TB_OldSAV.Name = "TB_OldSAV"; TB_OldSAV.ReadOnly = true; - TB_OldSAV.Size = new System.Drawing.Size(487, 25); + TB_OldSAV.Size = new System.Drawing.Size(567, 25); TB_OldSAV.TabIndex = 4; // // RTB_Diff @@ -165,7 +165,7 @@ private void InitializeComponent() RTB_Diff.Margin = new System.Windows.Forms.Padding(0); RTB_Diff.Name = "RTB_Diff"; RTB_Diff.ReadOnly = true; - RTB_Diff.Size = new System.Drawing.Size(590, 281); + RTB_Diff.Size = new System.Drawing.Size(670, 281); RTB_Diff.TabIndex = 6; RTB_Diff.Text = ""; // @@ -179,14 +179,14 @@ private void InitializeComponent() TC_Features.Name = "TC_Features"; TC_Features.Padding = new System.Drawing.Point(0, 0); TC_Features.SelectedIndex = 0; - TC_Features.Size = new System.Drawing.Size(606, 381); + TC_Features.Size = new System.Drawing.Size(686, 381); TC_Features.TabIndex = 42; // // SAV_FlagWork9a // AllowDrop = true; AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit; - ClientSize = new System.Drawing.Size(624, 441); + ClientSize = new System.Drawing.Size(704, 441); Controls.Add(TC_Features); Controls.Add(L_EventFlagWarn); Controls.Add(B_Save); @@ -194,7 +194,6 @@ private void InitializeComponent() Icon = Properties.Resources.Icon; Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); MaximizeBox = false; - MaximumSize = new System.Drawing.Size(779, 917); MinimizeBox = false; MinimumSize = new System.Drawing.Size(640, 480); Name = "SAV_FlagWork9a"; diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_FlagWork9a.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_FlagWork9a.cs index c874f0b31..05285d21c 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_FlagWork9a.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_FlagWork9a.cs @@ -9,7 +9,8 @@ namespace PKHeX.WinForms; public sealed partial class SAV_FlagWork9a : Form { - private readonly Dictionary Lookup = []; + private readonly Dictionary Lookup = new() { {FnvHash.HashEmpty, ""} }; + private readonly EventWorkLookup Names; private readonly IEventWorkGrid[] Grids; @@ -21,18 +22,28 @@ public SAV_FlagWork9a(SAV9ZA sav) if (File.Exists(path)) SCBlockMetadata.AddExtraKeyNames64(Lookup, File.ReadLines(path)); + Names = new EventWorkLookup(Lookup); + // Create grids for each block Grids = [ - EventWorkGrid64.CreateFlags(GetTab(nameof(sav.Blocks.Flags)), sav.Blocks.Flags, Lookup), - EventWorkGrid64.CreateFlags(GetTab(nameof(sav.Blocks.Event)), sav.Blocks.Event, Lookup), - EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.Work)), sav.Blocks.Work, Lookup), - EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.Quest)), sav.Blocks.Quest, Lookup), - EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.WorkMable)), sav.Blocks.WorkMable, Lookup), - EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.CountMable)), sav.Blocks.CountMable, Lookup), + EventWorkGrid64.CreateFlags(GetTab(nameof(sav.Blocks.Flags)), sav.Blocks.Flags, Names), + EventWorkGrid64.CreateFlags(GetTab(nameof(sav.Blocks.Event)), sav.Blocks.Event, Names), + EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.Work)), sav.Blocks.Work, Names), + EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.Quest)), sav.Blocks.Quest, Names), + EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.WorkMable)), sav.Blocks.WorkMable, Names), + EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.CountMable)), sav.Blocks.CountMable, Names), - EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.CountTitle)), sav.Blocks.CountTitle, Lookup), - EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.WorkSpawn)), sav.Blocks.WorkSpawn, Lookup), + EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.CountTitle)), sav.Blocks.CountTitle, Names), + EventWorkGridTuple.CreateValues(GetTab(nameof(sav.Blocks.Report)), sav.Blocks.Report, Names), + + EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.WorkSpawn)), sav.Blocks.WorkSpawn, Names), + EventWorkGrid64.CreateValues(GetTab(nameof(sav.Blocks.InfiniteRank)), sav.Blocks.InfiniteRank, Names), + EventWorkGridTuple.CreateValues(GetTab(nameof(sav.Blocks.Spawner2)), sav.Blocks.Spawner2, Names), + EventWorkGrid128.CreateValues(GetTab(nameof(sav.Blocks.Spawner4)), sav.Blocks.Spawner4, Names), + + EventWorkGridTuple.CreateValues(GetTab(nameof(sav.Blocks.Obstruction)), sav.Blocks.Obstruction, Names), + EventWorkGrid192.CreateValues(GetTab(nameof(sav.Blocks.FieldObjectInteractable)), sav.Blocks.FieldObjectInteractable, Names), ]; // Translate headings @@ -142,7 +153,7 @@ private void DiffSaves(string fileUpdated, string filePrevious) break; var u = update.GetValue(i); - var name = EventWorkGrid64.GetNameDisplay(hash, Lookup); + var name = Names.GetName(hash); if (!previous.TryGetValue(hash, out var p)) result.Add($"{name} added with value {u}"); else if (!p.Equals(u)) @@ -161,7 +172,7 @@ private void DiffSaves(string fileUpdated, string filePrevious) if (hashes.Contains(hash)) continue; var p = previous.GetValue(i); - var name = EventWorkGrid64.GetNameDisplay(hash, Lookup); + var name = Names.GetName(hash); result.Add($"{name} @ {i:X} removed, was {p}"); } return result;