Merge pull request #29 from haven1433/dev

Egg Move Support
This commit is contained in:
haven1433
2019-05-22 22:09:38 -05:00
committed by GitHub
23 changed files with 876 additions and 145 deletions

View File

@@ -52,6 +52,7 @@
<Compile Include="Models\IDataModel.cs" />
<Compile Include="Models\Runs\ArrayRunElementSegment.cs" />
<Compile Include="Models\Runs\AsciiRun.cs" />
<Compile Include="Models\Runs\EggMoveRun.cs" />
<Compile Include="Models\Runs\HeaderRow.cs" />
<Compile Include="ViewModels\AutoCompleteSelectionItem.cs" />
<Compile Include="ViewModels\Visitors\CompleteEditOperation.cs" />

View File

@@ -38,6 +38,7 @@ namespace HavenSoft.HexManiac.Core.Models {
DecodeHeader();
DecodeNameArrays();
DecodeDataArrays();
DecodeStreams();
}
}
@@ -54,12 +55,12 @@ namespace HavenSoft.HexManiac.Core.Models {
private void DecodeNameArrays() {
// movenames
if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var movenames)) {
ObserveAnchorWritten(noChangeDelta, "movenames", movenames);
ObserveAnchorWritten(noChangeDelta, EggMoveRun.MoveNamesTable, movenames);
}
// pokenames
if (TrySearch(this, noChangeDelta, "[name\"\"11]", out var pokenames)) {
ObserveAnchorWritten(noChangeDelta, "pokenames", pokenames);
ObserveAnchorWritten(noChangeDelta, EggMoveRun.PokemonNameTable, pokenames);
}
// abilitynames / trainer names
@@ -116,5 +117,11 @@ namespace HavenSoft.HexManiac.Core.Models {
// @3D4294 ^itemicons[image<> palette<>]items
// @4886E8 ^movedescriptions[description<>]354
}
private void DecodeStreams() {
if (EggMoveRun.TrySearch(this, noChangeDelta, out var eggmoves)) {
ObserveAnchorWritten(noChangeDelta, "eggmoves", eggmoves);
}
}
}
}

View File

