HexManiacAdvance/src/HexManiac.Core/ViewModels/Visitors/CompleteCellEdit.cs
Benjamin Popp 7ae90ec14d Allow '+' on LZSpriteRun length to increase by 1 row
For certain images, increasing the height is allowed. It doesn't need to be easy, just possible.
* For an uncompressed image, changing the format will work to increase the size of the run.
* For compressed images, allow the user to type '+' on the length to make it grow by one row.
2021-02-18 22:54:20 -06:00

823 lines
38 KiB
C#

using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.Models.Runs.Sprites;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using static HavenSoft.HexManiac.Core.Models.Runs.PCSRun;
using static HavenSoft.HexManiac.Core.Models.Runs.PointerRun;
namespace HavenSoft.HexManiac.Core.ViewModels.Visitors {
internal class CompleteCellEdit : IDataFormatVisitor {
private readonly IDataModel Model;
private readonly int memoryLocation;
private readonly string CurrentText;
private readonly ModelDelta CurrentChange;
public bool Result { get; private set; } // if true, the edit was completed correctly
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, 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.
public CompleteCellEdit(IDataModel model, int memoryLocation, string currentText, ModelDelta currentChange) {
Model = model;
this.memoryLocation = memoryLocation;
CurrentText = currentText;
CurrentChange = currentChange;
NewDataIndex = memoryLocation;
}
public void Visit(Undefined dataFormat, byte data) => Visit((None)null, data);
public void Visit(None dataFormat, byte data) {
if (CurrentText.StartsWith(PointerStart.ToString())) {
if (CurrentText.Last() != PointerEnd && CurrentText.Last() != ' ') return;
CompletePointerEdit();
Result = true;
} else if (CurrentText.StartsWith("::")) {
if (CurrentText.Last() != ' ') return;
CompleteWordEdit();
Result = true;
} else if (CurrentText.StartsWith(":")) {
if (CurrentText.Last() != ' ') return;
CompleteNamedConstantEdit(2);
Result = true;
} else if (CurrentText.StartsWith(".")) {
if (CurrentText.Last() != ' ') return;
CompleteNamedConstantEdit(1);
Result = true;
} else {
if (CurrentText.Length < 2) return;
CompleteHexEdit(CurrentText);
Result = true;
}
}
public void Visit(UnderEdit dataFormat, byte data) => throw new NotImplementedException();
public void Visit(Pointer pointer, byte data) {
var run = Model.GetNextRun(memoryLocation);
if (run is ITableRun && CurrentText[0] != PointerStart) {
ErrorText = "Pointers in tables cannot be removed without removing the table.";
return;
}
Visit((None)null, data);
}
public void Visit(Anchor anchor, byte data) {
anchor.OriginalFormat.Visit(this, data);
if (NewCell != null) NewCell = new HexElement(NewCell, new Anchor(NewCell.Format, anchor.Name, anchor.Format, anchor.Sources));
}
public void Visit(SpriteDecorator sprite, byte data) => sprite.OriginalFormat.Visit(this, data);
public void Visit(PCS pcs, byte data) => VisitPCS(pcs);
private void VisitPCS(IDataFormatStreamInstance pcs) {
var currentText = CurrentText;
if (currentText.StartsWith(StringDelimeter.ToString())) currentText = currentText.Substring(1);
if (pcs.Position != 0 && CurrentText == StringDelimeter.ToString()) {
CompleteStringEdit();
Result = true;
} else if (pcs.Position == 0 && CurrentText == StringDelimeter.ToString() + StringDelimeter) {
CompleteStringEdit();
Result = true;
} else if (PCSString.PCS.Any(str => str == currentText)) {
CompleteCharacterEdit(pcs);
Result = true;
}
}
public void Visit(EscapedPCS pcs, byte data) {
if (CurrentText.Length < 2) return;
CompleteCharacterEdit(pcs);
Result = true;
}
public void Visit(ErrorPCS pcs, byte data) => VisitPCS(pcs);
public void Visit(Ascii ascii, byte data) {
CompleteAsciiEdit(ascii);
Result = true;
}
public void Visit(Integer integer, byte data) {
if (CurrentText == "+" && Model.GetNextRun(memoryLocation) is LzSpriteRun spriteRun) {
var newRun = spriteRun.IncreaseHeight(1, CurrentChange);
if (newRun.Start != spriteRun.Start) {
MessageText = $"Sprite was automatically moved to {newRun.Start:X6}. Pointers were updated.";
DataMoved = true;
NewDataIndex = newRun.Start + 1;
}
Result = true;
}
if (char.IsWhiteSpace(CurrentText.Last())) {
CompleteIntegerEdit(integer);
Result = true;
}
}
public void Visit(IntegerEnum integer, byte data) {
// must end in whitespace or must have matching quotation marks (ex. "Mr. Mime")
var quoteCount = CurrentText.Count(c => c == '"');
if (quoteCount == 0 && char.IsWhiteSpace(CurrentText.Last())) {
CompleteIntegerEnumEdit();
Result = true;
} else if (quoteCount == 2) {
CompleteIntegerEnumEdit();
Result = true;
}
}
public void Visit(IntegerHex integerHex, byte data) {
if (char.IsWhiteSpace(CurrentText.Last())) {
CompleteIntegerHexEdit(integerHex);
Result = true;
}
}
public void Visit(EggSection section, byte data) => CompleteEggEdit();
public void Visit(EggItem item, byte data) => CompleteEggEdit();
public void Visit(PlmItem item, byte data) {
var memoryLocation = this.memoryLocation;
var run = (PLMRun)Model.GetNextRun(memoryLocation);
// part 1: contraction (if they entered the end token)
if (CurrentText == EggMoveRun.GroupStart + EggMoveRun.GroupEnd) {
for (int i = this.memoryLocation; i < run.Start + run.Length; i += 2) Model.WriteMultiByteValue(i, 2, CurrentChange, 0xFFFF);
Model.ObserveRunWritten(CurrentChange, new PLMRun(Model, run.Start));
NewDataIndex = memoryLocation + 2;
Result = true;
return;
}
// part 2: validation
if (!CurrentText.Contains(" ")) return;
var quoteCount = CurrentText.Count(c => c == StringDelimeter);
if (quoteCount % 2 != 0) return;
if (!CurrentText.EndsWith(StringDelimeter.ToString()) && !CurrentText.EndsWith(" ")) return;
ErrorText = ValidatePlmText(run, quoteCount, out var level, out var move);
if (ErrorText != null || move == -1) return;
// part 3: write to the model
NewDataIndex = memoryLocation + 2;
Result = true;
var value = (level << 9) + move;
var initialItemValue = Model.ReadMultiByteValue(memoryLocation, 2);
Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value);
// part 4: expansion
if (initialItemValue == 0xFFFF) {
var newRun = Model.RelocateForExpansion(CurrentChange, run, run.Length + 2);
if (newRun.Start != run.Start) {
MessageText = $"Level Up Moves were automatically moved to {newRun.Start:X6}. Pointers were updated.";
memoryLocation += newRun.Start - run.Start;
NewDataIndex = memoryLocation + 2;
DataMoved = true;
}
Model.WriteMultiByteValue(memoryLocation + 2, 2, CurrentChange, 0xFFFF);
var lvlRun = new PLMRun(Model, newRun.Start);
Model.ObserveRunWritten(CurrentChange, lvlRun);
}
}
public void Visit(BitArray array, byte data) {
var currentText = CurrentText.Replace(" ", "");
if (currentText.All(ViewPort.AllHexCharacters.Contains) && currentText.Length == array.Length * 2) {
HandleHexChangeToBitArray(array, currentText);
return;
}
if (CurrentText.Equals("-")) {
HandleBitArrayClear(array);
return;
}
if (CurrentText.EndsWith(" ") && !CurrentText.StartsWith("\"")) {
currentText = CurrentText.Replace(" ", "");
if (!(currentText.All(ViewPort.AllHexCharacters.Contains) && (CurrentText.Count(c => c == ' ') > 1 || currentText.Length == 2))) {
HandleBitArrayEntry(CurrentText);
return;
}
}
if (CurrentText.StartsWith("\"") && CurrentText.Trim().EndsWith("\"") && CurrentText.Length > 1) {
HandleBitArrayEntry(CurrentText);
return;
}
if (CurrentText == "/") {
NewDataIndex = memoryLocation + array.Length;
Result = true;
}
}
public void Visit(MatchedWord word, byte data) => Visit((None)null, data);
public void Visit(EndStream endStream, byte data) {
// the only valid edit for an EndStream is to extend the stream.
Result = true;
var run = (TableStreamRun)Model.GetNextRun(memoryLocation);
var newRun = run.Append(CurrentChange, 1);
if (newRun.Start != run.Start) {
MessageText = $"Stream was automatically moved to {newRun.Start:X6}. Pointers were updated.";
NewDataIndex = memoryLocation + newRun.Start - run.Start;
DataMoved = true;
}
Model.ObserveRunWritten(CurrentChange, newRun);
}
public void Visit(LzMagicIdentifier lz, byte data) {
if (CurrentText.ToLower() == "lz") {
Result = true;
NewDataIndex = memoryLocation + 1;
}
}
public void Visit(LzGroupHeader lz, byte data) {
if (!CurrentText.EndsWith(" ")) return;
if (byte.TryParse(CurrentText, NumberStyles.HexNumber, CultureInfo.CurrentCulture.NumberFormat, out var result)) {
var oldValue = Model[memoryLocation];
CurrentChange.ChangeData(Model, memoryLocation, result);
var run = (LZRun)Model.GetNextRun(memoryLocation);
int runIndex = memoryLocation - run.Start;
Result = true;
if (!TryFixupLzRun(ref run, runIndex + 1)) {
CurrentChange.ChangeData(Model, memoryLocation, oldValue);
ErrorText = $"Could not write header {result:X2} without making the compressed data invalid.";
} else {
NewDataIndex = run.Start + runIndex + 1;
}
}
}
public void Visit(LzCompressed lz, byte data) {
if (!CurrentText.EndsWith(" ")) return;
var sections = CurrentText.Trim().Split(":");
if (sections.Length != 2) return;
if (!int.TryParse(sections[0], out int runLength)) return;
if (!int.TryParse(sections[1], out int runOffset)) return;
Result = true;
if (runLength < 3 || runLength > 18) {
ErrorText = "Run Length must be > 2 and < 19";
return;
} else if (runOffset < 1 || runOffset > 0x1000) {
ErrorText = "Run Offset must be > 0 and <= 4096";
return;
}
var result = LZRun.CompressedToken((byte)runLength, (short)runOffset);
var previousValue = Model.ReadMultiByteValue(memoryLocation, 2);
CurrentChange.ChangeData(Model, memoryLocation, result[0]);
CurrentChange.ChangeData(Model, memoryLocation + 1, result[1]);
var run = (LZRun)Model.GetNextRun(memoryLocation);
int runIndex = memoryLocation - run.Start;
if (!TryFixupLzRun(ref run, runIndex + 2)) {
Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, previousValue);
ErrorText = $"Could not write {runLength}:{runOffset} without making the compressed data invalid.";
} else {
NewDataIndex = run.Start + runIndex + 2;
}
}
public void Visit(LzUncompressed lz, byte data) {
if (!CurrentText.EndsWith(" ")) return;
if (byte.TryParse(CurrentText, NumberStyles.HexNumber, CultureInfo.CurrentCulture.NumberFormat, out var result)) {
CurrentChange.ChangeData(Model, memoryLocation, result);
Result = true;
NewDataIndex = memoryLocation + 1;
}
}
public void Visit(UncompressedPaletteColor color, byte data) {
if (!CurrentText.EndsWith(" ")) return;
if (CurrentText.Length < 4) return;
// option 1: 4 hex bytes
if (CurrentText.Length == 5 && short.TryParse(CurrentText.Trim(), NumberStyles.HexNumber, CultureInfo.CurrentCulture.NumberFormat, out var result)) {
Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, result);
NewDataIndex = memoryLocation + 2;
Result = true;
return;
}
// option 2: 2 hex bytes, a space, and 2 more hex bytes
if (CurrentText.Length == 6) {
if (byte.TryParse(CurrentText.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.CurrentCulture.NumberFormat, out var byte1)) {
if (byte.TryParse(CurrentText.Substring(3, 2), NumberStyles.HexNumber, CultureInfo.CurrentCulture.NumberFormat, out var byte2)) {
CurrentChange.ChangeData(Model, memoryLocation + 0, byte1);
CurrentChange.ChangeData(Model, memoryLocation + 1, byte2);
NewDataIndex = memoryLocation + 2;
Result = true;
return;
}
}
}
// option 3: 3 decimal values, separated by :
var channels = CurrentText.Trim().Split(':');
if (channels.Length == 3 &&
byte.TryParse(channels[0], out var red) &&
byte.TryParse(channels[1], out var green) &&
byte.TryParse(channels[2], out var blue)) {
var newColor = (short)((blue << 10) | (green << 5) | red);
Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, newColor);
NewDataIndex = memoryLocation + 2;
Result = true;
return;
}
// incomplete
if (CurrentText.Count(c => c == ' ') > 1) {
Result = true;
ErrorText = $"Could not parse {CurrentText} as a palette color.";
}
}
public void Visit(DataFormats.Tuple tuple, byte data) {
Result = CurrentText.EndsWith(")");
if (CurrentText.EndsWith(" ")) {
var tokens = CurrentText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList();
TableStreamRun.Recombine(tokens, "\"", "\"");
if (tokens.Count == tuple.Model.VisibleElementCount) Result = true;
}
if (Result) {
tuple.Model.Write(Model, CurrentChange, memoryLocation, CurrentText);
NewDataIndex = memoryLocation + tuple.Length;
}
}
/// <summary>
/// Parses text in a PLM run to get the level and move.
/// returns an error string if the parse fails.
/// </summary>
private string ValidatePlmText(PLMRun run, int quoteCount, out int level, out int move) {
(level, move) = (default, default);
if (quoteCount == 0) {
var split = CurrentText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (split.Length < 2) { move = -1; return null; } // user hasn't entered a move yet
if (!CurrentText.EndsWith(" ")) { move = -1; return null; } // user is still entering a move
if (!int.TryParse(split[0], out level) || level < 1 || level > PLMRun.MaxLearningLevel) {
return $"Could not parse '{split[0]}' as a pokemon level.";
} else if (!run.TryGetMoveNumber(split[1], out move)) {
return $"Could not parse {split[1]} as a pokemon move.";
}
} else {
var rawLevel = CurrentText.Substring(0, CurrentText.IndexOf(' '));
var rawMove = CurrentText.Substring(rawLevel.Length + 1);
if (!int.TryParse(rawLevel, out level) || level < 1 || level > PLMRun.MaxLearningLevel) {
return $"Could not parse '{rawLevel}' as a pokemon level.";
} else if (!run.TryGetMoveNumber(rawMove, out move)) {
return $"Could not parse {rawMove} as a pokemon move.";
}
}
return null;
}
private void CompleteIntegerHexEdit(IntegerHex integerHex) {
if(!int.TryParse(CurrentText,NumberStyles.HexNumber,CultureInfo.CurrentCulture,out var result)) {
ErrorText = $"Could not parse {CurrentText} as a number";
return;
}
Model.WriteMultiByteValue(integerHex.Source, integerHex.Length, CurrentChange, result);
if (result >= Math.Pow(2L, integerHex.Length * 8)) ErrorText = $"Warning: number was too big to fit in the available space.";
NewDataIndex = integerHex.Source + integerHex.Length;
}
private void CompleteIntegerEdit(Integer integer) {
if (!int.TryParse(CurrentText, out var result)) {
ErrorText = $"Could not parse {CurrentText} as a number";
return;
}
var run = Model.GetNextRun(memoryLocation);
if (run is WordRun wordRun1) {
var desiredValue = result - wordRun1.ValueOffset;
var maxValue = (int)Math.Pow(2, wordRun1.Length * 8) - 1;
if (desiredValue < 0 || desiredValue > maxValue) {
ErrorText = "Virtual value out of range!";
return;
}
}
Model.WriteMultiByteValue(integer.Source, integer.Length, CurrentChange, result);
if (result >= Math.Pow(2L, integer.Length * 8)) ErrorText = $"Warning: number was too big to fit in the available space.";
int runIndex = integer.Source - run.Start;
if (run is LZRun lzRun) {
TryFixupLzRun(ref lzRun, runIndex + integer.Length); // this is before the first header: it cannot fail.
run = lzRun;
}
var (newDataIndex, errorText) = UpdateAllWords(Model, run, CurrentChange, result, alsoUpdateArrays: true);
NewDataIndex = run.Start + runIndex + integer.Length;
if (newDataIndex >= 0) (NewDataIndex, ErrorText) = (newDataIndex, errorText);
}
public static (int, string) UpdateAllWords(IDataModel model, IFormattedRun run, ModelDelta token, int value, bool alsoUpdateArrays) {
int newDataIndex = -1;
string errorText = null;
if (run is WordRun wordRun) {
// update the other word runs with the same token name
var desiredValue = (value - wordRun.ValueOffset) / wordRun.MultOffset;
if (alsoUpdateArrays) {
foreach (var array in model.Arrays.Where(a => a.LengthFromAnchor == wordRun.SourceArrayName).ToList()) {
var delta = value - array.ElementCount;
var movedArray = model.RelocateForExpansion(token, array, (array.ElementCount + delta) * array.ElementLength);
var newArray = movedArray.Append(token, delta);
if (newArray != array) model.ObserveRunWritten(token, newArray);
}
}
foreach (var address in model.GetMatchedWords(wordRun.SourceArrayName)) {
if (address == run.Start) continue; // don't write the current run
if (!(model.GetNextRun(address) is WordRun currentRun)) continue;
var writeValue = desiredValue * currentRun.MultOffset + currentRun.ValueOffset;
var maxValue = (int)Math.Pow(2, currentRun.Length * 8) - 1;
if (writeValue < 0 || writeValue > maxValue) {
newDataIndex = currentRun.Start;
errorText = $"{currentRun.Start:X6}: value out of range!";
}
model.WriteMultiByteValue(address, currentRun.Length, token, writeValue);
}
}
return (newDataIndex, errorText);
}
private void CompleteIntegerEnumEdit() {
var array = (ITableRun)Model.GetNextRun(memoryLocation);
var offsets = array.ConvertByteOffsetToArrayOffset(memoryLocation);
var segment = (ArrayRunEnumSegment)array.ElementContent[offsets.SegmentIndex];
if (segment.TryParse(Model, CurrentText, out int value)) {
Model.WriteMultiByteValue(offsets.SegmentStart, segment.Length, CurrentChange, value);
NewDataIndex = offsets.SegmentStart + segment.Length;
} else {
ErrorText = $"Could not parse {CurrentText}as an enum from the {segment.EnumName} array";
}
}
private void CompleteAsciiEdit(Ascii asciiFormat) {
var content = (byte)CurrentText[0];
CurrentChange.ChangeData(Model, memoryLocation, content);
NewCell = new HexElement(content, true, new Ascii(asciiFormat.Source, asciiFormat.Position, CurrentText[0]));
NewDataIndex = memoryLocation + 1;
}
private void CompletePointerEdit() {
// if they just started a pointer and then clicked off, there's nothing to complete
if (CurrentText == PointerStart + " ") return;
var destination = CurrentText.Substring(1, CurrentText.Length - 2);
if (destination.Length == 2 && destination.All(ViewPort.AllHexCharacters.Contains)) { CompleteHexEdit(destination); return; }
Model.ExpandData(CurrentChange, memoryLocation + 3);
var currentRun = Model.GetNextRun(memoryLocation);
if (currentRun.Start > memoryLocation) currentRun = null;
bool inArray = currentRun is ITableRun && currentRun.Start <= memoryLocation;
var sources = currentRun?.PointerSources;
if (!inArray) {
if (destination != string.Empty) {
Model.ClearFormatAndData(CurrentChange, memoryLocation, 4);
sources = null;
} else if (!(currentRun is NoInfoRun)) {
Model.ClearFormat(CurrentChange, memoryLocation, 4);
sources = null;
}
}
int destinationValue;
int offset = 0;
if (destination == string.Empty) {
destinationValue = Model.ReadPointer(memoryLocation);
} else if (destination.All(ViewPort.AllHexCharacters.Contains) && destination.Length <= 7) {
while (destination.Length < 6) destination = "0" + destination;
destinationValue = int.Parse(destination, NumberStyles.HexNumber);
} else if (destination.Contains("+") && !destination.Contains("-")) {
var destinationParts = destination.Split("+");
if (!int.TryParse(destinationParts[0], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out destinationValue)) {
destinationValue = Model.GetAddressFromAnchor(CurrentChange, memoryLocation, destinationParts[0]);
}
if (!int.TryParse(destinationParts[1], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out offset)) {
ErrorText = $"Could not parse {destinationParts[0]}+{destinationParts[1]} into an address.";
}
} else if (destination.Contains("-") && !destination.Contains("+")) {
var destinationParts = destination.Split("-");
if (!int.TryParse(destinationParts[0], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out destinationValue)) {
destinationValue = Model.GetAddressFromAnchor(CurrentChange, memoryLocation, destinationParts[0]);
}
if (!int.TryParse(destinationParts[1], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out offset)) {
ErrorText = $"Could not parse {destinationParts[0]}-{destinationParts[1]} into an adress.";
}
offset = -offset;
} else {
destinationValue = Model.GetAddressFromAnchor(CurrentChange, memoryLocation, destination);
}
var fullValue = destinationValue + offset;
if (destinationValue == Pointer.NULL || (0 <= destinationValue && destinationValue < Model.Count)) {
if (inArray) {
UpdateArrayPointer((ITableRun)currentRun, destinationValue);
} else {
if (Model.ReadPointer(memoryLocation) != fullValue) {
Model.WritePointer(CurrentChange, memoryLocation, fullValue);
}
var newRun = new PointerRun(memoryLocation, sources);
if (offset != 0) newRun = new OffsetPointerRun(memoryLocation, offset, sources);
Model.ObserveRunWritten(CurrentChange, newRun);
}
NewDataIndex = memoryLocation + 4;
} else {
ErrorText = $"Address {destinationValue:X2} is not within the data.";
}
}
private void CompleteWordEdit() {
var parentName = CurrentText.Substring(2).Trim();
Model.ExpandData(CurrentChange, memoryLocation + 3);
Model.ClearFormat(CurrentChange, memoryLocation, 4);
CurrentChange.AddMatchedWord(Model, memoryLocation, parentName);
Model.ObserveRunWritten(CurrentChange, new WordRun(memoryLocation, parentName, 4, 0, 1));
NewDataIndex = memoryLocation + 4;
}
private void CompleteNamedConstantEdit(int byteCount) {
var constantName = CurrentText.Substring(1).Trim();
int offset = 0, multOffset = 1;
if (constantName.Contains("+")) {
var split = constantName.Split('+');
int.TryParse(split[1], out offset);
constantName = split[0];
}
if (constantName.Contains("-")) {
var split = constantName.Split('-');
int.TryParse(split[1], out offset);
constantName = split[0];
offset = -offset;
}
if (constantName.Contains("*")) {
var split = constantName.Split('*');
int.TryParse(split[1], out multOffset);
constantName = split[0];
if (multOffset < 1) {
ErrorText = $"Could not create {constantName} with multiplier {multOffset}: the multiplier must be positive.";
return;
}
}
var coreValue = (Model[memoryLocation] / multOffset) - offset;
var maxValue = Math.Pow(2, byteCount * 8) - 1;
if (coreValue < 0) {
if (offset != 0) {
ErrorText = $"Could not create {constantName} with offset {offset} because then the virtual value would be below 0.";
} else if (multOffset != 1) {
ErrorText = $"Could not create {constantName} with multiplier {multOffset} because then the virtual value would be below 0.";
}
} else if (coreValue > maxValue) {
if (offset != 0) {
ErrorText = $"Could not create {constantName} with offset {offset} because then the virtual value would be above {maxValue}.";
} else if (multOffset != 1) {
ErrorText = $"Could not create {constantName} with multiplier {offset} because then the virtual value would be above {maxValue}.";
}
} else {
CurrentChange.AddMatchedWord(Model, memoryLocation, constantName);
Model.ObserveRunWritten(CurrentChange, new WordRun(memoryLocation, constantName, byteCount, offset, multOffset));
NewDataIndex = memoryLocation;
}
}
private void CompleteStringEdit() {
int memoryLocation = this.memoryLocation;
// all the bytes are already correct, just move to the next space
var run = Model.GetNextRun(memoryLocation);
if (run is PCSRun) {
while (run.Start + run.Length > memoryLocation) {
CurrentChange.ChangeData(Model, memoryLocation, 0xFF);
memoryLocation++;
NewDataIndex = memoryLocation;
var newRunLength = PCSString.ReadString(Model, run.Start, true);
Model.ObserveRunWritten(CurrentChange, new PCSRun(Model, run.Start, newRunLength, run.PointerSources));
}
} else if (run is ITableRun arrayRun) {
var offsets = arrayRun.ConvertByteOffsetToArrayOffset(memoryLocation);
CurrentChange.ChangeData(Model, memoryLocation, 0xFF);
memoryLocation++;
NewDataIndex = memoryLocation;
while (offsets.SegmentStart + arrayRun.ElementContent[offsets.SegmentIndex].Length > memoryLocation) {
CurrentChange.ChangeData(Model, memoryLocation, 0x00);
memoryLocation++;
NewDataIndex = memoryLocation;
}
}
}
private void CompleteCharacterEdit(IDataFormat originalFormat) {
var editText = CurrentText;
if (editText.StartsWith("\"")) editText = editText.Substring(1);
var escaped = originalFormat as EscapedPCS;
var run = Model.GetNextRun(memoryLocation);
var byteValue = escaped != null ?
byte.Parse(CurrentText, NumberStyles.HexNumber) :
(byte)0x100.Range().First(i => PCSString.PCS[i] == editText);
var position = originalFormat is IDataFormatStreamInstance pcs ? pcs.Position : escaped.Position;
HandleLastCharacterChange(memoryLocation, editText, run, position, byteValue);
}
private void HandleLastCharacterChange(int memoryLocation, string editText, IFormattedRun run, int position, byte byteValue) {
if (run is PCSRun) {
// if its the last character being edited on a normal string, try to expand
if (run.Length == position + 1) {
int extraBytesNeeded = editText == "\\\\" ? 2 : 1;
// last character edit: might require relocation
var newRun = Model.RelocateForExpansion(CurrentChange, run, run.Length + extraBytesNeeded);
if (newRun != run) {
MessageText = $"Text was automatically moved to {newRun.Start:X6}. Pointers were updated.";
memoryLocation += newRun.Start - run.Start;
run = newRun;
DataMoved = true;
}
CurrentChange.ChangeData(Model, memoryLocation + 1, 0xFF);
if (editText == "\\\\") CurrentChange.ChangeData(Model, memoryLocation + 2, 0xFF);
run = new PCSRun(Model, run.Start, run.Length + extraBytesNeeded, run.PointerSources);
Model.ObserveRunWritten(CurrentChange, run);
}
} else if (run is ITableRun arrayRun) {
// if the last characet is being edited for an array, truncate
var offsets = arrayRun.ConvertByteOffsetToArrayOffset(memoryLocation);
if (arrayRun.ElementContent[offsets.SegmentIndex].Length == position + 1) {
memoryLocation--; // move back one byte and edit that one instead
} else if (Model[memoryLocation] == 0xFF) {
CurrentChange.ChangeData(Model, memoryLocation + 1, 0xFF); // overwrote the closing ", so add a new one after (since there's room)
}
} else {
Debug.Fail("Why are we completing a character edit on something other than a PCSRun or an Array?");
}
CurrentChange.ChangeData(Model, memoryLocation, byteValue);
NewDataIndex = memoryLocation + 1;
}
private void CompleteHexEdit(string currentText) {
var byteValue = byte.Parse(currentText, NumberStyles.HexNumber);
var run = Model.GetNextRun(memoryLocation);
if (!(run is NoInfoRun || run is IScriptStartRun) || run.Start != memoryLocation) Model.ClearFormat(CurrentChange, memoryLocation, 1);
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: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);
}
}
private void HandleHexChangeToBitArray(BitArray array, string currentText) {
var parseArray = new byte[array.Length];
for (int i = 0; i < array.Length; i++) {
if (!byte.TryParse(currentText.Substring(i * 2, 2), NumberStyles.HexNumber, CultureInfo.CurrentCulture.NumberFormat, out var result)) {
ErrorText = $"Could not parse {CurrentText} as a bit-array.";
return;
}
parseArray[i] = result;
}
for (int i = 0; i < array.Length; i++) CurrentChange.ChangeData(Model, memoryLocation + i, parseArray[i]);
NewDataIndex = memoryLocation + array.Length;
Result = true;
}
private void HandleBitArrayClear(BitArray array) {
for (int i = 0; i < array.Length; i++) CurrentChange.ChangeData(Model, memoryLocation + i, 0);
NewDataIndex = memoryLocation;
Result = true;
}
private void HandleBitArrayEntry(string currentText) {
currentText = currentText.Replace("\"", "").Trim();
var run = (ITableRun)Model.GetNextRun(memoryLocation);
var offset = run.ConvertByteOffsetToArrayOffset(memoryLocation);
var segment = (ArrayRunBitArraySegment)run.ElementContent[offset.SegmentIndex];
var sourceName = segment.SourceArrayName;
var options = Model.GetBitOptions(sourceName);
var bit = options.IndexOfPartial(currentText);
if (bit == -1) {
ErrorText = $"Could not parse {CurrentText} as a bit name.";
return;
}
var dataIndex = memoryLocation;
while (bit >= 8) { dataIndex++; bit -= 8; }
var newData = (byte)(Model[dataIndex] | 1 << bit);
CurrentChange.ChangeData(Model, dataIndex, newData);
NewDataIndex = memoryLocation;
Result = true;
}
private void UpdateArrayPointer(ITableRun run, int pointerDestination) {
var offsets = run.ConvertByteOffsetToArrayOffset(memoryLocation);
var segment = run.ElementContent[offsets.SegmentIndex];
if (segment is ArrayRunPointerSegment pointerSegment) {
if (!pointerSegment.DestinationDataMatchesPointerFormat(Model, CurrentChange, offsets.SegmentStart, pointerDestination, run.ElementContent, -1)) {
ErrorText = $"This pointer must point to {pointerSegment.InnerFormat} data.";
return;
}
}
Model.UpdateArrayPointer(CurrentChange, segment, run.ElementContent, offsets.ElementIndex, memoryLocation, pointerDestination);
}
private bool TryFixupLzRun(ref LZRun run, int runIndex) {
var initialStart = run.Start;
var newRun = run.FixupEnd(Model, CurrentChange, runIndex);
if (newRun == null) return false;
Model.ObserveRunWritten(CurrentChange, newRun);
if (newRun.Start != initialStart) MessageText = $"LZ Compressed data was automatically moved to {newRun.Start:X6}. Pointers were updated.";
run = newRun;
return true;
}
}
}