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;