@@ -11,6 +11,7 @@ namespace HavenSoft.HexManiac.Core.Models {
byte[] RawData { get; }
new byte this[int index] { get; set; }
IReadOnlyList<ArrayRun> Arrays { get; }
IReadOnlyList<IFormattedRun> Streams { get; }
/// <summary>
/// If dataIndex is in the middle of a run, returns that run.
@@ -64,6 +65,7 @@ namespace HavenSoft.HexManiac.Core.Models {
public BaseModel(byte[] data) => RawData = data;
public virtual IReadOnlyList<ArrayRun> Arrays { get; } = new List<ArrayRun>();
public virtual IReadOnlyList<IFormattedRun> Streams { get; } = new List<IFormattedRun>();
public byte this[int index] { get => RawData[index]; set => RawData[index] = value; }

View File

@@ -29,6 +29,7 @@ namespace HavenSoft.HexManiac.Core.Models {
public virtual int EarliestAllowedAnchor => 0;
public override IReadOnlyList<ArrayRun> Arrays => runs.OfType<ArrayRun>().ToList();
public override IReadOnlyList<IFormattedRun> Streams => runs.OfType<EggMoveRun>().ToList();
#region Constructor
@@ -738,7 +739,7 @@ namespace HavenSoft.HexManiac.Core.Models {
public override string Copy(Func<ModelDelta> changeToken, int start, int length) {
var text = new StringBuilder();
var run = GetNextRun(start);
if (run.Start < start && !(run is ArrayRun)) {
if (run.Start < start && !(run is ArrayRun) && !(run is EggMoveRun)) {
length += start - run.Start;
start = run.Start;
}
@@ -784,6 +785,11 @@ namespace HavenSoft.HexManiac.Core.Models {
text.Append(" ");
length -= run.Start + run.Length - start;
start = run.Start + run.Length;
} else if (run is EggMoveRun eggRun) {
eggRun.AppendTo(text, start, length);
text.Append(" ");
length -= run.Start + run.Length - start;
start = run.Start + run.Length;
} else {
throw new NotImplementedException();
}
@@ -979,6 +985,8 @@ namespace HavenSoft.HexManiac.Core.Models {
} else {
return new ErrorInfo($"Ascii runs must include a length.");
}
} else if (format.StartsWith(StreamDelimeter + "egg" + StreamDelimeter)) {
run = new EggMoveRun(model, dataIndex);
} else {
var errorInfo = TryParse(model, format, dataIndex, null, out var arrayRun);
if (errorInfo == ErrorInfo.NoError) {
@@ -1083,6 +1091,8 @@ namespace HavenSoft.HexManiac.Core.Models {
newRun = new PCSRun(newStart, run.Length, run.PointerSources);
} else if (run is ArrayRun array) {
newRun = array.Move(newStart);
} else if (run is EggMoveRun egg) {
newRun = new EggMoveRun(this, newStart);
} else {
throw new NotImplementedException();
}

View File

@@ -116,8 +116,12 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
private IReadOnlyList<string> cachedOptions;
public IReadOnlyList<string> GetOptions(IDataModel model) {
if (cachedOptions != null) return cachedOptions;
cachedOptions = GetOptions(model, EnumName);
return cachedOptions;
}
if (!model.TryGetNameArray(EnumName, out var enumArray)) return null;
public static IReadOnlyList<string> GetOptions(IDataModel model, string enumName) {
if (!model.TryGetNameArray(enumName, out var enumArray)) return null;
// array must be at least as long as than the current value
var optionCount = enumArray.ElementCount;
@@ -139,7 +143,6 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
results.Add(value);
}
cachedOptions = results;
return results;
}

View File

@@ -0,0 +1,219 @@
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
namespace HavenSoft.HexManiac.Core.Models.Runs {
public class EggMoveRun : IFormattedRun {
public const int MagicNumber = 0x4E20; // anything above this number is a pokemon, anything below it is a move
public const int EndStream = 0xFFFF;
public const string PokemonNameTable = "pokenames";
public const string MoveNamesTable = "movenames";
public const string GroupStart = "[";
public const string GroupEnd = "]";
private readonly IDataModel model;
public int Start { get; }
public int Length { get; }
public IReadOnlyList<int> PointerSources { get; private set; }
public string FormatString => AsciiRun.StreamDelimeter + "egg" + AsciiRun.StreamDelimeter;
public EggMoveRun(IDataModel dataModel, int dataIndex) {
model = dataModel;
Start = dataIndex;
Length = 0;
for (int i = Start; i < model.Count; i += 2) {
if (model[i] == 0xFF && model[i + 1] == 0xFF) {
Length = i - Start + 2;
break;
}
}
}
public static bool TrySearch(IDataModel data, ModelDelta token, out EggMoveRun eggmoves) {
eggmoves = default;
var pokenames = data.GetNextRun(data.GetAddressFromAnchor(token, -1, PokemonNameTable)) as ArrayRun;
var movenames = data.GetNextRun(data.GetAddressFromAnchor(token, -1, MoveNamesTable)) as ArrayRun;
if (pokenames == null || movenames == null) return false;
for (var run = data.GetNextRun(0); run.Start < int.MaxValue; run = data.GetNextRun(run.Start + run.Length)) {
if (run is ArrayRun || run is PCSRun || run.PointerSources == null) continue;
// verify expected pointers to this
if (run.PointerSources.Count != 2) continue;
// verify limiter
var length = data.ReadMultiByteValue(run.PointerSources[1] - 4, 4);
// we just read the 'length' from basically a random byte... verify that it could make sense as a length
if (length < 0) continue;
if (run.Start + length * 2 + 3 < 0) continue;
if (run.Start + length * 2 + 3 > data.Count) continue;
var endValue = data.ReadMultiByteValue(run.Start + length * 2 + 2, 2);
if (endValue != EndStream) continue;
// verify content
bool possibleMatch = true;
for (int i = 0; i < length - 2; i++) {
var value = data.ReadMultiByteValue(run.Start + i * 2, 2);
if (value == EndStream) break; // early exit, the data was edited, but that's ok. Everything still matches up.
if (value >= MagicNumber) {
value -= MagicNumber;
if (value < pokenames.ElementCount) continue;
}
if (value < movenames.ElementCount) continue;
possibleMatch = false;
break;
}
if (!possibleMatch) continue;
// content is correct, length is correct: this is it!
eggmoves = new EggMoveRun(data, run.Start);
return true;
}
return false;
}
private IReadOnlyList<string> cachedPokenames, cachedMovenames;
private int lastFormatRequest = int.MinValue;
public IDataFormat CreateDataFormat(IDataModel data, int dataIndex) {
Debug.Assert(data == model);
if (dataIndex != lastFormatRequest + 1) {
cachedPokenames = ArrayRunEnumSegment.GetOptions(model, PokemonNameTable) ?? new List<string>();
cachedMovenames = ArrayRunEnumSegment.GetOptions(model, MoveNamesTable) ?? new List<string>();
}
lastFormatRequest = dataIndex;
var position = dataIndex - Start;
var groupStart = position % 2 == 1 ? position - 1 : position;
position -= groupStart;
var value = data.ReadMultiByteValue(Start + groupStart, 2);
if (value >= MagicNumber) {
value -= MagicNumber;
string content = cachedPokenames.Count > value ? cachedPokenames[value] : value.ToString();
if (value == EndStream - MagicNumber) content = string.Empty;
if (content.StartsWith("\"")) content = content.Substring(1);
if (content.EndsWith("\"")) content = content.Substring(0, content.Length - 1);
return new EggSection(groupStart + Start, position, $"{GroupStart}{content}{GroupEnd}");
} else {
string content = cachedMovenames.Count > value ? cachedMovenames[value] : value.ToString();
return new EggItem(groupStart + Start, position, content);
}
}
public IFormattedRun MergeAnchor(IReadOnlyList<int> sources) {
var newSources = new HashSet<int>();
if (sources != null) newSources.AddRange(sources);
if (PointerSources != null) newSources.AddRange(PointerSources);
return new EggMoveRun(model, Start) { PointerSources = newSources.ToList() };
}
public IFormattedRun RemoveSource(int source) {
var sources = PointerSources.ToList();
sources.Remove(source);
return new EggMoveRun(model, Start) { PointerSources = sources };
}
public int GetPokemonNumber(string input) {
if (input.StartsWith(GroupStart)) input = input.Substring(1, input.Length - 2);
var names = cachedPokenames.Select(name => name.Trim('"').ToLower()).ToList();
return GetNumber(input.ToLower(), names);
}
public int GetMoveNumber(string input) {
input = input.Trim('"').ToLower();
var names = cachedMovenames.Select(name => name.Trim('"').ToLower()).ToList();
return GetNumber(input, names);
}
public string SerializeForTool() {
var builder = new StringBuilder();
for (int i = 0; i < Length - 2; i += 2) {
var address = Start + i;
var value = model.ReadMultiByteValue(address, 2);
if (value >= MagicNumber) {
value -= MagicNumber;
builder.Append($"{GroupStart}{cachedPokenames[value].Trim('"')}{GroupEnd}");
} else {
builder.Append(cachedMovenames[value].Trim('"'));
}
if (i < Length - 4) builder.AppendLine();
}
return builder.ToString();
}
public int DeserializeFromTool(string content, ModelDelta token) {
var data = new List<int>();
var pokemonNames = cachedPokenames.Select(name => $"{GroupStart}{name.Trim('"').ToLower()}{GroupEnd}").ToList();
var moveNames = cachedMovenames.Select(name => name.Trim('"').ToLower()).ToList();
var lines = content.ToLower().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) {
var index = pokemonNames.IndexOf(line);
if (index != -1) { data.Add(index + MagicNumber); continue; }
index = moveNames.IndexOf(line);
if (index != -1) { data.Add(index); continue; }
// didn't find an exact match... look for a partial pokemon match
var matchFound = false;
for (int i = 0; i < pokemonNames.Count; i++) {
if (pokemonNames[i].Contains(line)) { data.Add(i + MagicNumber); matchFound = true; break; }
}
if (matchFound) continue;
// look for a partial move match
for (int i = 0; i < moveNames.Count; i++) {
if (moveNames[i].Contains(line)) { data.Add(i); break; }
}
}
var run = model.RelocateForExpansion(token, this, data.Count * 2 + 2);
for (int i = 0; i < data.Count; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, data[i]);
model.WriteMultiByteValue(run.Start + data.Count * 2, 2, token, EndStream); // write the new end token
for (int i = data.Count + 2; i < Length / 2; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, EndStream); // fill any remaining old space with FF
return run.Start;
}
public IEnumerable<string> GetAutoCompleteOptions() {
var pokenames = cachedPokenames.Select(name => $"{GroupStart}{name}{GroupEnd}");
var movenames = cachedMovenames.Select(name => name + " ");
return pokenames.Concat(movenames);
}
private static int GetNumber(string input, IList<string> names) {
var matchIndex = names.IndexOf(input);
if (matchIndex != -1) return matchIndex;
var match = names.FirstOrDefault(name => name.Contains(input));
if (match == null) return -1;
return names.IndexOf(match);
}
public void AppendTo(StringBuilder text, int start, int length) {
while (length > 0 && start < Start + Length) {
var value = model.ReadMultiByteValue(start, 2);
if (value == EndStream) {
text.Append($"{GroupStart}{GroupEnd}");
} else if (value >= MagicNumber) {
value -= MagicNumber;
if (value >= cachedPokenames.Count) text.Append($"{GroupStart}{value}{GroupEnd}");
else text.Append($"{GroupStart}{cachedPokenames[value].Trim('"')}{GroupEnd}");
} else {
if (value >= cachedMovenames.Count) text.Append($"{value}");
else text.Append($"{cachedMovenames[value]}");
}
start += 2;
length -= 2;
if (length > 0 && start < Start + Length) text.Append(" ");
}
}
public bool UpdateLimiter(ModelDelta token) {
if (PointerSources.Count != 2) return false;
var address = PointerSources.Last() - 4;
var limiter = Length / 2 - 2;
model.WriteMultiByteValue(address, 4, token, limiter);
return true;
}
}
}

View File

@@ -21,5 +21,8 @@ namespace HavenSoft.HexManiac.Core {
return true;
}
public static void AddRange<T>(this HashSet<T> set, IEnumerable<T> items) {
foreach (var item in items) set.Add(item);
}
}
}

View File

@@ -46,5 +46,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
public void Visit(Integer integer, byte data) => Result = integer.Value.ToString();
public void Visit(IntegerEnum integerEnum, byte data) => Result = integerEnum.Value;
public void Visit(EggSection section, byte data) => Result = section.SectionName;
public void Visit(EggItem item, byte data) => Result = item.ItemName;
}
}

View File

@@ -11,6 +11,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
void Visit(IDataFormatVisitor visitor, byte data);
}
public interface IDataFormatInstance : IDataFormat {
int Source { get; } // the beginning of the run/group that this instance belongs to
int Position { get; } // the index in the run/group that this instance belongs to
}
public interface IDataFormatVisitor {
void Visit(Undefined dataFormat, byte data);
void Visit(None dataFormat, byte data);
@@ -23,6 +28,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
void Visit(Ascii ascii, byte data);
void Visit(Integer integer, byte data);
void Visit(IntegerEnum integer, byte data);
void Visit(EggSection section, byte data);
void Visit(EggItem item, byte data);
}
/// <summary>
@@ -82,7 +89,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
}
}
public class Pointer : IDataFormat {
public class Pointer : IDataFormatInstance {
public const int NULL = -0x08000000;
public int Source { get; } // 6 hex digits
public int Position { get; } // 0 through 3
@@ -127,7 +134,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data);
}
public class PCS : IDataFormat {
public class PCS : IDataFormatInstance {
public int Source { get; }
public int Position { get; }
public string FullString { get; }
@@ -143,7 +150,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data);
}
public class EscapedPCS : IDataFormat {
public class EscapedPCS : IDataFormatInstance {
public int Source { get; }
public int Position { get; }
public string FullString { get; }
@@ -159,7 +166,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data);
}
public class ErrorPCS : IDataFormat {
public class ErrorPCS : IDataFormatInstance {
public int Source { get; }
public int Position { get; }
public string FullString { get; }
@@ -175,7 +182,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data);
}
public class Ascii : IDataFormat {
public class Ascii : IDataFormatInstance {
public int Source { get; }
public int Position { get; }
public char ThisCharacter { get; }
@@ -190,7 +197,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data);
}
public class Integer : IDataFormat {
public class Integer : IDataFormatInstance {
public int Source { get; }
public int Position { get; }
public int Value { get; }
@@ -227,4 +234,44 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats {
public override void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data);
}
public class EggSection : IDataFormatInstance {
public int Source { get; }
public int Position { get; }
public int Length => 2;
public string SectionName { get; }
public EggSection(int source, int position, string name) => (Source, Position, SectionName) = (source, position, name);
public bool Equals(IDataFormat other) {
if (other is EggSection that) {
return that.SectionName == SectionName &&
that.Source == Source &&
that.Length == Length;
}
return false;
}
public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data);
}
public class EggItem : IDataFormatInstance {
public int Source { get; }
public int Position { get; }
public int Length { get; }
public string ItemName { get; }
public EggItem(int source, int position, string name) => (Source, Position, ItemName) = (source, position, name);
public bool Equals(IDataFormat other) {
if (other is EggItem that) {
return that.ItemName == ItemName &&
that.Source == Source &&
that.Length == Length;
}
return false;
}
public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data);
}
}

