HexManiacAdvance/src/HexManiac.Core/Models/HardcodeTablesModel.cs
Benjamin Popp a9318c00ea Bugfix: load tables based on constants correctly
Since tables are loaded from metadata before constants, loading metadata made the types load as a single-element table. This is wrong. Make it change the length of the type name table when it later loads the constants.

This should be true in the general case too, not just the startup case. When a new constant is loaded, if a table is known to have that length, update the table accordingly.

Add additional testing to verify that the constant is correct before adding it. This matters for a new TOML being created for an existing rom, where some of the values may've been changed.

Add additional verification that all the constants / table lengths actually match up after loading is complete.
2021-02-27 22:44:51 -06:00

236 lines
12 KiB
C#

using HavenSoft.HexManiac.Core.Models.Runs;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace HavenSoft.HexManiac.Core.Models {
/// <summary>
/// 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.
/// </summary>
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",
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 = "moveeffects",
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();
/// <summary>
/// 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).
/// </summary>
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 };
if (gamesToDecode.Contains(gameCode)) {
foreach (var defaultMetadata in GetDefaultMetadatas(gameCode.Substring(0, 4).ToLower(), gameCode.ToLower())) {
this.LoadMetadata(defaultMetadata);
}
DecodeHeader();
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 DecodeHeader() {
if (!gameCode.IsAny(Ruby, Sapphire, Ruby1_1, Sapphire1_1)) {
ObserveAnchorWritten(noChangeDelta, "data.header.romname", new AsciiRun(0x108, 0x20));
}
}
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}";
AddTable(table.Address, table.Offset, table.Name, format);
}
}
}
public static StoredMetadata DecodeConstantsFromReference(IReadOnlyList<byte> model, IMetadataInfo info, StoredMetadata metadata, GameReferenceConstants constants) {
if (metadata == null) return metadata;
var words = metadata.MatchedWords.ToList();
var constantSet = new Dictionary<string, IList<StoredMatchedWord>>();
foreach (var constant in constants.SelectMany(c => c.ToStoredMatchedWords())) {
if (!constantSet.ContainsKey(constant.Name)) constantSet[constant.Name] = new List<StoredMatchedWord>();
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, info, metadata.FreeSpaceSearch, metadata.FreeSpaceBuffer, metadata.NextExportID);
}
/// <summary>
/// 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.
/// </summary>
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));
}
/// <summary>
/// Find a table given an address for that table
/// </summary>
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);
}
}
}