mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-10-02 21:26:34 -05:00
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
219
src/HexManiac.Core/Models/Runs/EggMoveRun.cs
Normal file
219
src/HexManiac.Core/Models/Runs/EggMoveRun.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
234
src/HexManiac.Tests/EggMoveTests.cs
Normal file
234
src/HexManiac.Tests/EggMoveTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user