View File

@@ -86,7 +86,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
CanExecute = arg => viewPort?.Goto != null,
Execute = arg => {
var text = Text;
if (CompletionIndex != -1) text = AutoCompleteOptions[CompletionIndex].CompletionText;
var index = completionIndex.LimitToRange(-1, AutoCompleteOptions.Count - 1);
if (index != -1) text = AutoCompleteOptions[index].CompletionText;
if (arg is string) text = (string)arg;
viewPort?.Goto?.Execute(text);
ControlVisible = false;

View File

@@ -42,7 +42,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
if (selectionStart.Equals(value)) return;
if (!Scroll.ScrollToPoint(ref value)) {
PreviewSelectionStartChanged?.Invoke(this, rawSelectionStart);
PreviewSelectionStartChanged?.Invoke(this, getSpan(rawSelectionStart).start);
}
rawSelectionStart = value;

View File

@@ -66,6 +66,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
if (run.Start > address) return; // wrong run, don't adjust
if (run is PCSRun pcsRun) UpdateRun(pcsRun);
if (run is ArrayRun arrayRun) UpdateRun(arrayRun);
if (run is EggMoveRun eggRun) UpdateRun(eggRun);
}
}
}
@@ -77,7 +78,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
if (ignoreExternalUpdates) return;
var run = model.GetNextRun(value);
if (TryUpdate(ref address, value)) {
if ((run is PCSRun || run is ArrayRun) && run.Start <= value) {
if ((run is PCSRun || run is ArrayRun || run is EggMoveRun) && run.Start <= value) {
runner.Schedule(DataForCurrentRunChanged);
Enabled = true;
ShowMessage = false;
@@ -174,6 +175,13 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
}
}
return;
} else if (run is EggMoveRun egg) {
var newContent = egg.SerializeForTool();
ignoreSelectionUpdates = true;
using (new StubDisposable { Dispose = () => ignoreSelectionUpdates = false }) {
TryUpdate(ref content, newContent, nameof(Content));
}
return;
}
throw new NotImplementedException();
@@ -186,11 +194,15 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
return newContent;
}
/// <summary>
/// If a selection update is requested due to a change in the Content, ignore it.
/// Otherwise, the selection update could send us back to the begining of the run.
/// </summary>
private bool ignoreSelectionUpdates;
private void UpdateSelectionFromTool() {
if (ignoreSelectionUpdates) return;
var run = model.GetNextRun(Address);
if (!(run is ArrayRun) && !(run is PCSRun)) return;
if (!(run is ArrayRun) && !(run is PCSRun) && !(run is EggMoveRun)) return;
// for arrays, the address must be at the start of a string segment within the first element of the array
if (run is ArrayRun arrayRun) {
@@ -216,6 +228,15 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
var afterLines = content.Substring(0, contentIndex + selectionLength).Split(new[] { Environment.NewLine }, StringSplitOptions.None);
var selectionEnd = textStart + (afterLines.Length - 1) * array.ElementLength + afterLines[afterLines.Length - 1].Length;
selectionLength = selectionEnd - selectionStart;
} else if (run is EggMoveRun egg) {
var beforeSelection = content.Substring(0, selectionStart);
var beforeLineCount = (beforeSelection.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Length - 1).LimitToRange(0, int.MaxValue);
var withSelection = content.Substring(0, selectionStart + selectionLength);
var withSelectionLineCount = (withSelection.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Length - 1).LimitToRange(0, int.MaxValue);
selectionStart = egg.Start + beforeLineCount * 2;
var selectionEnd = egg.Start + withSelectionLineCount * 2;
selectionLength = selectionEnd - selectionStart;
}
selection.SelectionStart = selection.Scroll.DataIndexToViewPoint(selectionStart);
@@ -272,5 +293,21 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
TryUpdate(ref address, run.Start, nameof(Address));
ignoreExternalUpdates = false;
}
private void UpdateRun(EggMoveRun run) {
ignoreExternalUpdates = true;
var newStart = run.DeserializeFromTool(content, history.CurrentChange);
var newRun = new EggMoveRun(model, newStart);
if (newRun.Length != run.Length) {
model.ObserveRunWritten(history.CurrentChange, newRun);
newRun = (EggMoveRun)model.GetNextRun(newRun.Start);
history.CurrentChange.AddRun(newRun);
newRun.UpdateLimiter(history.CurrentChange);
}
if (run.Start != newRun.Start) ModelDataMoved?.Invoke(this, (run.Start, newRun.Start));
ModelDataChanged?.Invoke(this, newRun);
ignoreExternalUpdates = false;
}
}
}

View File

