diff --git a/src/HexManiac.Core/HexManiac.Core.csproj b/src/HexManiac.Core/HexManiac.Core.csproj index 4b48dca3..597219fc 100644 --- a/src/HexManiac.Core/HexManiac.Core.csproj +++ b/src/HexManiac.Core/HexManiac.Core.csproj @@ -59,6 +59,7 @@ + diff --git a/src/HexManiac.Core/Models/AutoSearchModel.cs b/src/HexManiac.Core/Models/AutoSearchModel.cs index d5136de0..975d6fd7 100644 --- a/src/HexManiac.Core/Models/AutoSearchModel.cs +++ b/src/HexManiac.Core/Models/AutoSearchModel.cs @@ -110,10 +110,10 @@ namespace HavenSoft.HexManiac.Core.Models { var pokenames = GetNextRun(GetAddressFromAnchor(noChangeDelta, -1, EggMoveRun.PokemonNameTable)) as ArrayRun; if (pokenames != null && TrySearch(this, noChangeDelta, format, out var pokestatdata, run => run.PointerSources.Count > 5)) { if (pokestatdata.ElementCount < pokenames.ElementCount) { - pokenames = pokenames.Append(pokestatdata.ElementCount - pokenames.ElementCount); + pokenames = pokenames.Append(noChangeDelta, pokestatdata.ElementCount - pokenames.ElementCount); ObserveAnchorWritten(noChangeDelta, EggMoveRun.PokemonNameTable, pokenames); } else if (pokestatdata.ElementCount > pokenames.ElementCount) { - pokestatdata = pokestatdata.Append(pokenames.ElementCount - pokestatdata.ElementCount); + pokestatdata = pokestatdata.Append(noChangeDelta, pokenames.ElementCount - pokestatdata.ElementCount); } ObserveAnchorWritten(noChangeDelta, "pokestats", pokestatdata); diff --git a/src/HexManiac.Core/Models/HardcodeTablesModel.cs b/src/HexManiac.Core/Models/HardcodeTablesModel.cs index fbd36b1b..9d0d61c6 100644 --- a/src/HexManiac.Core/Models/HardcodeTablesModel.cs +++ b/src/HexManiac.Core/Models/HardcodeTablesModel.cs @@ -13,6 +13,7 @@ namespace HavenSoft.HexManiac.Core.Models { /// Lengths of some tables are still calculated dynamically based on best-fit, so operations like adding pokemon from a separate tool should still be picked up correctly. /// public class HardcodeTablesModel : PokemonModel { + public const string ItemsTableName = "items"; private readonly string gameCode; private readonly ModelDelta noChangeDelta = new NoDataChangeDeltaModel(); @@ -111,7 +112,7 @@ namespace HavenSoft.HexManiac.Core.Models { case FireRed: case LeafGreen: case Emerald: source = 0x0001BC; break; case Ruby: case Sapphire: source = 0x010B64; break; } - var format = $"[hp. attack. def. speed. spatk. spdef. type1.types type2.types catchRate. baseExp. evs: item1:items item2:items genderratio. steps2hatch. basehappiness. growthrate. egg1. egg2. ability1.abilitynames ability2.abilitynames runrate. unknown. padding:]{EggMoveRun.PokemonNameTable}"; + var format = $"[hp. attack. def. speed. spatk. spdef. type1.types type2.types catchRate. baseExp. evs: item1:{ItemsTableName} item2:{ItemsTableName} genderratio. steps2hatch. basehappiness. growthrate. egg1. egg2. ability1.abilitynames ability2.abilitynames runrate. unknown. padding:]{EggMoveRun.PokemonNameTable}"; AddTable(source, "pokestats", format); // items @@ -120,7 +121,7 @@ namespace HavenSoft.HexManiac.Core.Models { case Ruby: case Sapphire: source = 0x0A98F0; break; } format = $"[name\"\"14 index: price: holdeffect: description<{PCSRun.SharedFormatString}> keyitemvalue. bagkeyitem. pocket. type. fieldeffect<> battleusage:: battleeffect<> battleextra::]"; - AddTable(source, "items", format); + AddTable(source, ItemsTableName, format); // movedata switch (gameCode) { @@ -184,7 +185,7 @@ namespace HavenSoft.HexManiac.Core.Models { case Ruby: case Sapphire: source = -1; break; } source = GetNextRun(source).Start; - AddTable(source, "itemimages", "[image<> palette<>]items"); + AddTable(source, "itemimages", $"[image<> palette<>]{ItemsTableName}"); // trainer teams switch (gameCode) { @@ -192,7 +193,7 @@ namespace HavenSoft.HexManiac.Core.Models { case Emerald: source = 0x03587C; break; case Ruby: case Sapphire: source = 0x00D890; break; } - AddTable(source, "trainerdata", "[structType.4 class.trainerclassnames introMusic. sprite. name\"\"12 item1:items item2:items item3:items item4:items doubleBattle:: ai:: pokemonCount:: pokemon<>]"); + AddTable(source, "trainerdata", $"[structType.4 class.trainerclassnames introMusic. sprite. name\"\"12 item1:{ItemsTableName} item2:{ItemsTableName} item3:{ItemsTableName} item4:{ItemsTableName} doubleBattle:: ai:: pokemonCount:: pokemon<`tpt`>]"); } private void DecodeStreams() { @@ -223,7 +224,7 @@ namespace HavenSoft.HexManiac.Core.Models { if (interruptingRun.Start < destination && interruptingRun is ArrayRun array) { var elementLength = array.ElementLength; var elementCount = (destination - array.Start) / array.ElementLength; - array = array.Append(elementCount - array.ElementCount); + array = array.Append(noChangeDelta, elementCount - array.ElementCount); ObserveAnchorWritten(noChangeDelta, GetAnchorFromAddress(-1, array.Start), array); } diff --git a/src/HexManiac.Core/Models/IDataModel.cs b/src/HexManiac.Core/Models/IDataModel.cs index 6a66beb4..0b5e2932 100644 --- a/src/HexManiac.Core/Models/IDataModel.cs +++ b/src/HexManiac.Core/Models/IDataModel.cs @@ -30,7 +30,7 @@ namespace HavenSoft.HexManiac.Core.Models { bool TryGetUsefulHeader(int address, out string header); - bool IsAtEndOfArray(int dataIndex, out ArrayRun arrayRun); // is this byte the first one after the end of an array run? (also return true if the array is length 0 and starts right here) + bool IsAtEndOfArray(int dataIndex, out ITableRun tableRun); // is this byte the first one after the end of a table run? (also return true if the table is length 0 and starts right here) void ObserveRunWritten(ModelDelta changeToken, IFormattedRun run); void ObserveAnchorWritten(ModelDelta changeToken, string anchorName, IFormattedRun run); @@ -101,7 +101,7 @@ namespace HavenSoft.HexManiac.Core.Models { public virtual bool TryGetUsefulHeader(int address, out string header) { header = null; return false; } - public abstract bool IsAtEndOfArray(int dataIndex, out ArrayRun arrayRun); + public abstract bool IsAtEndOfArray(int dataIndex, out ITableRun tableRun); public virtual void Load(byte[] newData, StoredMetadata metadata) => RawData = newData; @@ -251,27 +251,30 @@ namespace HavenSoft.HexManiac.Core.Models { return format; } - public static ErrorInfo CompleteArrayExtension(this IDataModel model, ModelDelta changeToken, ref ArrayRun arrayRun) { - var currentArrayName = model.GetAnchorFromAddress(-1, arrayRun.Start); + public static ErrorInfo CompleteArrayExtension(this IDataModel model, ModelDelta changeToken, ref ITableRun table) { + var currentArrayName = model.GetAnchorFromAddress(-1, table.Start); - var visitedNames = new List() { model.GetAnchorFromAddress(-1, arrayRun.Start) }; - var visitedAddress = new List() { arrayRun.Start }; + var visitedNames = new List() { model.GetAnchorFromAddress(-1, table.Start) }; + var visitedAddress = new List() { table.Start }; - while (arrayRun.LengthFromAnchor != string.Empty) { - if (visitedNames.Contains(arrayRun.LengthFromAnchor)) { - // We kept going up the chain of tables but didn't find a top table. table length definitions are circular. - return new ErrorInfo($"Could not extend table safely. Table length has a circular dependency involving {arrayRun.LengthFromAnchor}."); + if (table is ArrayRun arrayRun) { + while (arrayRun.LengthFromAnchor != string.Empty) { + if (visitedNames.Contains(arrayRun.LengthFromAnchor)) { + // We kept going up the chain of tables but didn't find a top table. table length definitions are circular. + return new ErrorInfo($"Could not extend table safely. Table length has a circular dependency involving {arrayRun.LengthFromAnchor}."); + } + + var address = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, arrayRun.LengthFromAnchor); + visitedNames.Add(arrayRun.LengthFromAnchor); + visitedAddress.Add(address); + arrayRun = (ArrayRun)model.GetNextRun(address); } - - var address = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, arrayRun.LengthFromAnchor); - visitedNames.Add(arrayRun.LengthFromAnchor); - visitedAddress.Add(address); - arrayRun = (ArrayRun)model.GetNextRun(address); + table = arrayRun; } - ExtendArrayAndChildren(model, changeToken, arrayRun); + ExtendTableAndChildren(model, changeToken, table); - arrayRun = (ArrayRun)model.GetNextRun(model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, currentArrayName)); + table = (ITableRun)model.GetNextRun(model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, currentArrayName)); var changedNames = new List(); for (int i = 0; i < visitedNames.Count; i++) { @@ -284,9 +287,9 @@ namespace HavenSoft.HexManiac.Core.Models { return new ErrorInfo($"Tables {all} were moved. Pointers have been updated.", isWarningLevel: true); } - private static void ExtendArrayAndChildren(IDataModel model, ModelDelta changeToken, ArrayRun array) { - var newRun = (ArrayRun)model.RelocateForExpansion(changeToken, array, array.Length + array.ElementLength); - newRun = newRun.Append(1); + private static void ExtendTableAndChildren(IDataModel model, ModelDelta changeToken, ITableRun array) { + var newRun = (ITableRun)model.RelocateForExpansion(changeToken, array, array.Length + array.ElementLength); + newRun = newRun.Append(changeToken, 1); model.ObserveRunWritten(changeToken, newRun); } @@ -314,7 +317,7 @@ namespace HavenSoft.HexManiac.Core.Models { public override string GetAnchorFromAddress(int requestSource, int destination) => string.Empty; public override IFormattedRun GetNextRun(int dataIndex) => NoInfoRun.NullRun; public override IFormattedRun GetNextAnchor(int dataIndex) => NoInfoRun.NullRun; - public override bool IsAtEndOfArray(int dataIndex, out ArrayRun arrayRun) { arrayRun = null; return false; } + public override bool IsAtEndOfArray(int dataIndex, out ITableRun tableRun) { tableRun = null; return false; } public override void ObserveRunWritten(ModelDelta changeToken, IFormattedRun run) { } public override void ObserveAnchorWritten(ModelDelta changeToken, string anchorName, IFormattedRun run) { } public override void MassUpdateFromDelta(IReadOnlyDictionary runsToRemove, IReadOnlyDictionary runsToAdd, IReadOnlyDictionary namesToRemove, IReadOnlyDictionary namesToAdd, IReadOnlyDictionary unmappedPointersToRemove, IReadOnlyDictionary unmappedPointersToAdd, IReadOnlyDictionary matchedWordsToRemove, IReadOnlyDictionary matchedWordsToAdd) { } diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index e9b7a4eb..36970891 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -192,7 +192,7 @@ namespace HavenSoft.HexManiac.Core.Models { if (i == runs.Count - 1 || runs[i].Start + runs[i].Length <= runs[i + 1].Start) continue; var debugRunStart1 = runs[i].Start.ToString("X6"); var debugRunStart2 = runs[i + 1].Start.ToString("X6"); - Debug.Fail("Conflict: there's a run that ends before the next run starts!"); + Debug.Fail("Conflict: there's a run that ends after the next run starts!"); } } @@ -394,10 +394,10 @@ namespace HavenSoft.HexManiac.Core.Models { return true; } - public override bool IsAtEndOfArray(int dataIndex, out ArrayRun arrayRun) { + public override bool IsAtEndOfArray(int dataIndex, out ITableRun arrayRun) { var index = BinarySearch(dataIndex); if (index >= 0 && runs[index].Length == 0) { - arrayRun = runs[index] as ArrayRun; + arrayRun = runs[index] as ITableRun; return arrayRun != null; } @@ -409,7 +409,7 @@ namespace HavenSoft.HexManiac.Core.Models { return false; } - arrayRun = runs[index] as ArrayRun; + arrayRun = runs[index] as ITableRun; return arrayRun != null && runs[index].Start + runs[index].Length == dataIndex; } @@ -493,7 +493,7 @@ namespace HavenSoft.HexManiac.Core.Models { /// Remove the sources that match the array's original location. /// Add new sources corresponding to the array's new location. /// - private void UpdateAnchorsFromArrayMove(ModelDelta changeToken, ArrayRun original, ArrayRun moved) { + private void UpdateAnchorsFromArrayMove(ModelDelta changeToken, ITableRun original, ITableRun moved) { int originalOffset = original.Start; int segmentOffset = moved.Start; // i loops over the different segments in the array @@ -531,7 +531,7 @@ namespace HavenSoft.HexManiac.Core.Models { if (anchor.Equals(table.LengthFromAnchor)) { if (arrayRun.ElementCount == table.ElementCount) continue; newTable = (ArrayRun)RelocateForExpansion(changeToken, table, arrayRun.ElementCount * table.ElementLength); - newTable = newTable.Append(arrayRun.ElementCount - table.ElementCount); + newTable = newTable.Append(changeToken, arrayRun.ElementCount - table.ElementCount); ObserveRunWritten(changeToken, newTable); } // option 2: this table includes a bit-array based on the given table @@ -605,8 +605,8 @@ namespace HavenSoft.HexManiac.Core.Models { // the pointer points to a known normal anchor var existingRun = runs[index]; changeToken.RemoveRun(existingRun); - UpdateNewRunFromPointerFormat(ref existingRun, segment as ArrayRunPointerSegment, changeToken); existingRun = existingRun.MergeAnchor(new[] { start }); + UpdateNewRunFromPointerFormat(ref existingRun, segment as ArrayRunPointerSegment, changeToken); index = BinarySearch(destination); // runs could've been removed during UpdateNewRunFromPointerFormat: search for the index again. if (index < 0) { runs.Insert(~index, existingRun); @@ -636,6 +636,10 @@ namespace HavenSoft.HexManiac.Core.Models { run = runAttempt.MergeAnchor(run.PointerSources); ClearFormat(token, run.Start, run.Length); } + } else if (segment.InnerFormat == TrainerPokemonTeamRun.SharedFormatString) { + var runAttempt = new TrainerPokemonTeamRun(this, run.Start, run.PointerSources); + ClearFormat(token, run.Start, runAttempt.Length); + run = runAttempt; } else { throw new NotImplementedException(); } @@ -1249,6 +1253,8 @@ namespace HavenSoft.HexManiac.Core.Models { private static ErrorInfo TryParseFormat(IDataModel model, string format, int dataIndex, out IFormattedRun run) { run = new NoInfoRun(dataIndex); + var existingRun = model.GetNextRun(dataIndex); + if (existingRun.Start == run.Start) run = run.MergeAnchor(existingRun.PointerSources); if (format == PCSRun.SharedFormatString) { var length = PCSString.ReadString(model, dataIndex, true); @@ -1269,6 +1275,8 @@ namespace HavenSoft.HexManiac.Core.Models { } else if (format == PLMRun.SharedFormatString) { run = new PLMRun(model, dataIndex); if (run.Length == 0) return new ErrorInfo("Format specified was for pokemon level-up move data, but could not parse that location as level-up move data."); + } else if (format == TrainerPokemonTeamRun.SharedFormatString) { + run = new TrainerPokemonTeamRun(model, dataIndex, run.PointerSources); } else { var errorInfo = TryParse(model, format, dataIndex, null, out var arrayRun); if (errorInfo == ErrorInfo.NoError) { @@ -1368,19 +1376,9 @@ namespace HavenSoft.HexManiac.Core.Models { } // move run - IFormattedRun newRun; - if (run is PCSRun pcs) { - newRun = new PCSRun(this, newStart, run.Length, run.PointerSources); - } else if (run is ArrayRun array) { - var array1 = array.Move(newStart); - UpdateAnchorsFromArrayMove(changeToken, array, array1); - newRun = array1; - } else if (run is EggMoveRun egg) { - newRun = new EggMoveRun(this, newStart); - } else if (run is PLMRun plm) { - newRun = new PLMRun(this, newStart); - } else { - throw new NotImplementedException(); + var newRun = run.Duplicate(newStart, run.PointerSources.ToArray()); + if (newRun is ITableRun array) { + UpdateAnchorsFromArrayMove(changeToken, (ITableRun)run, array); } int index = BinarySearch(run.Start); diff --git a/src/HexManiac.Core/Models/Runs/ArrayRun.cs b/src/HexManiac.Core/Models/Runs/ArrayRun.cs index 4958d3b7..f4af0496 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRun.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRun.cs @@ -5,6 +5,54 @@ using System.Linq; using System.Text; namespace HavenSoft.HexManiac.Core.Models.Runs { + public interface ITableRun : IFormattedRun { + int ElementCount { get; } + int ElementLength { get; } + IReadOnlyList ElementNames { get; } + IReadOnlyList ElementContent { get; } + ITableRun Append(ModelDelta token, int length); + } + + public static class ITableRunExtensions { + public static ArrayOffset ConvertByteOffsetToArrayOffset(this ITableRun self, int byteOffset) { + var offset = byteOffset - self.Start; + int elementIndex = offset / self.ElementLength; + int elementOffset = offset % self.ElementLength; + int segmentIndex = 0, segmentOffset = elementOffset; + while (self.ElementContent[segmentIndex].Length <= segmentOffset) { + segmentOffset -= self.ElementContent[segmentIndex].Length; segmentIndex++; + } + return new ArrayOffset(elementIndex, segmentIndex, byteOffset - segmentOffset, segmentOffset); + } + + public static IDataFormat CreateSegmentDataFormat(this ITableRun self, IDataModel data, int index) { + var offsets = self.ConvertByteOffsetToArrayOffset(index); + var currentSegment = self.ElementContent[offsets.SegmentIndex]; + var position = index - offsets.SegmentStart; + if (currentSegment.Type == ElementContentType.Integer) { + if (currentSegment is ArrayRunEnumSegment enumSegment) { + var value = enumSegment.ToText(data, index); + return new IntegerEnum(offsets.SegmentStart, position, value, currentSegment.Length); + } else { + var value = ArrayRunElementSegment.ToInteger(data, offsets.SegmentStart, currentSegment.Length); + return new Integer(offsets.SegmentStart, position, value, currentSegment.Length); + } + } + + if (currentSegment.Type == ElementContentType.Pointer) { + var destination = data.ReadPointer(offsets.SegmentStart); + var destinationName = data.GetAnchorFromAddress(offsets.SegmentStart, destination); + return new Pointer(offsets.SegmentStart, position, destination, destinationName); + } + + if (currentSegment.Type == ElementContentType.BitArray) { + return new BitArray(offsets.SegmentStart, position, currentSegment.Length); + } + + throw new NotImplementedException(); + } + } + public class ArrayOffset { /// /// Ranges from 0 to ElementCount @@ -30,7 +78,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } } - public class ArrayRun : BaseRun { + public class ArrayRun : BaseRun, ITableRun { public const char ExtendArray = '+'; public const char ArrayStart = '['; public const char ArrayEnd = ']'; @@ -296,7 +344,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { private string cachedCurrentString; private int currentCachedStartIndex = -1, currentCachedIndex = -1; public override IDataFormat CreateDataFormat(IDataModel data, int index) { - var offsets = ConvertByteOffsetToArrayOffset(index); + var offsets = this.ConvertByteOffsetToArrayOffset(index); var currentSegment = ElementContent[offsets.SegmentIndex]; if (currentSegment.Type == ElementContentType.PCS) { if (currentCachedStartIndex != offsets.SegmentStart || currentCachedIndex > offsets.SegmentOffset) { @@ -308,42 +356,11 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { return PCSRun.CreatePCSFormat(data, offsets.SegmentStart, index, cachedCurrentString); } - var position = index - offsets.SegmentStart; - if (currentSegment.Type == ElementContentType.Integer) { - if (currentSegment is ArrayRunEnumSegment enumSegment) { - var value = enumSegment.ToText(data, index); - return new IntegerEnum(offsets.SegmentStart, position, value, currentSegment.Length); - } else { - var value = ArrayRunElementSegment.ToInteger(data, offsets.SegmentStart, currentSegment.Length); - return new Integer(offsets.SegmentStart, position, value, currentSegment.Length); - } - } - - if (currentSegment.Type == ElementContentType.Pointer) { - var destination = data.ReadPointer(offsets.SegmentStart); - var destinationName = data.GetAnchorFromAddress(offsets.SegmentStart, destination); - return new Pointer(offsets.SegmentStart, position, destination, destinationName); - } - - if (currentSegment.Type == ElementContentType.BitArray) { - return new BitArray(offsets.SegmentStart, position, currentSegment.Length); - } - - throw new NotImplementedException(); + return this.CreateSegmentDataFormat(data, index); } - public ArrayOffset ConvertByteOffsetToArrayOffset(int byteOffset) { - var offset = byteOffset - Start; - int elementIndex = offset / ElementLength; - int elementOffset = offset % ElementLength; - int segmentIndex = 0, segmentOffset = elementOffset; - while (ElementContent[segmentIndex].Length <= segmentOffset) { - segmentOffset -= ElementContent[segmentIndex].Length; segmentIndex++; - } - return new ArrayOffset(elementIndex, segmentIndex, byteOffset - segmentOffset, segmentOffset); - } - - public ArrayRun Append(int elementCount) { + ITableRun ITableRun.Append(ModelDelta token, int elementCount) => Append(token, elementCount); + public ArrayRun Append(ModelDelta token, int elementCount) { var lastArrayCharacterIndex = FormatString.LastIndexOf(ArrayEnd); var newFormat = FormatString.Substring(0, lastArrayCharacterIndex + 1); if (newFormat != FormatString) newFormat += ElementCount + elementCount; @@ -429,7 +446,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } public void AppendTo(IDataModel data, StringBuilder text, int start, int length) { - var offsets = ConvertByteOffsetToArrayOffset(start); + var offsets = this.ConvertByteOffsetToArrayOffset(start); length += offsets.SegmentOffset; for (int i = offsets.ElementIndex; i < ElementCount && length > 0; i++) { var offset = offsets.SegmentStart; @@ -643,7 +660,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (destination == Pointer.NULL) return true; if (0 > destination || destination > owner.Count) return false; if (segment is ArrayRunPointerSegment pointerSegment) { - if (!pointerSegment.DestinationDataMatchesPointerFormat(owner, new NoDataChangeDeltaModel(), destination)) return false; + if (!pointerSegment.DestinationDataMatchesPointerFormat(owner, new NoDataChangeDeltaModel(), start, destination)) return false; } return true; case ElementContentType.BitArray: diff --git a/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs b/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs index d6511f19..c37b56ce 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs @@ -157,6 +157,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { get { if (InnerFormat == PCSRun.SharedFormatString) return true; if (InnerFormat == PLMRun.SharedFormatString) return true; + if (InnerFormat == TrainerPokemonTeamRun.SharedFormatString) return true; return false; } } @@ -165,7 +166,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { InnerFormat = innerFormat; } - public bool DestinationDataMatchesPointerFormat(IDataModel owner, ModelDelta token, int destination) { + public bool DestinationDataMatchesPointerFormat(IDataModel owner, ModelDelta token, int source, int destination) { if (destination == Pointer.NULL) return true; var run = owner.GetNextAnchor(destination); if (run.Start < destination) return false; @@ -187,11 +188,19 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (!(token is NoDataChangeDeltaModel)) owner.ObserveRunWritten(token, plmRun); return true; } + } else if (InnerFormat == TrainerPokemonTeamRun.SharedFormatString) { + var teamRun = new TrainerPokemonTeamRun(owner, destination, new[] { source }); + var length = teamRun.Length; + if (length >= 2) { + if (!(token is NoDataChangeDeltaModel)) owner.ObserveRunWritten(token, teamRun); + return true; + } } } else { // easy case: already have a useful format, just see if it matches if (InnerFormat == PCSRun.SharedFormatString) return run is PCSRun; if (InnerFormat == PLMRun.SharedFormatString) return run is PLMRun; + if (InnerFormat == TrainerPokemonTeamRun.SharedFormatString) return run is TrainerPokemonTeamRun; } return false; } diff --git a/src/HexManiac.Core/Models/Runs/TrainerPokemonTeamRun.cs b/src/HexManiac.Core/Models/Runs/TrainerPokemonTeamRun.cs new file mode 100644 index 00000000..f13a3e29 --- /dev/null +++ b/src/HexManiac.Core/Models/Runs/TrainerPokemonTeamRun.cs @@ -0,0 +1,285 @@ +using HavenSoft.HexManiac.Core.ViewModels.DataFormats; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace HavenSoft.HexManiac.Core.Models.Runs { + public class TrainerPokemonTeamRun : BaseRun, IStreamRun, ITableRun { + public const int TrainerFormat_StructTypeOffset = 0; + public const int TrainerFormat_PokemonCountOffset = 32; + public const int TrainerFormat_PointerOffset = 36; + public const byte INCLUDE_MOVES = 1; + public const byte INCLUDE_ITEM = 2; + + public const int PokemonFormat_FixedIVStart = 0; + public const int PokemonFormat_LevelStart = 2; + public const int PokemonFormat_PokemonStart = 4; + public const int PokemonFormat_MoveStart = 6; + + public static readonly string SharedFormatString = AsciiRun.StreamDelimeter + "tpt" + AsciiRun.StreamDelimeter; + + private readonly IDataModel model; + + public byte StructType { get; } + + public TrainerPokemonTeamRun(IDataModel model, int start, IReadOnlyList sources) : base(start, sources) { + this.model = model; + + // trainer format (abbreviated): + // 0 1 2 3 4-15 16 18 20 22 24 28 32 36 40 total + // [structType. class. introMusic. sprite. name\"\"12 item1: item2: item3: item4: doubleBattle:: ai:: pokemonCount:: pokemon<>] + if (sources.Count < 1) throw new ArgumentException("Trainer Pokemon Team streams must be pointed to!"); + var source = sources.First(); + StructType = model[source - TrainerFormat_PointerOffset]; + ElementCount = model[source - TrainerFormat_PointerOffset + TrainerFormat_PokemonCountOffset]; + + var segments = new List(); + segments.Add(new ArrayRunElementSegment("ivSpread", ElementContentType.Integer, 2)); + segments.Add(new ArrayRunElementSegment("level", ElementContentType.Integer, 2)); + segments.Add(new ArrayRunEnumSegment("mon", 2, EggMoveRun.PokemonNameTable)); + if ((StructType & INCLUDE_MOVES) != 0) { + segments.Add(new ArrayRunEnumSegment("move1", 2, EggMoveRun.MoveNamesTable)); + segments.Add(new ArrayRunEnumSegment("move2", 2, EggMoveRun.MoveNamesTable)); + segments.Add(new ArrayRunEnumSegment("move3", 2, EggMoveRun.MoveNamesTable)); + segments.Add(new ArrayRunEnumSegment("move4", 2, EggMoveRun.MoveNamesTable)); + } + if ((StructType & INCLUDE_ITEM) != 0) { + segments.Add(new ArrayRunEnumSegment("item", 2, HardcodeTablesModel.ItemsTableName)); + } else { + segments.Add(new ArrayRunElementSegment("padding", ElementContentType.Integer, 2)); + } + + ElementContent = segments; + ElementLength = ElementContent.Sum(segment => segment.Length); + } + + public IEnumerable Search(string parentArrayName, int id) { + for (int i = 0; i < ElementCount; i++) { + int start = Start + i * ElementLength; + if (parentArrayName == EggMoveRun.MoveNamesTable && (StructType & INCLUDE_MOVES) != 0) { + for (int j = 0; j < 4; j++) { + var index = start + PokemonFormat_MoveStart + j * 2; + var moveID = model.ReadMultiByteValue(index, 2); + if (moveID == id) yield return index; + } + } else if (parentArrayName == EggMoveRun.PokemonNameTable) { + var index = start + PokemonFormat_PokemonStart; + var pokemonID = model.ReadMultiByteValue(index, 2); + if (pokemonID == id) yield return index; + } else if (parentArrayName == HardcodeTablesModel.ItemsTableName && (StructType & INCLUDE_ITEM) != 0) { + var index = start + ElementLength - 2; + var itemID = model.ReadMultiByteValue(index, 2); + if (itemID == id) yield return index; + } + } + } + + #region BaseRun + + public override int Length => ElementLength * ElementCount; + + public override string FormatString => SharedFormatString; + + public override IDataFormat CreateDataFormat(IDataModel data, int index) => this.CreateSegmentDataFormat(data, index); + + protected override BaseRun Clone(IReadOnlyList newPointerSources) => new TrainerPokemonTeamRun(model, Start, newPointerSources); + + #endregion + + #region ITableRun + + public int ElementCount { get; } + + public int ElementLength { get; } + + public IReadOnlyList ElementNames { get; } = new List(); + + public IReadOnlyList ElementContent { get; } + + public ITableRun Append(ModelDelta token, int length) { + var totalLength = ElementLength * (ElementCount + length); + var workingRun = this; + if (totalLength > workingRun.Length) workingRun = (TrainerPokemonTeamRun)model.RelocateForExpansion(token, workingRun, totalLength); + + // delete old elements + for (int i = -1; i >= length; i--) { + var start = workingRun.Start + workingRun.Length + i * ElementLength; + for (int j = 0; j < ElementLength; j++) token.ChangeData(model, start + j, 0xFF); + } + + // add new elements + for (int i = 0; i < length; i++) { + var start = workingRun.Start + workingRun.Length + i * ElementLength; + for (int j = 0; j < ElementLength; j++) token.ChangeData(model, start + j, 0x00); + } + + // update parent + var parent = workingRun.PointerSources[0] - TrainerFormat_PointerOffset; + model.WriteMultiByteValue(parent + TrainerFormat_PokemonCountOffset, 4, token, ElementCount + length); + return new TrainerPokemonTeamRun(model, workingRun.Start, workingRun.PointerSources); + } + + #endregion + + #region IStreamRun + + // example serialized pokemon: + // + // 50 Butterfree (31)@"Silk Powder" + // - "Stun Spore" + // - "Super Sonic" + // - "Aerial Ace" + // - "Silver Wind" + + public IStreamRun DeserializeRun(string content, ModelDelta token) { + var lines = content.Split(Environment.NewLine).Select(line => line.Trim()).ToArray(); + + // step 1: parse it into some data containers + var data = new TeamData(ModelCacheScope.GetCache(model), lines); + + // step 2: figure out what I need based on the data + var elementLength = data.MovesIncluded ? 16 : 8; + var totalLength = elementLength * data.Pokemon.Count; + var workingRun = this; + if (totalLength > workingRun.Length) workingRun = (TrainerPokemonTeamRun)model.RelocateForExpansion(token, workingRun, totalLength); + + // step 3: write the run data + for (int i = 0; i < data.Pokemon.Count; i++) { + int start = workingRun.Start + elementLength * i; + model.WriteMultiByteValue(start + 0, 2, token, data.Levels[i]); + model.WriteMultiByteValue(start + 2, 2, token, data.Pokemon[i]); + model.WriteMultiByteValue(start + 4, 2, token, data.IVs[i]); + start += 6; + if (data.MovesIncluded) { + for (int j = 0; j < 4; j++) model.WriteMultiByteValue(start + j * 2, 2, token, data.Moves[i * 4 + j]); + start += 8; + } + model.WriteMultiByteValue(start, 2, token, data.Items[i]); + } + + // step 4: write the parent data + var structType = (data.ItemsIncluded ? INCLUDE_ITEM : 0) ^ (data.MovesIncluded ? INCLUDE_MOVES : 0); + var parent = workingRun.PointerSources[0] - TrainerFormat_PointerOffset; + model.WriteMultiByteValue(parent + TrainerFormat_StructTypeOffset, 1, token, structType); + model.WriteMultiByteValue(parent + TrainerFormat_PokemonCountOffset, 4, token, data.Pokemon.Count); + + return new TrainerPokemonTeamRun(model, workingRun.Start, workingRun.PointerSources); + } + + public string SerializeRun() { + var cache = ModelCacheScope.GetCache(model); + var buffer = new StringBuilder(); + for (int i = 0; i < ElementCount; i++) { + var start = Start + ElementLength * i; + var ivSpread = model.ReadMultiByteValue(start + 0, 2) * 31 / 255; + var level = model.ReadMultiByteValue(start + 2, 2); + var pokeID = model.ReadMultiByteValue(start + 4, 2); + var pokemonNames = cache.GetOptions(EggMoveRun.PokemonNameTable); + var pokemon = pokemonNames.Count > pokeID ? pokemonNames[pokeID] : pokeID.ToString(); + var item = string.Empty; + if ((StructType & INCLUDE_ITEM) != 0) { + var itemID = model.ReadMultiByteValue(start + ElementLength - 2, 2); + var itemNames = cache.GetOptions(HardcodeTablesModel.ItemsTableName); + item = itemNames.Count > itemID ? itemNames[itemID] : itemID.ToString(); + item = "@" + item; + } + buffer.AppendLine($"{level} {pokemon} ({ivSpread}){item}"); + if ((StructType & INCLUDE_MOVES) != 0) { + var moveNames = cache.GetOptions(EggMoveRun.MoveNamesTable); + for(int j = 0; j < 4; j++) { + var moveID = model.ReadMultiByteValue(start + 6 + j * 2, 2); + var move = moveNames.Count > moveID ? moveNames[moveID] : moveID.ToString(); + buffer.AppendLine($"- {move}"); + } + } + if (i + 1 < ElementCount) buffer.AppendLine(); + } + return buffer.ToString(); + } + + private class TeamData { + private readonly List levels = new List(); + private readonly List pokemons = new List(); + private readonly List ivs = new List(); + private readonly List items = new List(); + private readonly List moves = new List(); + + public bool ItemsIncluded { get; private set; } + public bool MovesIncluded { get; private set; } + + public IReadOnlyList Levels => levels; + public IReadOnlyList Pokemon => pokemons; + public IReadOnlyList IVs => ivs; + public IReadOnlyList Items => items; + public IReadOnlyList Moves => moves; + + public TeamData(ModelCacheScope cache, string[] lines) { + var currentPokemonMoveCount = 0; + var moveNames = cache.GetOptions(EggMoveRun.MoveNamesTable); + var itemNames = cache.GetOptions(HardcodeTablesModel.ItemsTableName); + var pokemonNames = cache.GetOptions(EggMoveRun.PokemonNameTable); + + foreach (var line in lines) { + if (line is "") continue; + if (line.StartsWith("-")) { + if (pokemons.Count == 0) continue; + if (currentPokemonMoveCount > 3) continue; + MovesIncluded = true; + var move = line.Substring(1).Trim(); + var moveIndex = moveNames.IndexOfPartial(move); + if (moveIndex < 0) moveIndex = 0; + moves[(pokemons.Count - 1) * 4 + currentPokemonMoveCount] = moveIndex; + currentPokemonMoveCount++; + } else { + if (pokemons.Count == 6) continue; + + var levelTokenized = line.Split(new[] { ' ' }, 2); + if (levelTokenized.Length != 2) continue; + if (!int.TryParse(levelTokenized[0], out int level)) continue; + levels.Add(level); + + var itemTokenized = line.Split(new[] { '@' }, 2); + AddItem(itemNames, items, itemTokenized); + + var ivTokenized = line.Split(new[] { '(' }, 2); + AddIV(ivs, ivTokenized); + + var pokemon = ivTokenized[0].Trim(); + var pokemonIndex = pokemonNames.IndexOfPartial(pokemon); + if (pokemonIndex < 0) pokemonIndex = 0; + pokemons.Add(pokemonIndex); + moves.AddRange(new[] { 0, 0, 0, 0 }); + currentPokemonMoveCount = 0; + } + } + } + + private void AddIV(List ivs, string[] ivTokenized) { + if (ivTokenized.Length == 2) { + ivTokenized[1] = ivTokenized[1].Replace(")", "").Trim(); + if (int.TryParse(ivTokenized[1], out int fixedIV)) { + ivs.Add(fixedIV * 255 / 31); + } else { + ivs.Add(0); + } + } else { + ivs.Add(0); + } + } + + private void AddItem(IReadOnlyList itemNames, List items, string[] itemTokenized) { + if (itemTokenized.Length == 2) { + ItemsIncluded = true; + var itemIndex = itemNames.IndexOfPartial(itemTokenized[1]); + if (itemIndex < 0) itemIndex = 0; + items.Add(itemIndex); + } else { + items.Add(0); + } + } + } + + #endregion + } +} diff --git a/src/HexManiac.Core/SystemExtensions.cs b/src/HexManiac.Core/SystemExtensions.cs index 503ca170..cf836a43 100644 --- a/src/HexManiac.Core/SystemExtensions.cs +++ b/src/HexManiac.Core/SystemExtensions.cs @@ -44,7 +44,7 @@ namespace HavenSoft.HexManiac.Core { return true; } - public static int IndexOfPartial(this IList names, string input) { + public static int IndexOfPartial(this IReadOnlyList names, string input) { // perfect match first var matchIndex = names.IndexOf(input); if (matchIndex != -1) return matchIndex; diff --git a/src/HexManiac.Core/ViewModels/QuickEditItems/MakeTmsExpandable.cs b/src/HexManiac.Core/ViewModels/QuickEditItems/MakeTmsExpandable.cs index 5cc4a512..af1f29b1 100644 --- a/src/HexManiac.Core/ViewModels/QuickEditItems/MakeTmsExpandable.cs +++ b/src/HexManiac.Core/ViewModels/QuickEditItems/MakeTmsExpandable.cs @@ -201,7 +201,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.QuickEditItems { // clear HMs from the TmMove table table = (ArrayRun)model.GetNextRun(tmMovesAddress); - table = table.Append(-8); + table = table.Append(token, -8); model.ObserveAnchorWritten(token, TmMoves, table); for (int i = 0; i < 8; i++) model.WriteMultiByteValue(table.Start + table.Length + i * 2, 2, token, 0); diff --git a/src/HexManiac.Core/ViewModels/Tools/IArrayElementViewModel.cs b/src/HexManiac.Core/ViewModels/Tools/IArrayElementViewModel.cs index ce343fc3..1f9e34e0 100644 --- a/src/HexManiac.Core/ViewModels/Tools/IArrayElementViewModel.cs +++ b/src/HexManiac.Core/ViewModels/Tools/IArrayElementViewModel.cs @@ -201,7 +201,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { get => selectedIndex; set { if (!TryUpdate(ref selectedIndex, value)) return; - var run = (ArrayRun)Model.GetNextRun(Start); + var run = (ITableRun)Model.GetNextRun(Start); var offsets = run.ConvertByteOffsetToArrayOffset(Start); var segment = (ArrayRunEnumSegment)run.ElementContent[offsets.SegmentIndex]; @@ -217,7 +217,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { public ComboBoxArrayElementViewModel(Selection selection, ChangeHistory history, IDataModel model, string name, int start, int length) { (this.history, Model, Name, Start, Length) = (history, model, name, start, length); - var run = (ArrayRun)Model.GetNextRun(Start); + var run = (ITableRun)Model.GetNextRun(Start); var offsets = run.ConvertByteOffsetToArrayOffset(start); var segment = (ArrayRunEnumSegment)run.ElementContent[offsets.SegmentIndex]; var optionSource = model.GetAddressFromAnchor(history.CurrentChange, -1, segment.EnumName); @@ -330,7 +330,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { Name = name; this.start = start; - var array = (ArrayRun)model.GetNextRun(start); + var array = (ITableRun)model.GetNextRun(start); var offset = array.ConvertByteOffsetToArrayOffset(start); segment = (ArrayRunBitArraySegment)array.ElementContent[offset.SegmentIndex]; var optionSource = model.GetAddressFromAnchor(history.CurrentChange, -1, segment.SourceArrayName); diff --git a/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs b/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs index 07cd3468..585545da 100644 --- a/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs +++ b/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs @@ -292,7 +292,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { } if (newRun.ElementCount != lines.Length) { - newRun = newRun.Append(lines.Length - newRun.ElementCount); + newRun = newRun.Append(history.CurrentChange, lines.Length - newRun.ElementCount); model.ObserveRunWritten(history.CurrentChange, newRun); history.CurrentChange.AddRun(newRun); } diff --git a/src/HexManiac.Core/ViewModels/Tools/TableTool.cs b/src/HexManiac.Core/ViewModels/Tools/TableTool.cs index aa81c0d4..a26d3f8c 100644 --- a/src/HexManiac.Core/ViewModels/Tools/TableTool.cs +++ b/src/HexManiac.Core/ViewModels/Tools/TableTool.cs @@ -58,7 +58,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { set { if (TryUpdate(ref address, value)) { var run = model.GetNextRun(value); - if (run.Start > value || !(run is ArrayRun array)) { + if (run.Start > value || !(run is ITableRun)) { Enabled = false; CommandCanExecuteChanged(); return; @@ -95,11 +95,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { previous = new StubCommand { CanExecute = parameter => { - var array = model.GetNextRun(address) as ArrayRun; + var array = model.GetNextRun(address) as ITableRun; return array != null && array.Start < address; }, Execute = parameter => { - var array = (ArrayRun)model.GetNextRun(address); + var array = (ITableRun)model.GetNextRun(address); selection.SelectionStart = selection.Scroll.DataIndexToViewPoint(Address - array.ElementLength); selection.SelectionEnd = selection.Scroll.DataIndexToViewPoint(selection.Scroll.ViewPointToDataIndex(selection.SelectionStart) + array.ElementLength - 1); } @@ -107,11 +107,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { next = new StubCommand { CanExecute = parameter => { - var array = model.GetNextRun(address) as ArrayRun; + var array = model.GetNextRun(address) as ITableRun; return array != null && array.Start + array.Length > address + array.ElementLength; }, Execute = parameter => { - var array = (ArrayRun)model.GetNextRun(address); + var array = (ITableRun)model.GetNextRun(address); selection.SelectionStart = selection.Scroll.DataIndexToViewPoint(Address + array.ElementLength); selection.SelectionEnd = selection.Scroll.DataIndexToViewPoint(selection.Scroll.ViewPointToDataIndex(selection.SelectionStart) + array.ElementLength - 1); } @@ -119,12 +119,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { append = new StubCommand { CanExecute = parameter => { - var array = model.GetNextRun(address) as ArrayRun; + var array = model.GetNextRun(address) as ITableRun; return array != null && array.Start + array.Length == address + array.ElementLength; }, Execute = parameter => { using (ModelCacheScope.CreateScope(model)) { - var array = (ArrayRun)model.GetNextRun(address); + var array = (ITableRun)model.GetNextRun(address); var originalArray = array; var error = model.CompleteArrayExtension(viewPort.CurrentChange, ref array); if (array.Start != originalArray.Start) { @@ -151,7 +151,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { foreach (var child in Children) child.DataChanged -= ForwardModelChanged; Children.Clear(); - var array = model.GetNextRun(Address) as ArrayRun; + var array = model.GetNextRun(Address) as ITableRun; if (array == null) { CurrentElementName = "The Table tool only works if your cursor is on table data."; return; @@ -161,6 +161,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { TryUpdate(ref selectedTableIndex, model.Arrays.IndexOf(array), nameof(SelectedTableIndex)); var basename = model.GetAnchorFromAddress(-1, array.Start); + if (string.IsNullOrEmpty(basename)) basename = array.Start.ToString("X6"); var index = (Address - array.Start) / array.ElementLength; if (array.ElementNames.Count > index) { CurrentElementName = $"{basename}/{index}" + Environment.NewLine + $"{basename}/{array.ElementNames[index]}"; @@ -168,18 +169,18 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { CurrentElementName = $"{basename}/{index}"; } - if (!string.IsNullOrEmpty(array.LengthFromAnchor)) basename = array.LengthFromAnchor; // basename is now a 'parent table' name, if there is one - - // add content from this table AddChildrenFromTable(array, index); - // add content from related tables - foreach (var currentArray in model.Arrays) { - if (currentArray == array) continue; - var currentArrayName = model.GetAnchorFromAddress(-1, currentArray.Start); - if (currentArray.LengthFromAnchor == basename || currentArrayName == basename) { - Children.Add(new SplitterArrayElementViewModel(currentArrayName)); - AddChildrenFromTable(currentArray, index); + if (array is ArrayRun arrayRun) { + if (!string.IsNullOrEmpty(arrayRun.LengthFromAnchor)) basename = arrayRun.LengthFromAnchor; // basename is now a 'parent table' name, if there is one + + foreach (var currentArray in model.Arrays) { + if (currentArray == arrayRun) continue; + var currentArrayName = model.GetAnchorFromAddress(-1, currentArray.Start); + if (currentArray.LengthFromAnchor == basename || currentArrayName == basename) { + Children.Add(new SplitterArrayElementViewModel(currentArrayName)); + AddChildrenFromTable(currentArray, index); + } } } @@ -210,7 +211,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { } } - private void AddChildrenFromTable(ArrayRun table, int index) { + private void AddChildrenFromTable(ITableRun table, int index) { var itemAddress = table.Start + table.ElementLength * index; foreach (var item in table.ElementContent) { IArrayElementViewModel viewModel = null; @@ -231,9 +232,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { viewModel.DataChanged += ForwardModelChanged; if (item is ArrayRunPointerSegment pointerSegment) { var destination = model.ReadPointer(itemAddress); - if (destination != Pointer.NULL && model.GetNextRun(destination) is IStreamRun && pointerSegment.DestinationDataMatchesPointerFormat(model, new NoDataChangeDeltaModel(), destination)) { - if (pointerSegment.InnerFormat == PCSRun.SharedFormatString || pointerSegment.InnerFormat == PLMRun.SharedFormatString) { - var streamElement = new StreamArrayElementViewModel(viewPort, (FieldArrayElementViewModel)viewModel, model, item.Name, itemAddress); + if (destination != Pointer.NULL && model.GetNextRun(destination) is IStreamRun && pointerSegment.DestinationDataMatchesPointerFormat(model, new NoDataChangeDeltaModel(), itemAddress, destination)) { + if (pointerSegment.InnerFormat == PCSRun.SharedFormatString || pointerSegment.InnerFormat == PLMRun.SharedFormatString || pointerSegment.InnerFormat == TrainerPokemonTeamRun.SharedFormatString) { + var streamElement = new StreamArrayElementViewModel(history, (FieldArrayElementViewModel)viewModel, model, item.Name, itemAddress); int parentIndex = Children.Count - 1; var streamElementName = item.Name; var streamAddress = itemAddress; diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index bcccf231..5371baa5 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -174,7 +174,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var run = Model.GetNextRun(dataIndex); using (ModelCacheScope.CreateScope(Model)) { - if (run.Start <= dataIndex && run is ArrayRun array) { + if (run.Start <= dataIndex && run is ITableRun array) { var offsets = array.ConvertByteOffsetToArrayOffset(dataIndex); Tools.StringTool.Address = offsets.SegmentStart - offsets.ElementIndex * array.ElementLength; Tools.TableTool.Address = array.Start + array.ElementLength * offsets.ElementIndex; @@ -206,7 +206,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var result = "Address: " + left.ToString("X6"); var run = Model.GetNextRun(left); - if (run is ArrayRun array1 && array1.Start <= left) { + if (run is ITableRun array1 && array1.Start <= left) { var index = array1.ConvertByteOffsetToArrayOffset(left).ElementIndex; var basename = Model.GetAnchorFromAddress(-1, array1.Start); if (array1.ElementNames.Count > index) { @@ -216,7 +216,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } } else if (run.PointerSources != null && run.PointerSources.Count > 0 && string.IsNullOrEmpty(Model.GetAnchorFromAddress(-1, run.Start))) { var sourceRun = Model.GetNextRun(run.PointerSources[0]); - if (sourceRun is ArrayRun array2) { + if (sourceRun is ITableRun array2) { // we are an anchor that's pointed to from an array var offset = array2.ConvertByteOffsetToArrayOffset(run.PointerSources[0]); var index = offset.ElementIndex; @@ -442,7 +442,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var right = Math.Max(selectionStart, selectionEnd); var startRun = Model.GetNextRun(left); var endRun = Model.GetNextRun(right); - if (startRun == endRun && startRun.Start <= left && (startRun.Start < left || startRun.Start + startRun.Length - 1 > right) && startRun is ArrayRun arrayRun) { + if (startRun == endRun && startRun.Start <= left && (startRun.Start < left || startRun.Start + startRun.Length - 1 > right) && startRun is ITableRun arrayRun) { for (int i = 0; i < arrayRun.ElementCount; i++) { var start = arrayRun.Start + arrayRun.ElementLength * i; if (start + arrayRun.ElementLength <= left) continue; @@ -558,7 +558,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { public bool IsTable(Point point) { var search = scroll.ViewPointToDataIndex(point); var run = Model.GetNextRun(search); - return run.Start <= search && run is ArrayRun; + return run.Start <= search && run is ITableRun; } public void Refresh() => RefreshBackingData(); @@ -604,11 +604,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels { TryCompleteEdit(point); return; } - if (key == ConsoleKey.Enter && run is ArrayRun arrayRun1) { + if (key == ConsoleKey.Enter && run is ITableRun arrayRun1) { var offsets = arrayRun1.ConvertByteOffsetToArrayOffset(offset); SilentScroll(offsets.SegmentStart + arrayRun1.ElementLength); } - if (key == ConsoleKey.Tab && run is ArrayRun arrayRun2) { + if (key == ConsoleKey.Tab && run is ITableRun arrayRun2) { var offsets = arrayRun2.ConvertByteOffsetToArrayOffset(offset); SilentScroll(offsets.SegmentStart + arrayRun2.ElementContent[offsets.SegmentIndex].Length); } @@ -702,7 +702,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var cellToText = new ConvertCellToText(Model, run.Start); var cell = currentView[point.X, point.Y]; - if (run is ArrayRun array) { + if (run is ITableRun array) { var offsets = array.ConvertByteOffsetToArrayOffset(index); if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) { for (int i = index + 1; i < offsets.SegmentStart + array.ElementContent[offsets.SegmentIndex].Length; i++) history.CurrentChange.ChangeData(Model, i, 0x00); @@ -868,6 +868,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (child is PLMRun plmRun && parentArrayName == EggMoveRun.MoveNamesTable) { foreach (var result in plmRun.Search(parentArrayName, offsets.ElementIndex)) yield return result; } + // option 3: the value is a move used by trainer teams + if (child is TrainerPokemonTeamRun team) { + foreach (var result in team.Search(parentArrayName, offsets.ElementIndex)) { + yield return (result, result + 1); + } + } } } @@ -1014,7 +1020,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (currentRun is IStreamRun) { Tools.StringTool.Address = currentRun.Start; Tools.SelectedIndex = Tools.IndexOf(Tools.StringTool); - } else if (currentRun is ArrayRun array) { + } else if (currentRun is ITableRun array) { var offsets = array.ConvertByteOffsetToArrayOffset(byteOffset); if (format is PCS) { Tools.StringTool.Address = offsets.SegmentStart - offsets.ElementIndex * array.ElementLength; @@ -1031,7 +1037,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var index = scroll.ViewPointToDataIndex(SelectionStart); var run = Model.GetNextRun(index); if (run.Start > index) return; - if (run is ArrayRun array) { + if (run is ITableRun array) { var offsets = array.ConvertByteOffsetToArrayOffset(index); if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.Pointer) { FollowLink(x, y); @@ -1150,7 +1156,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } else if (newText.StartsWith(GotoMarker.ToString())) { return Model.GetNewPointerAutocompleteOptions(newText, selectedIndex); } else if (originalFormat is IntegerEnum intEnum) { - var array = (ArrayRun)Model.GetNextRun(intEnum.Source); + var array = (ITableRun)Model.GetNextRun(intEnum.Source); 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); @@ -1252,7 +1258,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (even) return pair(index, index + 1); return pair(index - 1, index); } - if (!(run is ArrayRun array)) return (p, p); + if (!(run is ITableRun array)) return (p, p); var offset = array.ConvertByteOffsetToArrayOffset(index); var type = array.ElementContent[offset.SegmentIndex].Type; @@ -1322,8 +1328,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels { 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 || run is PLMRun || run is EggMoveRun) Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged); + if (run is ITableRun) Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); + if (run is ITableRun || run is IStreamRun) Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged); if (completeEditOperation.MessageText != null) OnMessage?.Invoke(this, completeEditOperation.MessageText); if (completeEditOperation.ErrorText != null) OnError?.Invoke(this, completeEditOperation.ErrorText); } @@ -1493,8 +1499,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels { RefreshBackingData(); } - if (run is ArrayRun && sender != Tools.StringTool && Model.GetNextRun(Tools.StringTool.Address).Start == run.Start) Tools.StringTool.DataForCurrentRunChanged(); - if (run is ArrayRun && sender != Tools.TableTool && Model.GetNextRun(Tools.TableTool.Address).Start == run.Start) Tools.TableTool.DataForCurrentRunChanged(); + if (run is ITableRun && sender != Tools.StringTool && Model.GetNextRun(Tools.StringTool.Address).Start == run.Start) Tools.StringTool.DataForCurrentRunChanged(); + if (run is ITableRun && sender != Tools.TableTool && Model.GetNextRun(Tools.TableTool.Address).Start == run.Start) Tools.TableTool.DataForCurrentRunChanged(); } private void ModelDataMovedByTool(object sender, (int originalLocation, int newLocation) locations) { diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index 605a041c..73993461 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs @@ -55,7 +55,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(Pointer pointer, byte data) { var run = Model.GetNextRun(memoryLocation); - if (run is ArrayRun && CurrentText[0] != PointerStart) { + if (run is ITableRun && CurrentText[0] != PointerStart) { ErrorText = "Pointers in tables cannot be removed without removing the table."; return; } @@ -215,7 +215,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { return; } - var run = (ArrayRun)Model.GetNextRun(memoryLocation); + var run = (ITableRun)Model.GetNextRun(memoryLocation); var offsets = run.ConvertByteOffsetToArrayOffset(memoryLocation); int length = run.ElementContent[offsets.SegmentIndex].Length; for (int i = 0; i < length; i++) { @@ -228,7 +228,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { } private void CompleteIntegerEnumEdit(IntegerEnum integer) { - var array = (ArrayRun)Model.GetNextRun(memoryLocation); + 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)) { @@ -259,7 +259,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { var currentRun = Model.GetNextRun(memoryLocation); if (currentRun.Start > memoryLocation) currentRun = null; - bool inArray = currentRun is ArrayRun && currentRun.Start <= memoryLocation; + bool inArray = currentRun is ITableRun && currentRun.Start <= memoryLocation; var sources = currentRun?.PointerSources; if (!inArray) { @@ -284,7 +284,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { if (fullValue == Pointer.NULL || (0 <= fullValue && fullValue < Model.Count)) { if (inArray) { - UpdateArrayPointer((ArrayRun)currentRun, fullValue); + UpdateArrayPointer((ITableRun)currentRun, fullValue); } else { Model.WritePointer(CurrentChange, memoryLocation, fullValue); Model.ObserveRunWritten(CurrentChange, new PointerRun(memoryLocation, sources)); @@ -318,7 +318,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { var newRunLength = PCSString.ReadString(Model, run.Start, true); Model.ObserveRunWritten(CurrentChange, new PCSRun(Model, run.Start, newRunLength, run.PointerSources)); } - } else if (run is ArrayRun arrayRun) { + } else if (run is ITableRun arrayRun) { var offsets = arrayRun.ConvertByteOffsetToArrayOffset(memoryLocation); CurrentChange.ChangeData(Model, memoryLocation, 0xFF); memoryLocation++; @@ -365,7 +365,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { run = new PCSRun(Model, run.Start, run.Length + extraBytesNeeded, run.PointerSources); Model.ObserveRunWritten(CurrentChange, run); } - } else if (run is ArrayRun arrayRun) { + } 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) { @@ -457,11 +457,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { } } - private void UpdateArrayPointer(ArrayRun run, int pointerDestination) { + 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, pointerDestination)) { + if (!pointerSegment.DestinationDataMatchesPointerFormat(Model, CurrentChange, offsets.SegmentStart, pointerDestination)) { ErrorText = $"This pointer must point to {pointerSegment.InnerFormat} data."; return; } diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs index c508054b..cfdcb13c 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs @@ -63,7 +63,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Results.Add(new ContextItem("Repoint to New Copy", arg => ViewPort.RepointToNewCopy(pointerAddress))); } - var arrayRun = ViewPort.Model.GetNextRun(pointerAddress) as ArrayRun; + var arrayRun = ViewPort.Model.GetNextRun(pointerAddress) as ITableRun; if (arrayRun != null && arrayRun.Start <= pointerAddress) Results.AddRange(GetTableChildren(arrayRun)); else Results.AddRange(GetFormattedChildren()); } @@ -101,7 +101,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Results.Add(new ContextItem("Copy Selection", ViewPort.Copy.Execute) { ShortcutText = "Ctrl+C" }); var address = ViewPort.ConvertViewPointToAddress(point); - var arrayRun = ViewPort.Model.GetNextRun(address) as ArrayRun; + var arrayRun = ViewPort.Model.GetNextRun(address) as ITableRun; if (arrayRun != null && arrayRun.Start <= address) Results.AddRange(GetTableChildren(arrayRun)); else Results.AddRange(GetFormattedChildren()); } @@ -113,12 +113,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(Ascii ascii, byte data) => Results.AddRange(GetFormattedChildren()); public void Visit(Integer integer, byte data) { - var arrayRun = (ArrayRun)ViewPort.Model.GetNextRun(ViewPort.Tools.TableTool.Address); + var arrayRun = (ITableRun)ViewPort.Model.GetNextRun(ViewPort.Tools.TableTool.Address); Results.AddRange(GetTableChildren(arrayRun)); } public void Visit(IntegerEnum integer, byte data) { - var arrayRun = (ArrayRun)ViewPort.Model.GetNextRun(ViewPort.Tools.TableTool.Address); + var arrayRun = (ITableRun)ViewPort.Model.GetNextRun(ViewPort.Tools.TableTool.Address); Results.AddRange(GetTableChildren(arrayRun)); } @@ -137,13 +137,13 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { } public void Visit(BitArray array, byte data) { - var arrayRun = (ArrayRun)ViewPort.Model.GetNextRun(ViewPort.Tools.TableTool.Address); + var arrayRun = (ITableRun)ViewPort.Model.GetNextRun(ViewPort.Tools.TableTool.Address); Results.AddRange(GetTableChildren(arrayRun)); } public void Visit(MatchedWord word, byte data) => Results.AddRange(GetFormattedChildren()); - private IEnumerable GetTableChildren(ArrayRun array) { + private IEnumerable GetTableChildren(ITableRun array) { if (ViewPort.Tools.TableTool.Append.CanExecute(null)) { yield return new ContextItem("Extend Table", ViewPort.Tools.TableTool.Append.Execute); } diff --git a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs index 44860dee..7cc79f13 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs @@ -97,7 +97,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(PCS pcs, byte data) { // don't let it start with a space unless it's in quotes (for copy/paste) var run = Model.GetNextRun(MemoryLocation); - if (run is ArrayRun array) { + if (run is ITableRun array) { var offsets = array.ConvertByteOffsetToArrayOffset(MemoryLocation); if (offsets.SegmentStart == MemoryLocation && Input == ' ') return; } @@ -124,7 +124,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(IntegerEnum integer, byte data) { if (!integer.CanStartWithCharacter(Input)) return; - var arrayRun = (ArrayRun)Model.GetNextRun(MemoryLocation); + var arrayRun = (ITableRun)Model.GetNextRun(MemoryLocation); var offsets = arrayRun.ConvertByteOffsetToArrayOffset(MemoryLocation); var segment = (ArrayRunEnumSegment)arrayRun.ElementContent[offsets.SegmentIndex]; var allOptions = segment.GetOptions(Model).Select(option => option + " "); diff --git a/src/HexManiac.Tests/TableTests.cs b/src/HexManiac.Tests/TableTests.cs index 52198413..db89e3e2 100644 --- a/src/HexManiac.Tests/TableTests.cs +++ b/src/HexManiac.Tests/TableTests.cs @@ -1,5 +1,7 @@ using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.Models.Runs; using HavenSoft.HexManiac.Core.ViewModels.Tools; +using System; using System.Linq; using Xunit; @@ -17,5 +19,52 @@ namespace HavenSoft.HexManiac.Tests { // a combobox is used for numeric limit fields ViewPort.Tools.TableTool.Children.Single(child => child is ComboBoxArrayElementViewModel); } + + [Fact] + public void TrainerPokemonTeamEnumSelectionSelectsEntireEnum() { + ArrangeTrainerPokemonTeamData(0, 1); + + ViewPort.SelectionStart = new Point(0, 6); + + Assert.Equal(new Point(1, 6), ViewPort.SelectionEnd); + } + + [Fact] + public void HexEditingWorksForTrainerPokemon() { + ArrangeTrainerPokemonTeamData(0, 1); + + ViewPort.SelectionStart = new Point(TrainerPokemonTeamRun.PokemonFormat_PokemonStart, 6); + ViewPort.Edit("C "); + + Assert.Equal(2, Model[0x60 + TrainerPokemonTeamRun.PokemonFormat_PokemonStart]); + } + + [Fact] + public void CanExtendTrainerTeamViaStream() { + ArrangeTrainerPokemonTeamData(0, 1); + Model[0x6C] = 0x0B; // add a random data value so that extending will cause moving + + ViewPort.SelectionStart = new Point(0, 6); + var streamTool = ViewPort.Tools.StringTool; + streamTool.Content = $"10 A{Environment.NewLine}10 B"; + + Assert.NotEqual(0x60, Model.ReadPointer(0x24)); + } + + private void ArrangeTrainerPokemonTeamData(byte structType, byte pokemonCount) { + CreateTextTable(EggMoveRun.PokemonNameTable, 0x100, "ABCDEFGHIJKLMNOP".Select(c => c.ToString()).ToArray()); + CreateTextTable(EggMoveRun.MoveNamesTable, 0x140, "abcdefghijklmnop".Select(c => c.ToString()).ToArray()); + CreateTextTable(HardcodeTablesModel.ItemsTableName, 0x180, "0123456789".Select(c => c.ToString()).ToArray()); + + Model[TrainerPokemonTeamRun.TrainerFormat_StructTypeOffset] = structType; + Model[TrainerPokemonTeamRun.TrainerFormat_PokemonCountOffset] = pokemonCount; + Model.WritePointer(new ModelDelta(), TrainerPokemonTeamRun.TrainerFormat_PointerOffset, 0x60); + + ViewPort.Goto.Execute("00"); + ViewPort.SelectionStart = new Point(4, 2); + ViewPort.Edit($"^trainertable[team<{TrainerPokemonTeamRun.SharedFormatString}>]1 "); + + ViewPort.Goto.Execute("00"); + } } }