From 8e0ad84fda68948ab3ec70edeea34ae2d572d808 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Mon, 25 Mar 2019 20:56:58 -0500 Subject: [PATCH] first draft of the table tool --- src/HexManiac.Core/HexManiac.Core.csproj | 2 + src/HexManiac.Core/Models/IDataModel.cs | 18 ++ src/HexManiac.Core/Models/PokemonModel.cs | 2 +- src/HexManiac.Core/Models/Runs/ArrayRun.cs | 30 +++ .../Tools/IArrayElementViewModel.cs | 202 ++++++++++++++++++ .../ViewModels/Tools/IToolTrayViewModel.cs | 12 +- .../ViewModels/Tools/TableTool.cs | 94 ++++++++ src/HexManiac.Core/ViewModels/ViewPort.cs | 1 + src/HexManiac.WPF/Windows/MainWindow.xaml | 43 +++- 9 files changed, 390 insertions(+), 14 deletions(-) create mode 100644 src/HexManiac.Core/ViewModels/Tools/IArrayElementViewModel.cs create mode 100644 src/HexManiac.Core/ViewModels/Tools/TableTool.cs diff --git a/src/HexManiac.Core/HexManiac.Core.csproj b/src/HexManiac.Core/HexManiac.Core.csproj index db489d2d..924b1018 100644 --- a/src/HexManiac.Core/HexManiac.Core.csproj +++ b/src/HexManiac.Core/HexManiac.Core.csproj @@ -48,6 +48,7 @@ + @@ -76,6 +77,7 @@ + diff --git a/src/HexManiac.Core/Models/IDataModel.cs b/src/HexManiac.Core/Models/IDataModel.cs index 3b018d63..724d3cd2 100644 --- a/src/HexManiac.Core/Models/IDataModel.cs +++ b/src/HexManiac.Core/Models/IDataModel.cs @@ -133,6 +133,24 @@ namespace HavenSoft.HexManiac.Core.Models { IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + public static class IDataModelExtensions { + public static int ReadMultiByteValue(this IDataModel model, int index, int length) { + int word = 0; + while (length > 0) { + word <<= 8; + word += model.RawData[index + length - 1]; + length--; + } + return word; + } + public static void WriteMultiByteValue(this IDataModel model, int index, int length, ModelDelta changeToken, int value) { + for (int i = 0; i < length; i++) { + changeToken.ChangeData(model, index + i, (byte)value); + value >>= 8; + } + } + } + public class BasicModel : BaseModel { public BasicModel(byte[] data) : base(data) { } diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index 4f55cccf..dca25456 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -234,7 +234,7 @@ namespace HavenSoft.HexManiac.Core.Models { // option 4: pointing within an array that supports inner element anchors var containingRun = GetNextRun(address); - if (containingRun.Start < address && containingRun is ArrayRun array && array.SupportsPointersToElements) { + if (containingRun.Start < address && containingRun is ArrayRun array) { var arrayName = GetAnchorFromAddress(-1, array.Start); var arrayIndex = (address - array.Start) / array.ElementLength; var indexMod = (address - array.Start) % array.ElementLength; diff --git a/src/HexManiac.Core/Models/Runs/ArrayRun.cs b/src/HexManiac.Core/Models/Runs/ArrayRun.cs index 2839dd15..ac92c5be 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRun.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRun.cs @@ -43,7 +43,9 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { public class ArrayRunEnumSegment : ArrayRunElementSegment { public string EnumName { get; } + public ArrayRunEnumSegment(string name, int length, string enumName) : base(name, ElementContentType.Integer, length) => EnumName = enumName; + public override string ToText(IDataModel model, int offset) { var noChange = new NoDataChangeDeltaModel(); @@ -129,6 +131,34 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { // we went through the whole array and didn't find it :( return false; } + + public IReadOnlyList GetOptions(IDataModel model) { + var noChange = new NoDataChangeDeltaModel(); + + // enum must be the name of an array that starts with a string + var address = model.GetAddressFromAnchor(noChange, -1, EnumName); + if (address == Pointer.NULL) return null; + var enumArray = model.GetNextRun(address) as ArrayRun; + if (enumArray == null) return null; + if (enumArray.ElementContent.Count == 0) return null; + var firstContent = enumArray.ElementContent[0]; + if (firstContent.Type != ElementContentType.PCS) return null; + + // array must be at least as long as than the current value + var optionCount = enumArray.ElementCount; + + // sweet, we can convert from the integer value to the enum value + var results = new List(); + for (int i = 0; i < optionCount; i++) { + var elementStart = enumArray.Start + enumArray.ElementLength * i; + var valueWithQuotes = PCSString.Convert(model, elementStart, firstContent.Length).Trim(); + var value = valueWithQuotes.Substring(1, valueWithQuotes.Length - 2); + if (value.Contains(' ')) value = $"\"{value}\""; + results.Add(value); + } + + return results; + } } public class ArrayOffset { diff --git a/src/HexManiac.Core/ViewModels/Tools/IArrayElementViewModel.cs b/src/HexManiac.Core/ViewModels/Tools/IArrayElementViewModel.cs new file mode 100644 index 00000000..09eda837 --- /dev/null +++ b/src/HexManiac.Core/ViewModels/Tools/IArrayElementViewModel.cs @@ -0,0 +1,202 @@ +using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.Models.Runs; +using HavenSoft.HexManiac.Core.ViewModels.DataFormats; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; + +namespace HavenSoft.HexManiac.Core.ViewModels.Tools { + public enum ElementContentViewModelType { + Title, + TextField, + NumericField, + Address, + HexField, + ComboBox, + } + + public interface IArrayElementViewModel : INotifyPropertyChanged { + ElementContentViewModelType Type { get; } + bool IsInError { get; } + string ErrorText { get; } + } + + public interface IFieldArrayElementViewModelStrategy { + ElementContentViewModelType Type { get; } + void UpdateModelFromViewModel(FieldArrayElementViewModel viewModel); + string UpdateViewModelFromModel(FieldArrayElementViewModel viewModel); + } + + public class TitleArrayElementViewModel : ViewModelCore, IArrayElementViewModel { + public ElementContentViewModelType Type => ElementContentViewModelType.Title; + + public bool IsInError => false; + + public string ErrorText => string.Empty; + + public string Content { get; } + + public TitleArrayElementViewModel(string content) => Content = content; + } + + public class FieldArrayElementViewModel : ViewModelCore, IArrayElementViewModel { + private readonly IFieldArrayElementViewModelStrategy strategy; + + public ChangeHistory History { get; } + public IDataModel Model { get; } + public string Name { get; } + public int Start { get; } + public int Length { get; } + + public ElementContentViewModelType Type => strategy.Type; + + public bool IsInError => errorText != string.Empty; + + string errorText; + public string ErrorText { + get => errorText; + set { + if (TryUpdate(ref errorText, value)) NotifyPropertyChanged(nameof(IsInError)); + } + } + + string content; + public string Content { + get => content; + set { + if (TryUpdate(ref content, value)) { + strategy.UpdateModelFromViewModel(this); + } + } + } + + public FieldArrayElementViewModel(ChangeHistory history, IDataModel model, string name, int start, int length, IFieldArrayElementViewModelStrategy strategy) { + this.strategy = strategy; + (History, Model, Name, Start, Length) = (history, model, name, start, length); + content = strategy.UpdateViewModelFromModel(this); + } + } + + public class TextFieldStratgy : IFieldArrayElementViewModelStrategy { + public ElementContentViewModelType Type => ElementContentViewModelType.TextField; + + public void UpdateModelFromViewModel(FieldArrayElementViewModel viewModel) { + var textBytes = PCSString.Convert(viewModel.Content); + while (textBytes.Count < viewModel.Length) textBytes.Add(0x00); + if (textBytes.Count > viewModel.Length) textBytes[viewModel.Length - 1] = 0xFF; + for (int i = 0; i < viewModel.Length; i++) { + viewModel.History.CurrentChange.ChangeData(viewModel.Model, viewModel.Start + i, textBytes[i]); + } + } + + public string UpdateViewModelFromModel(FieldArrayElementViewModel viewModel) { + var text = PCSString.Convert(viewModel.Model, viewModel.Start, viewModel.Length).Trim(); + text = text.Substring(1, text.Length - 2); // take off quotes + return text; + } + } + + public class NumericFieldStrategy : IFieldArrayElementViewModelStrategy { + public ElementContentViewModelType Type => ElementContentViewModelType.NumericField; + + public void UpdateModelFromViewModel(FieldArrayElementViewModel viewModel) { + if (int.TryParse(viewModel.Content, out int content)) { + viewModel.Model.WriteMultiByteValue(viewModel.Start, viewModel.Length, viewModel.History.CurrentChange, content); + } else { + viewModel.ErrorText = $"{viewModel.Name} must be an integer."; + } + } + + public string UpdateViewModelFromModel(FieldArrayElementViewModel viewModel) { + int number = viewModel.Model.ReadMultiByteValue(viewModel.Start, viewModel.Length); + return number.ToString(); + } + } + + public class AddressFieldStratgy : IFieldArrayElementViewModelStrategy { + public ElementContentViewModelType Type => ElementContentViewModelType.Address; + + public void UpdateModelFromViewModel(FieldArrayElementViewModel viewModel) { + int address; + if (!int.TryParse(viewModel.Content, NumberStyles.HexNumber, CultureInfo.CurrentCulture.NumberFormat, out address)) { + address = viewModel.Model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, viewModel.Content); + } + + if (address != Pointer.NULL) { + viewModel.Model.WritePointer(viewModel.History.CurrentChange, address, viewModel.Start); + } else { + viewModel.ErrorText = "Address should be hexidecimal or an anchor."; + } + } + + public string UpdateViewModelFromModel(FieldArrayElementViewModel viewModel) { + var value = viewModel.Model.ReadPointer(viewModel.Start); + var text = value.ToString("X2"); + while (text.Length < 6) text = "0" + text; + return text; + } + } + + public class HexFieldStratgy : IFieldArrayElementViewModelStrategy { + public ElementContentViewModelType Type => ElementContentViewModelType.HexField; + + public void UpdateModelFromViewModel(FieldArrayElementViewModel viewModel) { + if (int.TryParse(viewModel.Content, NumberStyles.HexNumber, CultureInfo.CurrentCulture.NumberFormat, out int hexValue)) { + viewModel.Model.WriteMultiByteValue(viewModel.Start, viewModel.Length, viewModel.History.CurrentChange, hexValue); + } else { + viewModel.ErrorText = "Value should be hexidecimal."; + } + } + + public string UpdateViewModelFromModel(FieldArrayElementViewModel viewModel) { + int number = viewModel.Model.ReadMultiByteValue(viewModel.Start, viewModel.Length); + var text = number.ToString("X2"); + return text; + } + } + + public class ComboBoxArrayElementViewModel : ViewModelCore, IArrayElementViewModel { + private readonly ChangeHistory history; + + public IDataModel Model { get; } + public string Name { get; } + public int Start { get; } + public int Length { get; } + + public ElementContentViewModelType Type => ElementContentViewModelType.ComboBox; + + public bool IsInError => errorText != string.Empty; + + string errorText; + public string ErrorText { + get => errorText; + set { + if (TryUpdate(ref errorText, value)) NotifyPropertyChanged(nameof(IsInError)); + } + } + + public ObservableCollection Options { get; } + + private int selectedIndex; + + public int SelectedIndex { + get => selectedIndex; + set { + if (!TryUpdate(ref selectedIndex, value)) return; + var run = (ArrayRun)Model.GetNextRun(Start); + var offsets = run.ConvertByteOffsetToArrayOffset(Start); + var segment = (ArrayRunEnumSegment)run.ElementContent[offsets.SegmentIndex]; + Model.WriteMultiByteValue(Start, Length, history.CurrentChange, value); + } + } + + public ComboBoxArrayElementViewModel(ChangeHistory history, IDataModel model, string name, int start, int length) { + (this.history, Model, Name, Start, Length) = (history, model, name, start, length); + var run = (ArrayRun)Model.GetNextRun(Start); + var offsets = run.ConvertByteOffsetToArrayOffset(start); + var segment = (ArrayRunEnumSegment)run.ElementContent[offsets.SegmentIndex]; + Options = new ObservableCollection(segment.GetOptions(model)); + selectedIndex = model.ReadMultiByteValue(start, length); + } + } +} diff --git a/src/HexManiac.Core/ViewModels/Tools/IToolTrayViewModel.cs b/src/HexManiac.Core/ViewModels/Tools/IToolTrayViewModel.cs index 7a8496e5..12f47fba 100644 --- a/src/HexManiac.Core/ViewModels/Tools/IToolTrayViewModel.cs +++ b/src/HexManiac.Core/ViewModels/Tools/IToolTrayViewModel.cs @@ -12,6 +12,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { PCSTool StringTool { get; } + TableTool TableTool { get; } + IDisposable DeferUpdates { get; } void Schedule(Action action); @@ -20,7 +22,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { public class ToolTray : ViewModelCore, IToolTrayViewModel { private readonly IList tools; private readonly StubCommand hideCommand; - private readonly StubCommand stringToolCommand, tool2Command, tool3Command; + private readonly StubCommand stringToolCommand, tableToolCommand, tool3Command; private readonly HashSet deferredWork = new HashSet(); private int selectedIndex; @@ -38,12 +40,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { public ICommand HideCommand => hideCommand; public ICommand StringToolCommand => stringToolCommand; - public ICommand Tool2Command => tool2Command; + public ICommand TableToolCommand => tableToolCommand; public ICommand Tool3Command => tool3Command; public PCSTool StringTool => (PCSTool)tools[0]; - public IToolViewModel Tool2 => tools[1]; + public TableTool TableTool => (TableTool)tools[1]; public IToolViewModel Tool3 => tools[2]; @@ -65,7 +67,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { public ToolTray(IDataModel model, Selection selection, ChangeHistory history) { tools = new IToolViewModel[] { new PCSTool(model, selection, history, this), - new FillerTool("Tool2"), + new TableTool(model, history, this), new FillerTool("Tool3"), }; @@ -74,7 +76,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { Execute = arg => SelectedIndex = selectedIndex == 0 ? -1 : 0, }; - tool2Command = new StubCommand { + tableToolCommand = new StubCommand { CanExecute = ICommandExtensions.CanAlwaysExecute, Execute = arg => SelectedIndex = selectedIndex == 1 ? -1 : 1, }; diff --git a/src/HexManiac.Core/ViewModels/Tools/TableTool.cs b/src/HexManiac.Core/ViewModels/Tools/TableTool.cs new file mode 100644 index 00000000..4e6bf014 --- /dev/null +++ b/src/HexManiac.Core/ViewModels/Tools/TableTool.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.ObjectModel; +using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.Models.Runs; +using HavenSoft.HexManiac.Core.ViewModels.DataFormats; + +namespace HavenSoft.HexManiac.Core.ViewModels.Tools { + public class TableTool : ViewModelCore, IToolViewModel { + private readonly IDataModel model; + private readonly Selection selection; + private readonly ChangeHistory history; + private readonly IToolTrayViewModel toolTray; + public string Name => "Table"; + + public ObservableCollection Children { get; } + + // the address is the address not of the entire array, but of the current index of the array + private int address = Pointer.NULL; + public int Address { + get => address; + set { + var run = model.GetNextRun(value); + if (run.Start > value) { + Enabled = false; + return; + } + if (!(run is ArrayRun array)) { + Enabled = false; + return; + } + + var offsets = array.ConvertByteOffsetToArrayOffset(value); + value = array.Start + array.ElementLength * offsets.ElementIndex; + if (TryUpdate(ref address, value)) { + toolTray.Schedule(DataForCurrentRunChanged); + Enabled = true; + } + } + } + + private bool enabled; + public bool Enabled { + get => enabled; + private set => TryUpdate(ref enabled, value); + } + + public event EventHandler ModelDataChanged; + + public event EventHandler<(int originalLocation, int newLocation)> ModelDataMoved; // invoke when a new item gets added and the table has to move + + public TableTool(IDataModel model, ChangeHistory history, IToolTrayViewModel toolTray) { + this.model = model; + this.history = history; + this.toolTray = toolTray; + Children = new ObservableCollection(); + } + + private void DataForCurrentRunChanged() { + Children.Clear(); + + var array = model.GetNextRun(Address) as ArrayRun; + if (array == null) { + Children.Add(new TitleArrayElementViewModel("The Table tool only works if your cursor is on table data.")); + return; + } + + Children.Add(new TitleArrayElementViewModel(model.GetAnchorFromAddress(-1, Address))); // example: pokestat/Charmander + + int itemAddress = Address; + foreach (var item in array.ElementContent) { + if (item.Type == ElementContentType.Unknown) Children.Add(new FieldArrayElementViewModel(history, model, item.Name, itemAddress, item.Length, new HexFieldStratgy())); + else if (item.Type == ElementContentType.PCS) Children.Add(new FieldArrayElementViewModel(history, model, item.Name, itemAddress, item.Length, new TextFieldStratgy())); + else if (item.Type == ElementContentType.Pointer) Children.Add(new FieldArrayElementViewModel(history, model, item.Name, itemAddress, item.Length, new AddressFieldStratgy())); + else if (item.Type == ElementContentType.Integer) { + if (item is ArrayRunEnumSegment enumSegment) { + Children.Add(new ComboBoxArrayElementViewModel(history, model, item.Name, itemAddress, item.Length)); + } else { + Children.Add(new FieldArrayElementViewModel(history, model, item.Name, itemAddress, item.Length, new NumericFieldStrategy())); + } + } else { + throw new NotImplementedException(); + } + itemAddress += item.Length; + } + } + + private void UpdateRun(ArrayRun array) { + // TODO update the model based on changes in the configuration. + // generally, the formatted won't change. + // Instead, the content will change. + ModelDataChanged?.Invoke(this, null); + } + } +} diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index eb9092e9..0a88745e 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -136,6 +136,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (run.Start <= dataIndex && run is ArrayRun array) { var offsets = array.ConvertByteOffsetToArrayOffset(dataIndex); Tools.StringTool.Address = offsets.SegmentStart - offsets.ElementIndex * array.ElementLength; + Tools.TableTool.Address = array.Start + array.ElementLength * offsets.ElementIndex; } if (this[SelectionStart].Format is Anchor anchor) { diff --git a/src/HexManiac.WPF/Windows/MainWindow.xaml b/src/HexManiac.WPF/Windows/MainWindow.xaml index a7241cb4..84869bcb 100644 --- a/src/HexManiac.WPF/Windows/MainWindow.xaml +++ b/src/HexManiac.WPF/Windows/MainWindow.xaml @@ -6,6 +6,7 @@ xmlns:hsv="clr-namespace:HavenSoft.HexManiac.WPF.Resources" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:solarized="clr-namespace:Solarized" + xmlns:hsg3hvmtr="clr-namespace:HavenSoft.HexManiac.Core.ViewModels.Tools;assembly=HexManiac.Core" Icon="..\Resources\AppIcon.ico" PreviewMouseDown="RunDeferredActions" Title="Hex Maniac Advance" Height="450" Width="870" AllowDrop="True" Background="{DynamicResource Background}"> @@ -143,7 +144,7 @@ - + @@ -305,7 +306,7 @@ -