@@ -141,7 +141,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
ClearEdits(location);
} else {
var endEdit = " ";
if (underEdit.CurrentText.Count(c => c == '"') % 2 == 1) endEdit = "\"";
if (underEdit.CurrentText.Count(c => c == StringDelimeter) % 2 == 1) endEdit = StringDelimeter.ToString();
var originalFormat = underEdit.OriginalFormat;
if (originalFormat is Anchor anchor) originalFormat = anchor.OriginalFormat;
if (underEdit.CurrentText.StartsWith(EggMoveRun.GroupStart) && (originalFormat is EggSection || originalFormat is EggItem)) endEdit = EggMoveRun.GroupEnd;
currentView[location.X, location.Y] = new HexElement(element.Value, underEdit.Edit(endEdit));
if (!TryCompleteEdit(location)) ClearEdits(location);
}
@@ -165,7 +168,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
var offsets = array.ConvertByteOffsetToArrayOffset(dataIndex);
Tools.StringTool.Address = offsets.SegmentStart - offsets.ElementIndex * array.ElementLength;
Tools.TableTool.Address = array.Start + array.ElementLength * offsets.ElementIndex;
} else if (run.Start <= dataIndex && run is PCSRun) {
} else if (run.Start <= dataIndex && (run is PCSRun || run is EggMoveRun)) {
Tools.StringTool.Address = run.Start;
} else {
Tools.StringTool.Address = dataIndex;
@@ -587,6 +590,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
}
run = Model.GetNextRun(index);
var cellToText = new ConvertCellToText(Model, run.Start);
var cell = currentView[point.X, point.Y];
if (run is PCSRun pcs) {
for (int i = index; i < run.Start + run.Length; i++) history.CurrentChange.ChangeData(Model, i, 0xFF);
@@ -605,19 +610,16 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
RefreshBackingData();
SelectionStart = scroll.DataIndexToViewPoint(index - 1);
} else if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.Pointer) {
var cell = currentView[point.X, point.Y];
PrepareForMultiSpaceEdit(point, 4);
var destination = ((Pointer)cell.Format).DestinationAsText;
destination = destination.Substring(0, destination.Length - 1);
currentView[point.X, point.Y] = new HexElement(cell.Value, new UnderEdit(cell.Format, destination, 4));
} else if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.Integer) {
var cell = currentView[point.X, point.Y];
var format = (Integer)cell.Format;
PrepareForMultiSpaceEdit(point, format.Length);
var text = format.Value.ToString();
if (format is IntegerEnum intEnum) text = intEnum.Value;
PrepareForMultiSpaceEdit(point, ((Integer)cell.Format).Length);
cell.Format.Visit(cellToText, cell.Value);
var text = cellToText.Result;
text = text.Substring(0, text.Length - 1);
currentView[point.X, point.Y] = new HexElement(cell.Value, new UnderEdit(format, text, format.Length));
currentView[point.X, point.Y] = new HexElement(cell.Value, new UnderEdit(cell.Format, text, ((Integer)cell.Format).Length));
} else {
throw new NotImplementedException();
}
@@ -625,23 +627,32 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
return;
}
if (run is EggMoveRun eggRun) {
PrepareForMultiSpaceEdit(point, 2);
cell.Format.Visit(cellToText, cell.Value);
var text = cellToText.Result;
text = text.Substring(0, text.Length - 1);
currentView[point.X, point.Y] = new HexElement(cell.Value, new UnderEdit(cell.Format, text, 2));
NotifyCollectionChanged(ResetArgs);
return;
}
if (run.Start <= index && run.Start + run.Length > index) {
// I want to do a backspace at the end of this run
SelectionStart = scroll.DataIndexToViewPoint(run.Start);
var cellToText = new ConvertCellToText(Model, run.Start);
var element = currentView[SelectionStart.X, SelectionStart.Y];
element.Format.Visit(cellToText, element.Value);
var text = cellToText.Result;
var editLength = 1;
if (element.Format is Pointer pointer) editLength = 4;
// if (element.Format is Integer integer) editLength = integer.Length;
for (int i = 0; i < run.Length; i++) {
var p = scroll.DataIndexToViewPoint(run.Start + i);
string editString = i == 0 ? text.Substring(0, text.Length - 1) : string.Empty;
if (i > 0) editLength = 1;
currentView[p.X, p.Y] = new HexElement(currentView[p.X, p.Y].Value, new UnderEdit(currentView[p.X, p.Y].Format, editString, editLength));
var format = new UnderEdit(currentView[p.X, p.Y].Format, editString, editLength);
currentView[p.X, p.Y] = new HexElement(currentView[p.X, p.Y].Value, format);
}
} else {
SelectionStart = scroll.DataIndexToViewPoint(index);
@@ -697,38 +708,70 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
var textResults = Search(searchBytes).ToList();
Model.ConsiderResultsAsTextRuns(history.CurrentChange, textResults);
foreach (var result in textResults) {
// if the result is in an array, we care about things that use that array
if (Model.GetNextRun(result) is ArrayRun parentArray && parentArray.LengthFromAnchor == string.Empty) {
var offsets = parentArray.ConvertByteOffsetToArrayOffset(result);
var parentArrayName = Model.GetAnchorFromAddress(-1, parentArray.Start);
if (offsets.SegmentIndex == 0 && parentArray.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) {
foreach (var child in Model.Arrays) {
// option 1: another table has a row named after this element
if (child.LengthFromAnchor == parentArrayName) {
var address = child.Start + child.ElementLength * offsets.ElementIndex;
yield return (address, address + child.ElementLength - 1);
}
foreach (var dataResult in FindMatchingDataResultsFromArrayElement(parentArray, result)) yield return dataResult;
}
// option 2: another table has an enum named after this element
var segmentOffset = 0;
foreach (var segment in child.ElementContent) {
if (!(segment is ArrayRunEnumSegment enumSegment) || enumSegment.EnumName != parentArrayName) {
segmentOffset += segment.Length;
continue;
}
for (int i = 0; i < child.ElementCount; i++) {
var address = child.Start + child.ElementLength * i + segmentOffset;
var enumValue = Model.ReadMultiByteValue(address, segment.Length);
if (enumValue != offsets.ElementIndex) continue;
yield return (address, address + segment.Length - 1);
}
segmentOffset += segment.Length;
yield return (result, result + pcsBytes.Count - 1);
}
}
/// <summary>
/// When performing a search, sometimes one of the search results is text from a table.
/// If so, then we also care about places where that table value is used.
/// This function finds uses of an element in a table.
/// </summary>
private IEnumerable<(int start, int end)> FindMatchingDataResultsFromArrayElement(ArrayRun parentArray, int parentIndex) {
var offsets = parentArray.ConvertByteOffsetToArrayOffset(parentIndex);
var parentArrayName = Model.GetAnchorFromAddress(-1, parentArray.Start);
if (offsets.SegmentIndex == 0 && parentArray.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) {
var arrayUses = FindTableUsages(offsets, parentArrayName);
var streamUses = FindStreamUsages(offsets, parentArrayName);
return arrayUses.Concat(streamUses);
}
return Enumerable.Empty<(int, int)>();
}
private IEnumerable<(int start, int end)> FindTableUsages(ArrayOffset offsets, string parentArrayName) {
foreach (var child in Model.Arrays) {
// option 1: another table has a row named after this element
if (child.LengthFromAnchor == parentArrayName) {
var address = child.Start + child.ElementLength * offsets.ElementIndex;
yield return (address, address + child.ElementLength - 1);
}
// option 2: another table has an enum named after this element
var segmentOffset = 0;
foreach (var segment in child.ElementContent) {
if (!(segment is ArrayRunEnumSegment enumSegment) || enumSegment.EnumName != parentArrayName) {
segmentOffset += segment.Length;
continue;
}
for (int i = 0; i < child.ElementCount; i++) {
var address = child.Start + child.ElementLength * i + segmentOffset;
var enumValue = Model.ReadMultiByteValue(address, segment.Length);
if (enumValue != offsets.ElementIndex) continue;
yield return (address, address + segment.Length - 1);
}
segmentOffset += segment.Length;
}
}
}
private IEnumerable<(int start, int end)> FindStreamUsages(ArrayOffset offsets, string parentArrayName) {
foreach (var child in Model.Streams) {
// option 1: the value is used by egg moves
if (child is EggMoveRun eggRun) {
var groupStart = 0;
if (parentArrayName == EggMoveRun.PokemonNameTable) groupStart = EggMoveRun.MagicNumber;
if (parentArrayName == EggMoveRun.PokemonNameTable || parentArrayName == EggMoveRun.MoveNamesTable) {
for (int i = 0; i < eggRun.Length - 2; i += 2) {
if (Model.ReadMultiByteValue(eggRun.Start + i, 2) == offsets.ElementIndex + groupStart) {
yield return (eggRun.Start + i, eggRun.Start + i + 1);
}
}
}
}
yield return (result, result + pcsBytes.Count - 1);
}
}
@@ -868,6 +911,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
}
Tools.SelectedIndex = Enumerable.Range(0, Tools.Count).First(i => Tools[i] is PCSTool);
}
if (format is EggSection || format is EggItem) {
var byteOffset = scroll.ViewPointToDataIndex(new Point(x, y));
var currentRun = Model.GetNextRun(byteOffset);
Tools.StringTool.Address = currentRun.Start;
Tools.SelectedIndex = Enumerable.Range(0, Tools.Count).First(i => Tools[i] is PCSTool);
}
}
public void ExpandSelection(int x, int y) {
@@ -950,6 +999,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
}
private IReadOnlyList<AutoCompleteSelectionItem> GetAutocompleteOptions(IDataFormat originalFormat, string newText, int selectedIndex = -1) {
if (originalFormat is Anchor anchor) originalFormat = anchor.OriginalFormat;
if (newText.StartsWith(PointerStart.ToString())) {
return Model.GetNewPointerAutocompleteOptions(newText, selectedIndex);
} else if (newText.StartsWith(GotoMarker.ToString())) {
@@ -959,6 +1009,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
var segment = (ArrayRunEnumSegment)array.ElementContent[array.ConvertByteOffsetToArrayOffset(intEnum.Source).SegmentIndex];
var options = segment.GetOptions(Model).Select(option => option + " "); // autocomplete needs to complete after selection, so add a space
return AutoCompleteSelectionItem.Generate(options.Where(option => option.MatchesPartial(newText)), selectedIndex);
} else if (originalFormat is EggSection || originalFormat is EggItem) {
var eggRun = (EggMoveRun)Model.GetNextRun(((IDataFormatInstance)originalFormat).Source);
var allOptions = eggRun.GetAutoCompleteOptions();
return AutoCompleteSelectionItem.Generate(allOptions.Where(option => option.MatchesPartial(newText)), selectedIndex);
} else {
throw new NotImplementedException();
}
@@ -1035,12 +1089,20 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
var run = Model.GetNextRun(index);
if (run.Start > index) return (p, p);
if (run is PointerRun) return (scroll.DataIndexToViewPoint(run.Start), scroll.DataIndexToViewPoint(run.Start + run.Length - 1));
(Point, Point) pair(int start, int end) => (scroll.DataIndexToViewPoint(start), scroll.DataIndexToViewPoint(end));
if (run is PointerRun) return pair(run.Start, run.Start + run.Length - 1);
if (run is EggMoveRun) {
var even = (index - run.Start) % 2 == 0;
if (even) return pair(index, index + 1);
return pair(index - 1, index);
}
if (!(run is ArrayRun array)) return (p, p);
var offset = array.ConvertByteOffsetToArrayOffset(index);
if (array.ElementContent[offset.SegmentIndex].Type == ElementContentType.Pointer || array.ElementContent[offset.SegmentIndex].Type == ElementContentType.Integer) {
return (scroll.DataIndexToViewPoint(offset.SegmentStart), scroll.DataIndexToViewPoint(offset.SegmentStart + array.ElementContent[offset.SegmentIndex].Length - 1));
var type = array.ElementContent[offset.SegmentIndex].Type;
if (type == ElementContentType.Pointer || type == ElementContentType.Integer) {
return pair(offset.SegmentStart, offset.SegmentStart + array.ElementContent[offset.SegmentIndex].Length - 1);
}
return (p, p);
@@ -1101,6 +1163,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels {
RefreshBackingData();
}
var run = Model.GetNextRun(completeEditOperation.NewDataIndex);
if (run.Start > completeEditOperation.NewDataIndex) run = new NoInfoRun(Model.Count);
if (completeEditOperation.DataMoved) UpdateToolsFromSelection(run.Start);
if (run is ArrayRun) Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged);
if (run is ArrayRun || run is PCSRun) Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged);

