/// <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 static bool TrySearch(IDataModel data, ModelDelta token, out EggMoveRun eggmoves) { eggmoves = default; var pokenames = data.GetNextRun(data.GetAddressFromAnchor(token, -1, PokemonNameTable)) as ArrayRun; var movenames = data.GetNextRun(data.GetAddressFromAnchor(token, -1, MoveNamesTable)) as ArrayRun; if (pokenames == null || movenames == null) return false; for (var run = data.GetNextRun(0); run.Start < int.MaxValue; run = data.GetNextRun(run.Start + run.Length)) { if (run is ArrayRun || run is PCSRun || run.PointerSources == null) continue; // verify expected pointers to this if (run.PointerSources.Count < 2 || run.PointerSources.Count > 10) continue; // verify limiter var length = data.ReadMultiByteValue(run.PointerSources[1] - 4, 4); // we just read the 'length' from basically a random byte... verify that it could make sense as a length if (length < 1000 || length > 7000) continue; if (run.Start + length * 2 + 3 < 0) continue; if (run.Start + length * 2 + 3 > data.Count) continue; // verify content bool possibleMatch = true; int lastValue = -1; for (int i = 0; i < length - 2; i++) { var value = data.ReadMultiByteValue(run.Start + i * 2, 2); // if the same byte pairs are repeated multiple times, then this pokemon is listed twice or has the same egg move twice. // that seems unlikely... this is probably the wrong data. if (value == lastValue) { possibleMatch = false; break; } lastValue = value; if (value == EndStream) break; // early exit, the data was edited, but that's ok. Everything still matches up. if (value >= MagicNumber) { value -= MagicNumber; if (value < pokenames.ElementCount) continue; } if (value < movenames.ElementCount) continue; possibleMatch = false; break; } if (!possibleMatch) continue; // content is correct, length is correct: this is it! eggmoves = new EggMoveRun(data, run.Start); return true; } return false; }
public override IDataFormat CreateDataFormat(IDataModel data, int index) { var inner = ITableRunExtensions.CreateSegmentDataFormat(this, data, index); if (index > lastFormatCreated) { lastFormatCreated = index; return(inner); } if ((index - Start) % ElementLength != 0) { return(inner); } var address = data.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, TilemapAnchor); var run = data.GetNextRun(address) as ITilemapRun; var pixels = data.CurrentCacheScope.GetImage(run); if (pixels == null) { return(inner); } var missingTopRows = (index - Start) / ElementLength; pixels = Crop(pixels, -Margins.Left * 8, -Margins.Top * 8, -Margins.Right * 8, -Margins.Bottom * 8); pixels = DuplicateDown(pixels, Margins.LengthMultiplier); pixels = Crop(pixels, 0, missingTopRows * 8, 0, 0); lastFormatCreated = index; return(new SpriteDecorator(inner, pixels, pixels.PixelWidth / 8, pixels.PixelHeight / 8)); }
public int[,] GetPixels(IDataModel model, int page) { var mapData = Decompress(model, Start); if (mapData == null) { return(null); } var tilesetAddress = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, Format.MatchingTileset); var tileset = model.GetNextRun(tilesetAddress) as ISpriteRun; if (tileset == null) { tileset = model.GetNextRun(arrayTilesetAddress) as ISpriteRun; } if (tileset == null || tileset is ITilemapRun) { return(new int[Format.TileWidth * 8, Format.TileHeight * 8]); // relax the conditions slightly: if the run we found is an LZSpriteRun, that's close enough, we can use it as a tileset. } var tiles = tileset.GetData(); if (tiles == null) { return(null); } return(GetPixels(mapData, tiles, Format, BytesPerTile)); }
public static ITilemapRun SetPixels(ITilemapRun run, IDataModel model, ModelDelta token, int page, int[,] pixels, ref int arrayTilesetAddress, Func <byte[], ModelDelta, ITilemapRun> replaceData) { var tileData = Tilize(pixels, run.Format.BitsPerPixel); var tiles = GetUniqueTiles(tileData, run.BytesPerTile == 2); var tilesetAddress = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, run.Format.MatchingTileset); var tileset = model.GetNextRun(tilesetAddress) as ITilesetRun; if (tileset == null) { FindMatchingTileset(run, model, ref arrayTilesetAddress); tileset = model.GetNextRun(arrayTilesetAddress) as ITilesetRun; } var tilesToKeep = new HashSet <int>((tileset.DecompressedLength / tileset.TilesetFormat.BitsPerPixel / 8).Range()); var originalUsedTiles = GetUsedTiles(run).ToHashSet(); foreach (var tile in originalUsedTiles) { tilesToKeep.Remove(tile); } foreach (var tilemap in tileset.FindDependentTilemaps(model).Except(run)) { tilesToKeep.AddRange(GetUsedTiles(tilemap)); } var oldTileDataRaw = tileset.GetData(); var previousTiles = Tilize(oldTileDataRaw, run.Format.BitsPerPixel); tiles = MergeTilesets(previousTiles, tilesToKeep, tiles, run.BytesPerTile == 2); tileset.SetPixels(model, token, tiles); var mapData = run.GetTilemapData(); var tileWidth = tileData.GetLength(0); var tileHeight = tileData.GetLength(1); for (int y = 0; y < tileHeight; y++) { for (int x = 0; x < tileWidth; x++) { var i = y * tileWidth + x; var(tile, paletteIndex) = tileData[x, y]; var(tileIndex, matchType) = FindMatch(tile, tiles, run.BytesPerTile == 2); if (tileIndex == -1) { tileIndex = 0; } if (run.BytesPerTile == 2) { var mapping = PackMapping(paletteIndex, matchType, tileIndex); mapData[i * 2 + 0] = (byte)mapping; mapData[i * 2 + 1] = (byte)(mapping >> 8); } else { mapData[i] = (byte)tileIndex; } } } return(replaceData(mapData, token)); }
public void AddMatchedWord(IDataModel model, int memoryLocation, string parentName) { var parentAddress = model.GetAddressFromAnchor(this, -1, parentName); var parent = model.GetNextRun(parentAddress) as ArrayRun; model.WriteValue(this, memoryLocation, parent?.ElementCount ?? default); addedMatchedWords[memoryLocation] = parentName; }
private int GetParentIndex(IReadOnlyList <int> pointerSources) { if (parentName == string.Empty) { var matches = pointerSources.Where(SourceIsFromParentTable).ToList(); return(matches.Count > 0 ? matches[0] : -1); } else { return(model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, parentName)); } }
public void AddMatchedWord(IDataModel model, int memoryLocation, string parentName, int byteCount) { var parentAddress = model.GetAddressFromAnchor(this, -1, parentName); var parent = model.GetNextRun(parentAddress) as ArrayRun; if (parent != null && !(this is NoDataChangeDeltaModel)) { model.WriteMultiByteValue(memoryLocation, byteCount, this, parent.ElementCount); } using (CaptureNonDataChange()) addedMatchedWords[memoryLocation] = parentName; }
public int[,] GetPixels(IDataModel model, int page) { var result = new int[Format.TileWidth * 8, Format.TileHeight * 8]; var mapData = Decompress(model, Start); var tilesetAddress = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, Format.MatchingTileset); var tileset = model.GetNextRun(tilesetAddress) as ISpriteRun; if (tileset == null) { tileset = model.GetNextRun(arrayTilesetAddress) as ISpriteRun; } if (tileset == null || !(tileset is LZRun)) { return(result); // relax the conditions slightly: if the run we found is an LZSpriteRun, that's close enough, we can use it as a tileset. } var tiles = Decompress(model, tileset.Start); var tileSize = tileset is LzTilesetRun lztsRun ? lztsRun.Format.BitsPerPixel * 8 : tileset.SpriteFormat.BitsPerPixel * 8; for (int y = 0; y < Format.TileHeight; y++) { var yStart = y * 8; for (int x = 0; x < Format.TileWidth; x++) { var map = mapData.ReadMultiByteValue((Format.TileWidth * y + x) * 2, 2); var tile = map & 0x3FF; var tileStart = tile * tileSize; var pixels = SpriteRun.GetPixels(tiles, tileStart, 1, 1, Format.BitsPerPixel); // TODO cache this during this method so we don't load the same tile more than once var hFlip = (map >> 10) & 0x1; var vFlip = (map >> 11) & 0x1; var pal = (map >> 12) & 0xF; pal <<= 4; var xStart = x * 8; for (int yy = 0; yy < 8; yy++) { for (int xx = 0; xx < 8; xx++) { var inX = hFlip == 1 ? 7 - xx : xx; var inY = vFlip == 1 ? 7 - yy : yy; result[xStart + xx, yStart + yy] = pixels[inX, inY] + pal; } } } } return(result); }
/// <summary> /// Finds what 4 moves a pokemon would have by default, based on lvlmoves and the given level /// </summary> private IReadOnlyList <int> GetDefaultMoves(int pokemon, int currentLevel) { var results = new List <int>(); var levelMovesAddress = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, HardcodeTablesModel.LevelMovesTableName); var lvlMoves = model.GetNextRun(levelMovesAddress) as ArrayRun; if (lvlMoves != null) { var movesStart = model.ReadPointer(lvlMoves.Start + lvlMoves.ElementLength * pokemon); if (model.GetNextRun(movesStart) is ITableRun run) { for (int i = 0; i < run.ElementCount; i += 1) { int level = 0, move = 0; if (run.ElementContent.Count == 1) { var pair = model.ReadMultiByteValue(run.Start + i * run.ElementLength, run.ElementLength); (level, move) = PLMRun.SplitToken(pair); } else if (run.ElementContent.Count == 2) { move = model.ReadMultiByteValue(run.Start + i * run.ElementLength, run.ElementContent[0].Length); level = model.ReadMultiByteValue(run.Start + i * run.ElementLength + run.ElementContent[0].Length, run.ElementContent[1].Length); } else { level = int.MaxValue; } if (currentLevel >= level) { results.Add(move); } else { break; } } } } while (results.Count > 4) { results.RemoveAt(0); // restrict to the last 4 moves } while (results.Count < 4) { results.Add(0); // pad with extra '0' moves if needed } return(results); }
public ISpriteRun SetPixels(IDataModel model, ModelDelta token, int page, int[,] pixels) { var tileData = Tilize(pixels); var tiles = GetUniqueTiles(tileData); var tilesetAddress = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, Format.MatchingTileset); var tileset = model.GetNextRun(tilesetAddress) as LzTilesetRun; if (tileset == null) { tileset = model.GetNextRun(arrayTilesetAddress) as LzTilesetRun; } tileset.SetPixels(model, token, tiles); if (tiles.Length > 0x400) { // TODO fail: too many unique tiles return(this); } var mapData = Decompress(model, Start); var tileWidth = tileData.GetLength(0); var tileHeight = tileData.GetLength(1); for (int y = 0; y < tileHeight; y++) { for (int x = 0; x < tileWidth; x++) { var i = y * tileWidth + x; var(tile, paletteIndex) = tileData[x, y]; var(tileIndex, matchType) = FindMatch(tile, tiles); var mapping = PackMapping(paletteIndex, matchType, tileIndex); mapData[i * 2 + 0] = (byte)mapping; mapData[i * 2 + 1] = (byte)(mapping >> 8); } } var newModelData = Compress(mapData, 0, mapData.Length); var newRun = (ISpriteRun)model.RelocateForExpansion(token, this, newModelData.Count); for (int i = 0; i < newModelData.Count; i++) { token.ChangeData(model, newRun.Start + i, newModelData[i]); } for (int i = newModelData.Count; i < Length; i++) { token.ChangeData(model, newRun.Start + i, 0xFF); } newRun = new LzTilemapRun(Format, model, newRun.Start, newRun.PointerSources); model.ObserveRunWritten(token, newRun); return(newRun); }
public static void Run(IDataModel model, ModelDelta token) { ArrayRun get(string name) => model.GetNextRun(model.GetAddressFromAnchor(token, -1, name)) as ArrayRun; var regional = get(HardcodeTablesModel.RegionalDexTableName); var national = get(HardcodeTablesModel.NationalDexTableName); var convert = get(HardcodeTablesModel.ConversionDexTableName); 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) { model.WriteMultiByteValue(convert.Start + (regionalIndex - 1) * 2, 2, token, nationalIndex); } } }
public static IEnumerable <string> GetOptions(IDataModel model, string enumName) { if (int.TryParse(enumName, out var result)) { return(result.Range().Select(i => i.ToString())); } IEnumerable <string> options = model.GetOptions(enumName); // we _need_ options for the table tool // if we have none, just create "0", "1", ..., "n-1" based on the length of the EnumName table. if (!options.Any()) { if (model.GetNextRun(model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, enumName)) is ITableRun tableRun) { options = tableRun.ElementCount.Range().Select(i => i.ToString()); } } return(options); }
// TODO do some sort of caching: rendering these images every time probably sucks for performance. public IEnumerable <ComboOption> GetComboOptions(IDataModel model) { var defaultOptions = GetOptions(model) .Select((option, i) => new ComboOption(option, i)) .Where(combo => combo.Text != null) .ToList(); if (!(model.GetNextRun(model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, EnumName)) is ITableRun tableRun)) { return(defaultOptions); } if (!(tableRun.ElementContent[0] is ArrayRunPointerSegment pointerSegment)) { return(defaultOptions); } if (!LzSpriteRun.TryParseSpriteFormat(pointerSegment.InnerFormat, out var _) && !SpriteRun.TryParseSpriteFormat(pointerSegment.InnerFormat, out var _)) { return(defaultOptions); } var imageOptions = new List <ComboOption>(); for (int i = 0; i < tableRun.ElementCount; i++) { var destination = model.ReadPointer(tableRun.Start + tableRun.ElementLength * i); if (!(model.GetNextRun(destination) is ISpriteRun run)) { return(defaultOptions); } var sprite = run.GetPixels(model, 0); var paletteAddress = SpriteTool.FindMatchingPalette(model, run, 0); var paletteRun = model.GetNextRun(paletteAddress) as IPaletteRun; var palette = paletteRun?.GetPalette(model, paletteRun.PaletteFormat.InitialBlankPages) ?? TileViewModel.CreateDefaultPalette(16); var image = SpriteTool.Render(sprite, palette, paletteRun?.PaletteFormat.InitialBlankPages ?? default, 0); var option = VisualComboOption.CreateFromSprite(defaultOptions[i].Text, image, sprite.GetLength(0), i); imageOptions.Add(option); } return(imageOptions); }
private int ParseLengthFromAnchor() { // length is based on another array int address = owner.GetAddressFromAnchor(new ModelDelta(), -1, LengthFromAnchor); if (address == Pointer.NULL) { // the requested name was unknown... length is zero for now return(0); } var run = owner.GetNextRun(address) as ArrayRun; if (run == null || run.Start != address) { // the requested name was not an array, or did not start where anticipated // length is zero for now return(0); } return(run.ElementCount); }
public TilemapTableRun(IDataModel model, string anchorName, ArrayRunElementSegment segment, TilemapMargins margins, int start, SortedSpan <int> sources = null) : base(start, sources) { this.model = model; TilemapAnchor = anchorName; Segment = segment; Margins = margins; var tilemapAddress = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, anchorName); var tilemap = model.GetNextRun(tilemapAddress) as ITilemapRun; if (tilemap != null) { var height = tilemap.SpriteFormat.TileHeight + margins.Top + margins.Bottom; var width = tilemap.SpriteFormat.TileWidth + margins.Left + margins.Right; ElementCount = height * margins.LengthMultiplier; ElementLength = width * Segment.Length; ElementContent = width.Range().Select(i => segment).ToArray(); } Length = ElementCount * ElementLength; FormatString = $"[{segment.SerializeFormat}]{anchorName}{Margins}"; }
public TilemapRun(IDataModel model, int start, TilemapFormat format, SortedSpan <int> sources = null) : base(start, sources) { Model = model; Format = format; string hint = null; var address = Model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, Format.MatchingTileset); if (address >= 0 && address < Model.Count) { var tileset = Model.GetNextRun(address) as ISpriteRun; if (tileset == null) { tileset = Model.GetNextRun(arrayTilesetAddress) as ISpriteRun; } if (tileset != null && !(tileset is LzTilemapRun)) { hint = tileset.SpriteFormat.PaletteHint; } } SpriteFormat = new SpriteFormat(format.BitsPerPixel, format.TileWidth, format.TileHeight, hint); }
public static ITableRun GetTable(this IDataModel model, string name) { var address = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, name); return(model.GetNextRun(address) as ITableRun); }
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)); }