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 @@
-
+