View File

@@ -19,8 +19,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
public int NewDataIndex { get; private set; } // for completed edits, where should the selection move to?
public bool DataMoved { get; private set; }
public string MessageText { get; private set; } // is there a message to display to the user? For example, when data gets moved.
public string ErrorText { get; private set; } // is there an error to display to the user? For example, invalide pointer
public HexElement NewCell { get; private set; } // if result is true and this is not null, asign this one value back to the one cell
public string ErrorText { get; private set; } // is there an error to display to the user? For example, invalid pointer
public HexElement NewCell { get; private set; } // if result is true and this is not null, assign this one value back to the one cell
// and refresh the one cell (along with any other UnderEdit cells)
// if result is true and this _is_ null, then the entire screen needs to be refreshed.
@@ -109,6 +109,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
}
}
public void Visit(EggSection section, byte data) => CompleteEggEdit();
public void Visit(EggItem item, byte data) => CompleteEggEdit();
private void CompleteIntegerEdit(Integer integer) {
if (!int.TryParse(CurrentText, out var result)) {
ErrorText = $"Could not parse {CurrentText} as a number";
@@ -279,5 +283,73 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
CurrentChange.ChangeData(Model, memoryLocation, byteValue);
NewDataIndex = memoryLocation + 1;
}
private void CompleteEggEdit() {
var endChar = CurrentText[CurrentText.Length - 1];
if (!$"{EggMoveRun.GroupEnd} {StringDelimeter}".Contains(endChar)) return;
if (CurrentText.Count(c => c == StringDelimeter) % 2 != 0) return;
NewDataIndex = memoryLocation + 2;
Result = true;
var run = (EggMoveRun)Model.GetNextRun(memoryLocation);
if (CurrentText == EggMoveRun.GroupStart + EggMoveRun.GroupEnd) {
Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, 0xFFFF);
// clear all data after this and shorten the run
for (int i = memoryLocation + 2; i < run.Start + run.Length; i += 2) {
Model.WriteMultiByteValue(i, 2, CurrentChange, 0xFFFF);
}
var newRun = new EggMoveRun(Model, run.Start);
Model.ObserveRunWritten(CurrentChange, newRun);
newRun = (EggMoveRun)Model.GetNextRun(newRun.Start);
newRun.UpdateLimiter(CurrentChange);
} else if (CurrentText.EndsWith(EggMoveRun.GroupEnd)) {
var value = run.GetPokemonNumber(CurrentText);
if (value == -1) {
ErrorText = $"Could not parse {CurrentText} as a pokemon name";
NewDataIndex -= 2;
} else {
WriteNormalEggEdit(run, value + EggMoveRun.MagicNumber);
}
} else {
var text = CurrentText.Trim();
var value = run.GetMoveNumber(text);
if (value == -1) {
// wasn't a move... try again as a pokemon even though they didn't use the []
value = run.GetPokemonNumber(text);
if (value == -1) {
ErrorText = $"Could not parse {text} as a move name or pokemon name";
NewDataIndex -= 2;
} else {
WriteNormalEggEdit(run, value + EggMoveRun.MagicNumber);
}
} else {
WriteNormalEggEdit(run, value);
}
}
}
/// <summary>
/// Before we write this change to the model, see if we need to extend the egg run to make it fit.
/// </summary>
private void WriteNormalEggEdit(EggMoveRun run, int value) {
int memoryLocation = this.memoryLocation;
var initialItemValue = Model.ReadMultiByteValue(memoryLocation, 2);
Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value);
if (initialItemValue == 0xFFFF) {
var newRun = Model.RelocateForExpansion(CurrentChange, run, run.Length + 2);
if (newRun.Start != run.Start) {
MessageText = $"Egg Moves were automatically moved to {newRun.Start.ToString("X6")}. Pointers were updated.";
memoryLocation += newRun.Start - run.Start;
NewDataIndex = memoryLocation + 2;
DataMoved = true;
}
Model.WriteMultiByteValue(memoryLocation + 2, 2, CurrentChange, 0xFFFF);
var eggRun = new EggMoveRun(Model, newRun.Start);
Model.ObserveRunWritten(CurrentChange, eggRun);
eggRun = (EggMoveRun)Model.GetNextRun(eggRun.Start);
eggRun.UpdateLimiter(CurrentChange);
}
}
}
}

