mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-05-24 14:28:04 -05:00
Initialization happens on a background thread. We want to be able to run commands when initialization is complete. The way this was done before wasn't very thread safe. The new version will always run the command. * If initialization is complete, it runs the command right away. * If initialization is not complete, it defers execution until initialization is done. * Initialization is measured in scopes. An inner scope will not run initialization work when it's done, since there's still more initialization work that needs to be completed. * Things like "goto" will now defer execution until initialization is complete. For most users, this won't matter. For speed users, it means that goto will now work the way they expect. Streams now have the ability to express a "visualization" of the stream, which is a list of PixelViewModels. The table tool will display those in the background of the textbox, over to the right.
229 lines
11 KiB
C#
229 lines
11 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(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(IMetadataInfo info, StoredMetadata metadata, GameReferenceConstants constants) {
|
|
if (metadata == null) return metadata;
|
|
var words = metadata.MatchedWords.ToList();
|
|
foreach (var constant in constants) {
|
|
words.AddRange(constant.ToStoredMatchedWords());
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|