using HavenSoft.HexManiac.Core.Models.Runs; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; namespace HavenSoft.HexManiac.Core.Models { /// /// An alternative to the AutoSearchModel. /// Instead of using a 'smart' search algorithm to find all the data, /// follow hard-coded expected pointers to the known data. /// This should still be somewhat robust: the data may move, but the pointers to the data are more likely to be stable. /// /// 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 WildTableName = "data.pokemon.wild", SpecialsTable = "scripts.specials.thumb", MoveDataTable = "data.pokemon.moves.stats.battle", ItemsTableName = "data.items.stats", TypesTableName = "data.pokemon.type.names", MoveNamesTable = "data.pokemon.moves.names", PokeIconsTable = "graphics.pokemon.icons.sprites", DexInfoTableName = "data.pokedex.stats", PokemonNameTable = "data.pokemon.names", TrainerTableName = "data.trainers.stats", MoveEffectsTable = "scripts.moves.effects", NaturesTableName = "data.pokemon.natures.names", OverworldSprites = "graphics.overworld.sprites", BallSpritesTable = "graphics.items.ball.sprites", BackSpritesTable = "graphics.pokemon.sprites.front", PokemonStatsTable = "data.pokemon.stats", AbilityNamesTable = "data.abilities.names", EggMovesTableName = "data.pokemon.moves.egg", OverworldPalettes = "graphics.overworld.palettes", BallPalettesTable = "graphics.items.ball.palettes", FrontSpritesTable = "graphics.pokemon.sprites.front", PokePalettesTable = "graphics.pokemon.palettes.normal", ShinyPalettesTable = "graphics.pokemon.palettes.normal", EvolutionTableName = "data.pokemon.evolutions", TypeChartTableName = "data.pokemon.type.chart", ItemImagesTableName = "graphics.items.sprites", LevelMovesTableName = "data.pokemon.moves.levelup", MultichoiceTableName = "scripts.text.multichoice", DecorationsTableName = "data.decorations.stats", RegionalDexTableName = "data.pokedex.regional", NationalDexTableName = "data.pokedex.national", MoveDescriptionsName = "data.pokemon.moves.descriptions", PokeIconPalettesTable = "graphics.pokemon.icons.palettes", ConversionDexTableName = "data.pokedex.hoennToNational", TrainerClassNamesTable = "data.trainers.classes.names", DefaultSpriteNamespace = "graphics.new.sprite", DefaultTilemapNamespace = "graphics.new.tilemap", DefaultPaletteNamespace = "graphics.new.palette", AbilityDescriptionsTable = "data.abilities.descriptions", PokeIconPaletteIndexTable = "graphics.pokemon.icons.index", BattleParticleSpriteTable = "graphics.moves.particles.sprites", BattleParticlePaletteTable = "graphics.moves.particles.palettes"; public const string MoveInfoListName = "moveinfo", MoveEffectListName = "moveeffectoptions", MoveTargetListName = "movetarget", EvolutionMethodListName = "evolutionmethods", DecorationsShapeListName = "decorshape", DecorationsCategoryListName = "decorcategory", DecorationsPermissionListName = "decorpermissions"; public const string Ruby = "AXVE0", Sapphire = "AXPE0", Emerald = "BPEE0", FireRed = "BPRE0", LeafGreen = "BPGE0", Ruby1_1 = "AXVE1", Sapphire1_1 = "AXPE1", FireRed1_1 = "BPRE1", LeafGreen1_1 = "BPGE1"; public const string TmMoves = "data.pokemon.moves.tms", HmMoves = "data.pokemon.moves.hms", TmCompatibility = "data.pokemon.moves.tmcompatibility", MoveTutors = "data.pokemon.moves.tutors", TutorCompatibility = "data.pokemon.moves.tutorcompatibility"; private readonly string gameCode; private readonly ModelDelta noChangeDelta = new NoDataChangeDeltaModel(); /// /// The first 0x100 bytes of the GBA rom is always the header. /// The next 0x100 bytes contains some tables and some startup code, but nothing interesting to point to. /// Choosing 0x200 might prevent us from seeing an actual anchor, but it will also remove a bunch /// of false positives and keep us from getting conflicts with the RomName (see DecodeHeader). /// public override int EarliestAllowedAnchor => 0x200; public HardcodeTablesModel(Singletons singletons, byte[] data, StoredMetadata metadata = null) : base(data, metadata, singletons) { gameCode = this.GetGameCode(); if (metadata != null && !metadata.IsEmpty) { (singletons?.WorkDispatcher ?? InstantDispatch.Instance).RunBackgroundWork(() => Initialize(metadata)); return; } (singletons?.WorkDispatcher ?? InstantDispatch.Instance).RunBackgroundWork(() => { using (CreateInitializeScope()) { if (singletons.GameReferenceConstants.TryGetValue(gameCode, out var referenceConstants)) { metadata = DecodeConstantsFromReference(this, singletons.MetadataInfo, metadata, referenceConstants); } Initialize(metadata); isCFRU = GetIsCFRU(); // in vanilla emerald, this pointer isn't four-byte aligned // it's at the very front of the ROM, so if there's no metadata we can be pretty sure that the pointer is still there if (gameCode == Emerald && data.Length > EarliestAllowedAnchor && data[0x1C3] == 0x08) ObserveRunWritten(noChangeDelta, new PointerRun(0x1C0)); var gamesToDecode = new[] { Ruby, Sapphire, Emerald, FireRed, LeafGreen, Ruby1_1, Sapphire1_1, FireRed1_1, LeafGreen1_1, "BPRF0" }; if (gamesToDecode.Contains(gameCode)) { foreach (var defaultMetadata in GetDefaultMetadatas(gameCode.Substring(0, 4).ToLower(), gameCode.ToLower())) { this.LoadMetadata(defaultMetadata); } if (singletons.GameReferenceTables.TryGetValue(gameCode, out var referenceTables)) { DecodeTablesFromReference(referenceTables); } } ResolveConflicts(); } }); } [Conditional("DEBUG")] private void CheckForEmptyAnchors(int destination, string anchor) { var run = GetNextRun(destination); Debug.Assert(run.PointerSources == null || run.PointerSources.Count > 0, $"{anchor} refers to {destination:X6}, but has no pointers. So how did we find it?"); } private void DecodeTablesFromReference(GameReferenceTables tables) { foreach (var table in tables) { if (isCFRU && table.Name == "graphics.pokemon.sprites.coordinates.front") continue; if (isCFRU && table.Name == EggMovesTableName) continue; using (ModelCacheScope.CreateScope(this)) { var format = table.Format; if (isCFRU && table.Name == LevelMovesTableName) format = $"[movesFromLevel<[move:{MoveNamesTable} level.]!0000FF>]{PokemonNameTable}"; if (isCFRU && table.Name == AbilityNamesTable) format = $"[name\"\"17]"; AddTable(table.Address, table.Offset, table.Name, format); } } } public static StoredMetadata DecodeConstantsFromReference(IReadOnlyList model, IMetadataInfo info, StoredMetadata metadata, GameReferenceConstants constants) { if (metadata == null) return metadata; var words = metadata.MatchedWords.ToList(); var constantSet = new Dictionary>(); foreach (var constant in constants.SelectMany(c => c.ToStoredMatchedWords())) { if (!constantSet.ContainsKey(constant.Name)) constantSet[constant.Name] = new List(); constantSet[constant.Name].Add(constant); } foreach (var constant in constantSet.Values) { var virtualValues = constant.Select(c => (model.ReadMultiByteValue(c.Address, c.Length) - c.AddOffset) / c.MultOffset).ToList(); var match = virtualValues.All(vv => vv == virtualValues[0]); if (match) words.AddRange(constant); } return new StoredMetadata(metadata.NamedAnchors, metadata.UnmappedPointers, words, metadata.OffsetPointers, metadata.Lists, metadata.UnmappedConstants, metadata.GotoShortcuts, info, metadata.FreeSpaceSearch, metadata.FreeSpaceBuffer, metadata.NextExportID); } /// /// Find a table given a pointer to that table /// The pointer at the source may not point directly to the table: it make point to an offset from the start of the table. /// private void AddTable(int source, int offset, string name, string format) { format = AdjustFormatForCFRU(format); if (source < 0 || source > RawData.Length) return; var destination = ReadPointer(source) - offset; if (destination < 0 || destination > RawData.Length) return; var interruptingRun = GetNextRun(destination); if (interruptingRun.Start < destination && interruptingRun is ArrayRun array) { var elementCount = (destination - array.Start) / array.ElementLength; var desiredChange = elementCount - array.ElementCount; while (!string.IsNullOrEmpty(array.LengthFromAnchor)) { if (GetNextRun(GetAddressFromAnchor(noChangeDelta, -1, array.LengthFromAnchor)) is ArrayRun nextArray) { array = nextArray; } else { break; } } if (array.ElementCount + desiredChange <= 0) { // erase the entire run ClearFormat(noChangeDelta, array.Start, array.Length); } else { var arrayName = GetAnchorFromAddress(-1, array.Start); array = array.Append(noChangeDelta, desiredChange); // if append is negative, the name might get erased. Store it. ObserveAnchorWritten(noChangeDelta, arrayName, array); } } AddTableDirect(destination, name, format, validatePointerFound: offset == 0); if (offset != 0) ObserveRunWritten(noChangeDelta, new OffsetPointerRun(source, offset)); } /// /// Find a table given an address for that table /// private void AddTableDirect(int destination, string name, string format, bool validatePointerFound = false) { using (ModelCacheScope.CreateScope(this)) { var errorInfo = ApplyAnchor(this, noChangeDelta, destination, "^" + name + format, allowAnchorOverwrite: true); validatePointerFound &= !errorInfo.HasError; } if (validatePointerFound) { CheckForEmptyAnchors(destination, name); } } private bool isCFRU; private const int CFRU_Check_Address = 0x03436C, CFRU_Check_Value = 0x47004800; private bool GetIsCFRU() { if (gameCode != FireRed) return false; if (RawData.Length < CFRU_Check_Address + 3) return false; return ReadValue(CFRU_Check_Address) == CFRU_Check_Value; } private string AdjustFormatForCFRU(string format) { if (!isCFRU || !format.EndsWith("+28")) return format; return format.Substring(0, format.Length - 3); } } }