View File

@@ -117,6 +117,16 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
Results.AddRange(GetTableChildren(arrayRun));
}
public void Visit(EggSection section, byte data) {
var point = ViewPort.SelectionStart;
Results.Add(new ContextItem("Open In Text Tool", arg => ViewPort.FollowLink(point.X, point.Y)) { ShortcutText = "Ctrl+Click" });
}
public void Visit(EggItem item, byte data) {
var point = ViewPort.SelectionStart;
Results.Add(new ContextItem("Open In Text Tool", arg => ViewPort.FollowLink(point.X, point.Y)) { ShortcutText = "Ctrl+Click" });
}
private IEnumerable<IContextItem> GetTableChildren(ArrayRun array) {
if (ViewPort.Tools.TableTool.Append.CanExecute(null)) {
yield return new ContextItem("Extend Table", ViewPort.Tools.TableTool.Append.Execute);

View File

@@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using System;
using System.Linq;
@@ -71,5 +72,17 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
".~".Contains(Input) ||
char.IsWhiteSpace(Input);
}
public void Visit(EggSection section, byte data) => VisitEgg();
public void Visit(EggItem item, byte data) => VisitEgg();
public void VisitEgg() {
var specialCharacters = ". '-\\"; // mr. mime, farfetch'd, double-edge, nidoran
if (UnderEdit.CurrentText.StartsWith(EggMoveRun.GroupStart)) specialCharacters += ']';
if (UnderEdit.CurrentText.StartsWith(StringDelimeter.ToString())) specialCharacters += StringDelimeter;
Result =
char.IsLetterOrDigit(Input) ||
specialCharacters.Contains(Input) ||
char.IsWhiteSpace(Input);
}
}
}

View File

@@ -1,4 +1,5 @@
using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using System;
@@ -40,8 +41,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
public void Visit(Ascii ascii, byte data) => currentChange.ChangeData(buffer, index, 0xFF);
public void Visit(Integer integer, byte data) => buffer.WriteValue(currentChange, index, 0);
public void Visit(Integer integer, byte data) => buffer.WriteMultiByteValue(index, integer.Length, currentChange, 0);
public void Visit(IntegerEnum integerEnum, byte data) => buffer.WriteValue(currentChange, index, 0);
public void Visit(IntegerEnum integerEnum, byte data) => buffer.WriteMultiByteValue(index, integerEnum.Length, currentChange, 0);
public void Visit(EggSection section, byte data) => buffer.WriteMultiByteValue(index, 2, currentChange, EggMoveRun.MagicNumber);
public void Visit(EggItem item, byte data) => buffer.WriteMultiByteValue(index, 2, currentChange, 0x0000);
}
}

View File

@@ -49,7 +49,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
// just continue the existing edit.
public void Visit(UnderEdit underEdit, byte data) {
// handle special cases of "anywhere" formats first
if (underEdit.CurrentText[0] == ViewPort.GotoMarker) {
if (underEdit.CurrentText.StartsWith(ViewPort.GotoMarker.ToString())) {
Result = char.IsLetterOrDigit(Input) || Input == ArrayAnchorSeparator || char.IsWhiteSpace(Input);
return;
} else if (underEdit.CurrentText.StartsWith(AnchorStart.ToString())) {
@@ -67,10 +67,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
}
public void Visit(Pointer pointer, byte data) {
if (Input != PointerStart && !char.IsLetterOrDigit(Input)) {
Result = false;
return;
}
if (Input != PointerStart && !char.IsLetterOrDigit(Input)) return;
var editText = Input.ToString();
// if the user tries to edit the pointer but forgets the opening bracket, add it for them.
@@ -89,10 +86,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
public void Visit(PCS pcs, byte data) {
if (Model.GetNextRun(MemoryLocation) is ArrayRun array) {
var offsets = array.ConvertByteOffsetToArrayOffset(MemoryLocation);
if (offsets.SegmentStart == MemoryLocation && Input == ' ') {
Result = false; // don't let it start with a space unless it's in quotes (for copy/paste)
return;
}
// don't let it start with a space unless it's in quotes (for copy/paste)
if (offsets.SegmentStart == MemoryLocation && Input == ' ') return;
}
Result = Input == StringDelimeter || PCSString.PCS.Any(str => str != null && str.StartsWith(Input.ToString()));
@@ -109,20 +104,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
public void Visit(Ascii ascii, byte data) => Result = true;
public void Visit(Integer intFormat, byte data) {
if (!intFormat.CanStartWithCharacter(Input)) {
Result = false;
return;
}
if (!intFormat.CanStartWithCharacter(Input)) return;
NewFormat = new UnderEdit(intFormat, Input.ToString(), intFormat.Length, null);
Result = true;
}
public void Visit(IntegerEnum integer, byte data) {
if (!integer.CanStartWithCharacter(Input)) {
Result = false;
return;
}
if (!integer.CanStartWithCharacter(Input)) return;
var arrayRun = (ArrayRun)Model.GetNextRun(MemoryLocation);
var offsets = arrayRun.ConvertByteOffsetToArrayOffset(MemoryLocation);
@@ -132,5 +121,17 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
NewFormat = new UnderEdit(integer, Input.ToString(), integer.Length, autocomplete);
Result = true;
}
public void Visit(EggSection section, byte data) => VisitEgg(section);
public void Visit(EggItem item, byte data) => VisitEgg(item);
public void VisitEgg(IDataFormat eggFormat) {
if (!char.IsLetterOrDigit(Input) && !$"{StringDelimeter}[".Contains(Input)) return;
var stream = (EggMoveRun)Model.GetNextRun(MemoryLocation);
var allOptions = stream.GetAutoCompleteOptions();
var autocomplete = AutoCompleteSelectionItem.Generate(allOptions.Where(option => option.MatchesPartial(Input.ToString())), -1);
NewFormat = new UnderEdit(eggFormat, Input.ToString(), 2, autocomplete);
Result = true;
}
}
}

View File

@@ -30,7 +30,7 @@ namespace HavenSoft.HexManiac.Tests {
var model = LoadModel(game);
var noChange = new NoDataChangeDeltaModel();
var address = model.GetAddressFromAnchor(noChange, -1, "pokenames");
var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.PokemonNameTable);
var run = (ArrayRun)model.GetNextAnchor(address);
Assert.Equal(412, run.ElementCount);
}
@@ -41,7 +41,7 @@ namespace HavenSoft.HexManiac.Tests {
var model = LoadModel(game);
var noChange = new NoDataChangeDeltaModel();
var address = model.GetAddressFromAnchor(noChange, -1, "movenames");
var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.MoveNamesTable);
var run = (ArrayRun)model.GetNextAnchor(address);
Assert.Equal(355, run.ElementCount);
}
@@ -141,6 +141,22 @@ namespace HavenSoft.HexManiac.Tests {
for (int i = 0; i < compareSet.Length; i++) Assert.Equal(compareSet[i], poundStats[i]);
}
[SkippableTheory]
[MemberData(nameof(PokemonGames))]
public void EggMoveDataFound(string game) {
var model = LoadModel(game);
var noChange = new NoDataChangeDeltaModel();
var address = model.GetAddressFromAnchor(noChange, -1, "eggmoves");
var run = (EggMoveRun)model.GetNextAnchor(address);
Assert.Equal(2, run.PointerSources.Count);
var expectedLastElement = model.ReadMultiByteValue(run.PointerSources[1] - 4, 4);
var expectedLength = expectedLastElement + 1;
var actualLength = run.Length / 2 - 1; // remove the closing element.
Assert.InRange(actualLength, 790, expectedLength);
}
/// <summary>
/// Loading the model can take a while.
/// We want to know that loading the model created the correct arrays,

