mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-05-23 13:57:01 -05:00
Make the resize-clear smarter. When clearing out a format to add other formats at the end, clear the format at the beginning to preserve pointer information. Also, CFRU defaults to 17 bytes per ability name.
230 lines
12 KiB
C#
230 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);
|
|
}
|
|
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<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);
|
|
}
|
|
}
|
|
}
|