public void PokeStatsAreFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, "pokestats"); var run = (ArrayRun)model.GetNextAnchor(address); var firstPokemonStats = model.Skip(run.Start + run.ElementLength).Take(6).ToArray(); var compareSet = new[] { 45, 49, 49, 45, 65, 65 }; // Bulbasaur if (game.Contains("Vega")) { compareSet = new[] { 42, 53, 40, 70, 63, 40 } } ; // Nimbleaf if (game.Contains("Clover")) { compareSet = new[] { 56, 60, 55, 50, 47, 50 } } ; // Grasshole for (int i = 0; i < compareSet.Length; i++) { Assert.Equal(compareSet[i], firstPokemonStats[i]); } }
public void AbilitiyDescriptionsAreFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, "abilitydescriptions"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Clover")) { Assert.Equal(156, run.ElementCount); } else if (game.Contains("Gaia")) { Assert.Equal(188, run.ElementCount); } else { Assert.Equal(78, run.ElementCount); } if (game.Contains("Gaia")) { return; // don't validate description text in Gaia, it's actually invalid. } for (var i = 0; i < run.ElementCount; i++) { address = model.ReadPointer(run.Start + i * 4); var childRun = model.GetNextRun(address); Assert.IsType <PCSRun>(childRun); } }
public void MovesNamesAreFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.MoveNamesTable); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Vega")) { Assert.Equal(512, run.ElementCount); } else if (game.Contains("Clover")) { Assert.Equal(512, run.ElementCount); } else if (game.Contains("Gaia")) { Assert.Equal(511, run.ElementCount); } else { Assert.Equal(355, run.ElementCount); } }
public void TutorsAreFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var movesLocation = model.GetAddressFromAnchor(noChange, -1, AutoSearchModel.MoveTutors); var compatibilityLocation = model.GetAddressFromAnchor(noChange, -1, AutoSearchModel.TutorCompatibility); // ruby and sapphire have no tutors // Gaia has move tutors, but it does a bunch of custom stuff (multiple tables) so I don't feel bad about not supporting it by default. if (game.Contains("Ruby") || game.Contains("Sapphire") || game.Contains("Gaia")) { Assert.Equal(Pointer.NULL, movesLocation); Assert.Equal(Pointer.NULL, compatibilityLocation); return; } var moves = (ArrayRun)model.GetNextRun(movesLocation); var compatibility = (ArrayRun)model.GetNextRun(compatibilityLocation); var expectedMoves = game.Contains("Emerald") || game.Contains("Altair") ? 30 : 15; var compatibilityElementLength = (int)Math.Ceiling(expectedMoves / 8.0); Assert.Equal(expectedMoves, moves.ElementCount); Assert.Equal(compatibilityElementLength, compatibility.ElementContent[0].Length); }
/// <summary> /// Uses the hint, as well as this sprite's table location (if any), to find palettes that can be applied to this sprite. /// (1) if the sprite's hint is the name of a palette, return that. Example: title screen pokemon sprite/palette pair. /// (2) if the sprite's hint is the name of an enum table, use that enum's source as a list of palettes and get the appropriate one from the matching index of the enum table. Example: pokemon icons /// (3) if the sprite's hint is a table name followed by a key=value pair, go grab the a palette from the element within that table such that it's key equals that value. Example: Overworld sprites /// (4) if the sprite's hint is a table name, return all palettes within the matching index of that table. Example: trainer sprites/palettes. /// (5) if the sprite has no hint, return all palettes in arrays with matching length from the same index. Example: pokemon sprites. Leaving it empty allows both normal and shiny palettes to match. /// </summary> public static IReadOnlyList <IPaletteRun> FindRelatedPalettes(this ISpriteRun spriteRun, IDataModel model, int primarySource = -1, string hint = null, bool includeAllTableIndex = false) { // find all palettes that could be applied to this sprite run var noChange = new NoDataChangeDeltaModel(); var results = new List <IPaletteRun>(); if (spriteRun?.SpriteFormat.BitsPerPixel < 4) { return(results); // 1- and 2-bit sprites don't have palettes } hint = hint ?? spriteRun?.SpriteFormat.PaletteHint; if (primarySource == -1) { var pointerCount = spriteRun?.PointerSources?.Count ?? 0; for (int i = 0; i < pointerCount; i++) { if (!(model.GetNextRun(spriteRun.PointerSources[i]) is ArrayRun)) { continue; } primarySource = spriteRun.PointerSources[i]; break; } } var spriteTable = model.GetNextRun(primarySource) as ITableRun; var offset = spriteTable?.ConvertByteOffsetToArrayOffset(primarySource) ?? new ArrayOffset(-1, -1, -1, -1); if (primarySource < 0) { offset = new ArrayOffset(-1, -1, -1, -1); } if (!string.IsNullOrEmpty(hint)) { var address = model.GetAddressFromAnchor(noChange, -1, hint); var run = model.GetNextRun(address); if (run is IPaletteRun palRun && palRun.Start == address) { // option 1: hint is to a palette results.Add(palRun); return(results); } else if (run is ArrayRun enumArray && enumArray.ElementContent.Count == 1 && enumArray.ElementContent[0] is ArrayRunEnumSegment enumSegment) { // option 2: hint is to index into paletteTable, and I'm in a table var paletteTable = model.GetNextRun(model.GetAddressFromAnchor(noChange, -1, enumSegment.EnumName)) as ITableRun; if (offset.ElementIndex != -1 && paletteTable != null) { var paletteIndex = model.ReadMultiByteValue(enumArray.Start + enumArray.ElementLength * offset.ElementIndex, enumArray.ElementLength); var destination = model.ReadPointer(paletteTable.Start + paletteTable.ElementLength * paletteIndex); var tempRun = model.GetNextRun(destination); if (tempRun is IPaletteRun pRun && pRun.Start == destination) { results.Add(pRun); } } }
public void LvlUpMovesAreFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, "lvlmoves"); var run = (ArrayRun)model.GetNextAnchor(address); Assert.NotNull(run); }
public ErrorInfo Run(IViewPort viewPortInterface) { var noChange = new NoDataChangeDeltaModel(); var viewPort = (ViewPort)viewPortInterface; var model = viewPort.Model; var token = viewPort.CurrentChange; Run(model, token); return(ErrorInfo.NoError); }
public void ItemsAreFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, "items"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Altair")) { Assert.Equal(377, run.ElementCount); } else if (game.Contains("Emerald")) { Assert.Equal(377, run.ElementCount); } else if (game.Contains("FireRed")) { Assert.Equal(375, run.ElementCount); } else if (game.Contains("DarkRisingKAIZO")) { Assert.Equal(375, run.ElementCount); } else if (game.Contains("LeafGreen")) { Assert.Equal(375, run.ElementCount); } else if (game.Contains("Ruby")) { Assert.Equal(349, run.ElementCount); } else if (game.Contains("Sapphire")) { Assert.Equal(349, run.ElementCount); } else if (game.Contains("Vega")) { Assert.Equal(375, run.ElementCount); } else if (game.Contains("Clover")) { Assert.Equal(375, run.ElementCount); } else if (game.Contains("Gaia")) { Assert.Equal(375, run.ElementCount); } else { throw new NotImplementedException(); } }
public bool CanRun(IViewPort viewPortInterface) { // require that I have a tab with real data, not a search tab or a diff tab or something if (!(viewPortInterface is ViewPort viewPort)) { return(false); } // require that we fan find the specials table and that it's long enough var token = new NoDataChangeDeltaModel(); var specialsAddress = viewPort.Model.GetAddressFromAnchor(token, -1, HardcodeTablesModel.SpecialsTable); if (specialsAddress < 0 || specialsAddress > viewPort.Model.Count) { return(false); } var specials = viewPort.Model.GetNextRun(specialsAddress) as ITableRun; if (specials == null) { return(false); } if (specials.ElementCount < 397) { return(false); } // require that this data actually supports this change var model = viewPort.Model; var gameCode = model.GetGameCode(); var(getTutorMove, canPokemonLearnTutorMove, _, _) = GetOffsets(gameCode); if (getTutorMove < 0 || canPokemonLearnTutorMove < 0) { return(false); } // require that this data has a tutormoves and tutorcompatibility table, since we're messing with those var tutormoves = model.GetAddressFromAnchor(token, -1, MoveTutors); var tutorcompatibility = model.GetAddressFromAnchor(token, -1, TutorCompatibility); if (tutormoves == Pointer.NULL || tutorcompatibility == Pointer.NULL) { return(false); } // if the patch has already been applied, you can't apply it again if (viewPort.Model.GetNextRun(canPokemonLearnTutorMove + 0x20) is WordRun) { return(false); } return(true); }
public void MoveDataFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, "movedata"); var run = (ArrayRun)model.GetNextAnchor(address); var poundStats = model.Skip(run.Start + run.ElementLength).Take(8).ToArray(); var compareSet = new[] { 0, 40, 0, 100, 35, 0, 0, 0 }; for (int i = 0; i < compareSet.Length; i++) { Assert.Equal(compareSet[i], poundStats[i]); } }
public bool CanRun(IViewPort viewPortInterface) { var noChange = new NoDataChangeDeltaModel(); var viewPort = viewPortInterface as ViewPort; if (viewPort == null) { return(false); } var model = viewPort.Model; ArrayRun get(string name) => model.GetNextRun(model.GetAddressFromAnchor(noChange, -1, name)) as ArrayRun; var regional = get(HardcodeTablesModel.RegionalDexTableName); var national = get(HardcodeTablesModel.NationalDexTableName); var convert = get(HardcodeTablesModel.ConversionDexTableName); if (regional is null || national is null || convert is null) { return(false); } if (regional.ElementContent.Count != 1 || national.ElementContent.Count != 1 || convert.ElementContent.Count != 1) { return(false); } if (regional.ElementCount != national.ElementCount || regional.ElementCount != convert.ElementCount) { return(false); } if (regional.ElementLength != 2 || national.ElementLength != 2 || convert.ElementLength != 2) { return(false); } for (int i = 0; i < regional.ElementCount; i++) { var regionalIndex = model.ReadMultiByteValue(regional.Start + i * 2, 2); var nationalIndex = model.ReadMultiByteValue(national.Start + i * 2, 2); var conversionIndex = model.ReadMultiByteValue(convert.Start + (regionalIndex - 1) * 2, 2); if (nationalIndex != conversionIndex) { return(true); } } return(false); }
public void TrainerClassNamesAreFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, "trainerclassnames"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Altair")) { Assert.Equal(66, run.ElementCount); } else if (game.Contains("Emerald")) { Assert.Equal(66, run.ElementCount); } else if (game.Contains("FireRed")) { Assert.Equal(107, run.ElementCount); } else if (game.Contains("DarkRisingKAIZO")) { Assert.Equal(107, run.ElementCount); } else if (game.Contains("LeafGreen")) { Assert.Equal(107, run.ElementCount); } else if (game.Contains("Ruby")) { Assert.Equal(58, run.ElementCount); } else if (game.Contains("Sapphire")) { Assert.Equal(58, run.ElementCount); } else if (game.Contains("Vega")) { Assert.Equal(107, run.ElementCount); } }
public void AbilitiyNamesAreFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, "abilitynames"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Clover")) { Assert.Equal(156, run.ElementCount); } else if (game.Contains("Gaia")) { Assert.Equal(188, run.ElementCount); } else { Assert.Equal(78, run.ElementCount); } }
public void EggMoveDataFound(string game) { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); var address = model.GetAddressFromAnchor(noChange, -1, "eggmoves"); var run = (EggMoveRun)model.GetNextAnchor(address); if (game.Contains("Vega")) { Assert.Equal(3, run.PointerSources.Count); // there's a false positive in Vega... for now! Would be nice if this were 2, but it doesn't much matter. } else { Assert.Equal(2, run.PointerSources.Count); } var expectedLastElement = model.ReadMultiByteValue(run.PointerSources[1] - 4, 4); var expectedLength = expectedLastElement + 1; var actualLength = run.Length / 2 - 1; // remove the closing element. Assert.InRange(actualLength, 790, expectedLength); }
public bool CanRun(IViewPort viewPort) { if (!File.Exists(ExpandLevelUpMovesCode)) { return(false); } if (!(viewPort is IEditableViewPort editableViewPort)) { return(false); } var noChange = new NoDataChangeDeltaModel(); if (viewPort.Model.GetAddressFromAnchor(noChange, -1, LevelMovesTableName) == Pointer.NULL) { return(false); } if (viewPort.Model.GetAddressFromAnchor(noChange, -1, MoveDataTable) == Pointer.NULL) { return(false); } return(true); }
public async Task <ErrorInfo> Run(IViewPort viewPortInterface) { var viewPort = (ViewPort)viewPortInterface; var model = viewPort.Model; var token = new NoDataChangeDeltaModel(); var gameCode = model.GetGameCode(); var(getTutorMove, canPokemonLearnTutorMove, getTutorMove_Length, canPokemonLearnTutorMove_Length) = GetOffsets(gameCode); var specialsAddress = model.GetAddressFromAnchor(token, -1, SpecialsTable); var tutorSpecial = model.ReadPointer(specialsAddress + 397 * 4); // Emerald tutors is actually special 477, but we don't need to edit it so it doesn't matter. tutorSpecial -= 1; // the pointer is to thumb code, so it's off by one. var tutormoves = model.GetAddressFromAnchor(token, -1, MoveTutors); var tutorcompatibility = model.GetAddressFromAnchor(token, -1, TutorCompatibility); InsertRoutine_GetTutorMove(viewPort, getTutorMove, getTutorMove_Length); InsertRoutine_CanPokemonLearnTutorMove(viewPort, canPokemonLearnTutorMove, canPokemonLearnTutorMove_Length); UpdateRoutine_TutorSpecial(viewPort, tutorSpecial, gameCode); CanRunChanged?.Invoke(this, EventArgs.Empty); return(ErrorInfo.NoError); }
private static int KnownLengthSearch(IDataModel data, List <ArrayRunElementSegment> elementContent, int elementLength, string lengthToken, out int bestLength, Func <IFormattedRun, bool> runFilter) { var noChange = new NoDataChangeDeltaModel(); if (!int.TryParse(lengthToken, out bestLength)) { var matchedArrayName = lengthToken; var matchedArrayAddress = data.GetAddressFromAnchor(noChange, -1, matchedArrayName); if (matchedArrayAddress == Pointer.NULL) { return(Pointer.NULL); } var matchedRun = data.GetNextRun(matchedArrayAddress) as ArrayRun; if (matchedRun == null) { return(Pointer.NULL); } bestLength = matchedRun.ElementCount; } FormatMatchFlags flags = default; if (elementContent.Count == 1) { flags |= FormatMatchFlags.IsSingleSegment; } for (var run = data.GetNextRun(0); run.Start < data.Count; run = data.GetNextRun(run.Start + run.Length + 1)) { if (!(run is PointerRun)) { continue; } var targetRun = data.GetNextRun(data.ReadPointer(run.Start)); if (targetRun is ArrayRun) { continue; } // some searches allow special conditions on the run. For example, we could only be intersted in runs with >100 pointers leading to it. if (runFilter != null && !runFilter(targetRun)) { continue; } // tolerate a few errors in the data. We know what length we're looking for, so if most of the elements match, then // most likely we're just looking at the right collection but with some user-created bugs. int errorsToTolerate = bestLength / 80; int encounterErrors = 0; int lastGoodLength = 0; int currentLength = 0; int currentAddress = targetRun.Start; bool earlyExit = false; for (int i = 0; i < bestLength; i++) { var nextArray = data.GetNextAnchor(currentAddress + 1); bool match = DataMatchesElementFormat(data, currentAddress, elementContent, flags, nextArray); currentLength++; currentAddress += elementLength; if (match) { lastGoodLength = currentLength; } else { encounterErrors++; if (encounterErrors > errorsToTolerate) { // as long as this array is at least 80% of the passed in array, we're fine and can say that these are matched. // (the other one might have bad data at the end that needs to be removed) (example: see Gaia) earlyExit = bestLength * .8 > lastGoodLength; break; } } } currentLength = lastGoodLength; if (!earlyExit) { var dataEmpty = Enumerable.Range(targetRun.Start, currentLength * elementLength).Select(i => data[i]).All(d => d == 0xFF || d == 0x00); if (dataEmpty) { continue; // don't accept the run if it contains no data } bestLength = currentLength; return(targetRun.Start); } } return(Pointer.NULL); }
/// <summary> /// Uses the hint, as well as this sprite's table location (if any), to find palettes that can be applied to this sprite. /// (1) if the sprite's hint is the name of a palette, return that. Example: title screen pokemon sprite/palette pair. /// (2) if the sprite's hint is the name of an enum table, use that enum's source as a list of palettes and get the appropriate one from the matching index of the enum table. Example: pokemon icons /// (3) if the sprite's hint is a table name followed by a key=value pair, go grab the a palette from the element within that table such that it's key equals that value. Example: Overworld sprites /// (4) if the sprite's hint is a table name, return all palettes within the matching index of that table. Example: trainer sprites/palettes. /// (5) if the sprite has no hint, return all palettes in arrays with matching length from the same index. Example: pokemon sprites. Leaving it empty allows both normal and shiny palettes to match. /// </summary> public static IReadOnlyList <IPaletteRun> FindRelatedPalettes(this ISpriteRun spriteRun, IDataModel model, int primarySource = -1, string hint = null) { // find all palettes that could be applied to this sprite run var noChange = new NoDataChangeDeltaModel(); var results = new List <IPaletteRun>(); hint = hint ?? spriteRun?.SpriteFormat.PaletteHint; if (primarySource == -1) { var pointerCount = spriteRun?.PointerSources?.Count ?? 0; for (int i = 0; i < pointerCount; i++) { if (!(model.GetNextRun(spriteRun.PointerSources[i]) is ArrayRun)) { continue; } primarySource = spriteRun.PointerSources[i]; break; } } var spriteTable = model.GetNextRun(primarySource) as ArrayRun; var offset = spriteTable?.ConvertByteOffsetToArrayOffset(primarySource) ?? new ArrayOffset(-1, -1, -1, -1); if (primarySource < 0) { offset = new ArrayOffset(-1, -1, -1, -1); } if (!string.IsNullOrEmpty(hint)) { var run = model.GetNextRun(model.GetAddressFromAnchor(noChange, -1, hint)); if (run is IPaletteRun palRun) { // option 1: hint is to a palette results.Add(palRun); return(results); } else if (run is ArrayRun enumArray && enumArray.ElementContent.Count == 1 && enumArray.ElementContent[0] is ArrayRunEnumSegment enumSegment) { // option 2: hint is to index into paletteTable, and I'm in a table var paletteTable = model.GetNextRun(model.GetAddressFromAnchor(noChange, -1, enumSegment.EnumName)) as ArrayRun; if (offset.ElementIndex != -1) { var paletteIndex = model.ReadMultiByteValue(enumArray.Start + enumArray.ElementLength * offset.ElementIndex, enumArray.ElementLength); if (model.GetNextRun(model.ReadPointer(paletteTable.Start + paletteTable.ElementLength * paletteIndex)) is IPaletteRun pRun) { results.Add(pRun); } } } else if (hint.Contains(":")) { // option 3: hint is a table name, followed by a identifier=value pair var tableKeyPair = hint.Split(':'); var identifierValuePair = tableKeyPair.Length == 2 ? tableKeyPair[1].Split("=") : new string[0]; if (identifierValuePair.Length == 2) { var paletteTable = model.GetNextRun(model.GetAddressFromAnchor(noChange, -1, tableKeyPair[0])) as ITableRun; var segment = paletteTable?.ElementContent.FirstOrDefault(seg => seg.Name == identifierValuePair[0]); var pSegment = paletteTable?.ElementContent.FirstOrDefault(seg => seg is ArrayRunPointerSegment pSeg && PaletteRun.TryParsePaletteFormat(pSeg.InnerFormat, out var _)); var rawValue = identifierValuePair[1]; int keyValue; if (segment is ArrayRunEnumSegment eSegment) { keyValue = eSegment.GetOptions(model).ToList().IndexOf(rawValue); } else if (segment is ArrayRunHexSegment hSegment) { int.TryParse(rawValue, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out keyValue); } else { int.TryParse(rawValue, out keyValue); } if (segment != null) { var segmentOffset = paletteTable.ElementContent.Until(seg => seg == segment).Sum(seg => seg.Length); var pSegmentOffset = paletteTable.ElementContent.Until(seg => seg == pSegment).Sum(seg => seg.Length); var tableIndex = Enumerable.Range(0, paletteTable.ElementCount).FirstOrDefault(i => model.ReadMultiByteValue(paletteTable.Start + i * paletteTable.ElementLength + segmentOffset, segment.Length) == keyValue); var paletteStart = model.ReadPointer(paletteTable.Start + tableIndex * paletteTable.ElementLength + pSegmentOffset); if (model.GetNextRun(paletteStart) is IPaletteRun pRun) { results.Add(pRun); } } } } // option 4: I'm in a table, and my hint is a table name if (offset.ElementIndex == -1) { return(results); } if (!(run is ArrayRun array)) { return(results); } results.AddRange(model.GetPointedChildren <IPaletteRun>(array, offset.ElementIndex)); }