View File

@@ -0,0 +1,234 @@
using HavenSoft.HexManiac.Core;
using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace HavenSoft.HexManiac.Tests {
public class EggMoveTests {
private readonly byte[] data;
private readonly PokemonModel model;
private readonly ViewPort viewPort;
private readonly List<string> messages = new List<string>();
#region Setup
public EggMoveTests() {
data = Enumerable.Range(0, 0x200).Select(i => (byte)0xFF).ToArray();
model = new PokemonModel(data);
viewPort = new ViewPort("file.gba", model);
viewPort.Goto.Execute("000080");
viewPort.Edit("^pokenames[name\"\"8]8 \"Bob\" \"Steve\" \"Carl\" \"Sam\" \"Bryan\" \"Ryan\" \"Ian\" \"Matt\"");
viewPort.Goto.Execute("000100");
viewPort.Edit("^movenames[name\"\"8]8 \"Fire\" \"Water\" \"Earth\" \"Wind\" \"Light\" \"Dark\" \"Normal\" \"Magic\"");
viewPort.Goto.Execute("000180");
viewPort.Edit("<000000> Dead Beef 01 00 00 00 <000000>"); // limiter is at 188 for an eggrun at 000
viewPort.Goto.Execute("000000");
viewPort.OnMessage += (sender, e) => messages.Add(e);
}
private void CreateSimpleRun() {
var token = new ModelDelta();
model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl
model.WriteMultiByteValue(2, 2, token, 3); // Wind
viewPort.Edit("^eggmoves`egg` ");
}
#endregion
[Fact]
public void CanCreateEggMoveStream() {
viewPort.Edit("^eggmoves`egg` ");
Assert.Equal(2, model.GetNextRun(0).Length);
}
[Fact]
public void CanSeeEggMoveStreamWithCorrectFormat() {
CreateSimpleRun();
Assert.Equal(6, model.GetNextRun(0).Length);
var section = (EggSection)viewPort[1, 0].Format;
var item = (EggItem)viewPort[2, 0].Format;
var endSection = (EggSection)viewPort[5, 0].Format;
Assert.Equal("[Carl]", section.SectionName);
Assert.Equal("Wind", item.ItemName);
Assert.Equal("[]", endSection.SectionName);
}
[Fact]
public void SelectionDoneInPairs() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 0); // should select "Wind"
Assert.True(viewPort.IsSelected(new Point(3, 0)));
viewPort.MoveSelectionStart.Execute(Direction.Right); // should select "[]"
Assert.True(viewPort.IsSelected(new Point(4, 0)));
Assert.True(viewPort.IsSelected(new Point(5, 0)));
}
[Fact]
public void CanEditEggStreamManually() {
CreateSimpleRun();
viewPort.Edit("Dark ");
Assert.Equal(5, model[0]);
viewPort.Edit("[Bryan]");
Assert.Equal(EggMoveRun.MagicNumber + 4, model.ReadMultiByteValue(2, 2));
}
[Fact]
public void CanCopyPaste() {
CreateSimpleRun();
var fileSystem = new StubFileSystem();
viewPort.SelectionStart = new Point(2, 0);
viewPort.SelectionEnd = new Point(4, 0);
viewPort.Copy.Execute(fileSystem);
Assert.Equal("Wind []", fileSystem.CopyText.value);
}
[Fact]
public void RunAutoExtends() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(4, 0);
viewPort.Edit("[Carl]");
Assert.Equal(8, model.GetNextRun(0).Length);
Assert.Equal(2, model.ReadMultiByteValue(0x188, 4));
}
[Fact]
public void RunAutoMoves() {
viewPort.Edit("^eggmoves`egg` ");
model.WriteMultiByteValue(2, 2, new ModelDelta(), 0x0206);
viewPort.Edit("[Carl]");
Assert.Single(messages);
}
[Fact]
public void RunAutoShortens() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 0);
viewPort.Edit("[]");
Assert.Equal(4, model.GetNextRun(0).Length);
Assert.Equal(0, model.ReadMultiByteValue(0x188, 4));
}
[Fact]
public void CanViewInTextTool() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 2); // select off it
viewPort.SelectionStart = new Point(2, 0); // and then back on
viewPort.Tools.StringToolCommand.Execute();
Assert.Equal(@"[Carl]
Wind", viewPort.Tools.StringTool.Content);
viewPort.Tools.StringTool.Content = @"[Carl]
Earth
Light
[Ryan]
Fire
Water";
Assert.Equal(14, model.GetNextRun(0).Length);
Assert.Equal(5, model.ReadMultiByteValue(0x188, 4));
}
[Fact]
public void FollowLinkOpensTextTool() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 0);
viewPort.FollowLink(2, 0);
Assert.Equal(0, viewPort.Tools.SelectedIndex);
}
[Fact]
public void RightClickOptionToOpenTextTool() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 0);
var items = viewPort.GetContextMenuItems(viewPort.SelectionStart);
Assert.Contains(items, item => item.ShortcutText == "Ctrl+Click");
}
[Fact]
public void ToolCursorChangesCauseSelectionChanges() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 0);
viewPort.Tools.StringToolCommand.Execute();
viewPort.Tools.StringTool.ContentIndex = 5;
Assert.True(viewPort.IsSelected(new Point(0, 0)));
Assert.True(viewPort.IsSelected(new Point(1, 0)));
}
[Fact]
public void SearchFindsEggMoveData() {
CreateSimpleRun();
var pairs = viewPort.Find("wind");
Assert.Contains((2, 3), pairs);
pairs = viewPort.Find("carl");
Assert.Contains((0, 1), pairs);
}
[Fact]
public void BackspaceWorks() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 0);
viewPort.Edit(ConsoleKey.Backspace);
var format = (UnderEdit)viewPort[2, 0].Format;
Assert.Equal("Win", format.CurrentText); // Wind, but backspaced
}
/// <summary>
/// Since egg moves are multiple cells long, we want to know that
/// changes get reverted when 'selectionstart' is the rightmost cell
/// </summary>
[Fact]
public void SelectLeftThenBackspaceThenDownCompletesEdits() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 0);
viewPort.MoveSelectionStart.Execute(Direction.Left);
viewPort.Edit(ConsoleKey.Backspace);
viewPort.MoveSelectionStart.Execute(Direction.Down);
Assert.IsNotType<UnderEdit>(viewPort[1, 0].Format);
}
[Fact]
public void CanClearCellContent() {
CreateSimpleRun();
viewPort.SelectionStart = new Point(2, 0);
viewPort.Edit(ConsoleKey.Backspace); // d
viewPort.Edit(ConsoleKey.Backspace); // n
viewPort.Edit(ConsoleKey.Backspace); // i
viewPort.Edit(ConsoleKey.Backspace); // W
var format = (UnderEdit)viewPort[2, 0].Format;
Assert.Equal(string.Empty, format.CurrentText);
}
}
}

View File

@@ -71,6 +71,7 @@
<Compile Include="AutoSearchTests.cs" />
<Compile Include="ChangeHistoryTests.cs" />
<Compile Include="AsciiRunTests.cs" />
<Compile Include="EggMoveTests.cs" />
<Compile Include="FindTests.cs" />
<Compile Include="GeneralAppTests.cs" />
<Compile Include="..\..\artifacts\$(AssemblyName)\codegen\StubFileSystem.cs" />

View File

@@ -1,6 +1,7 @@
using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.WPF.Controls;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -45,14 +46,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations {
var content = dataFormat.CurrentText;
var text = new FormattedText(
content,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
FontSize,
brush,
1.0);
var text = CreateText(content, FontSize, brush);
var offset = CellTextOffset;
var widthOverflow = text.Width - HexContent.CellWidth * dataFormat.EditWidth;
@@ -72,18 +66,10 @@ namespace HavenSoft.HexManiac.WPF.Implementations {
if (dataFormat.Destination < 0) brush = Brush(nameof(Theme.Error));
Underline(brush, dataFormat.Position == 0, dataFormat.Position == 3);
var typeface = new Typeface("Consolas");
var destination = dataFormat.DestinationAsText;
if (destination.Length > 13) destination = destination.Substring(0, 11) + "…>";
var xOffset = 51 - (dataFormat.Position * HexContent.CellWidth) - destination.Length * 4.2; // centering
var text = new FormattedText(
destination,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
FontSize,
brush,
1.0);
var text = CreateText(destination, FontSize, brush);
if (dataFormat.Position > Position.X || Position.X - dataFormat.Position > modelWidth - 4) {
context.PushClip(rectangleGeometry);
@@ -103,15 +89,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations {
}
public void Visit(PCS pcs, byte data) {
var typeface = new Typeface("Consolas");
var text = new FormattedText(
pcs.ThisCharacter,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
FontSize,
Brush(nameof(Theme.Text1)),
1.0);
var text = CreateText(pcs.ThisCharacter, FontSize, Brush(nameof(Theme.Text1)));
var xOffset = 1 - pcs.ThisCharacter.Length;
context.DrawText(text, new Point(CellTextOffset.X + xOffset, CellTextOffset.Y));
@@ -123,33 +101,16 @@ namespace HavenSoft.HexManiac.WPF.Implementations {
public void Visit(ErrorPCS pcs, byte data) {
var brush = Brush(nameof(Theme.Error));
var typeface = new Typeface("Consolas");
var content = data.ToString("X2");
var text = new FormattedText(
content,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
FontSize,
brush,
1.0);
var text = CreateText(content, FontSize, brush);
context.DrawText(text, CellTextOffset);
}
public void Visit(Ascii ascii, byte data) {
var typeface = new Typeface("Consolas");
var text = new FormattedText(
ascii.ThisCharacter.ToString(),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
FontSize,
Brush(nameof(Theme.Text2)),
1.0);
var text = CreateText(ascii.ThisCharacter.ToString(), FontSize, Brush(nameof(Theme.Text2)));
context.DrawText(text, CellTextOffset);
}
@@ -158,15 +119,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations {
var stringValue = integer.Value.ToString();
var typeface = new Typeface("Consolas");
var text = new FormattedText(
stringValue,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
FontSize,
Brush(nameof(Theme.Data1)),
1.0);
var text = CreateText(stringValue, FontSize, Brush(nameof(Theme.Data1)));
var xOffset = CellTextOffset.X;
xOffset += HexContent.CellWidth / 2 * (integer.Length - 1); // adjust based on number of cells to use
@@ -178,16 +131,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations {
if (integerEnum.Position != 0) return;
var stringValue = integerEnum.Value;
var typeface = new Typeface("Consolas");
var text = new FormattedText(
stringValue,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
FontSize * 3 / 4,
Brush(nameof(Theme.Data2)),
1.0);
var text = CreateText(stringValue, FontSize * 3 / 4, Brush(nameof(Theme.Data2)));
var xOffset = CellTextOffset.X / 2;
context.PushClip(new RectangleGeometry(new Rect(0, 0, HexContent.CellWidth * integerEnum.Length, HexContent.CellHeight)));
@@ -195,6 +139,32 @@ namespace HavenSoft.HexManiac.WPF.Implementations {
context.Pop();
}
public void Visit(EggSection section, byte data) {
if (section.Position != 0) return;
var name = section.SectionName;
var text = CreateText(name, FontSize * 3 / 4, Brush(nameof(Theme.Stream1)));
var characterWidth = text.Width / name.Length;
var xOffset = HexContent.CellWidth - name.Length * characterWidth / 2;
if (xOffset < 0) xOffset = 0;
context.PushClip(new RectangleGeometry(new Rect(0, 0, HexContent.CellWidth * 2, HexContent.CellHeight)));
context.DrawText(text, new Point(xOffset, CellTextOffset.Y + 2));
context.Pop();
}
public void Visit(EggItem item, byte data) {
if (item.Position != 0) return;
var name = item.ItemName;
var text = CreateText(name, FontSize * 3 / 4, Brush(nameof(Theme.Stream2)));
var characterWidth = text.Width / name.Length;
var xOffset = HexContent.CellWidth - name.Length * characterWidth / 2;
if (xOffset < 0) xOffset = 0;
context.PushClip(new RectangleGeometry(new Rect(0, 0, HexContent.CellWidth * 2, HexContent.CellHeight)));
context.DrawText(text, new Point(xOffset, CellTextOffset.Y + 2));
context.Pop();
}
private void Underline(Brush brush, bool isStart, bool isEnd) {
int startPoint = isStart ? 5 : 0;
int endPoint = (int)HexContent.CellWidth - (isEnd ? 5 : 0);
@@ -227,6 +197,18 @@ namespace HavenSoft.HexManiac.WPF.Implementations {
noneVisualCache.AddRange(text);
}
private static readonly Typeface consolas = new Typeface("Consolas");
private static FormattedText CreateText(string text, double size, Brush color) {
return new FormattedText(
text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
consolas,
size,
color,
1.0);
}
private static SolidColorBrush Brush(string name) {
return (SolidColorBrush)Application.Current.Resources.MergedDictionaries[0][name];
}

View File

@@ -6,5 +6,5 @@
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyVersion("0.2.0.0")]
[assembly: AssemblyFileVersion("0.2.0.0")]
[assembly: AssemblyVersion("0.2.1.0")]
[assembly: AssemblyFileVersion("0.2.1.0")]