HexManiacAdvance/src/HexManiac.Core/Models/HardcodeTablesModel.cs
haven1433 a54b87db1b some initial logging
doesn't actually send the logs anywhere long-term yet, it just shows them in an optional tool. But I can now watch some navigation commands and anchor changes
2025-11-29 12:00:09 -06:00

568 lines
28 KiB
C#

using HavenSoft.HexManiac.Core.Models.Runs;
using System.Collections.Generic;
using System.Diagnostics;
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
FlySpawns = "data.maps.fly.spawn",
TradeTable = "data.pokemon.trades",
MapNameTable = "data.maps.names",
MapBankTable = "data.maps.banks",
RematchTable = "data.trainers.vsseeker",
RematchTableRSE = "data.trainers.rematches",
WildTableName = "data.pokemon.wild",
SpecialsTable = "scripts.commands.events.specials",
MoveDataTable = "data.pokemon.moves.stats.battle",
ItemsTableName = "data.items.stats",
MapLayoutTable = "data.maps.layouts",
BerryTableName = "data.items.berry.stats",
TypesTableName = "data.pokemon.type.names",
MoveNamesTable = "data.pokemon.moves.names",
FlyConnections = "data.maps.fly.connections",
PokeIconsTable = "graphics.pokemon.icons.sprites",
FontWidthTable = "graphics.text.font.black.width",
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.back",
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",
ContestTypesTable = "data.pokemon.type.contest.names",
PokePalettesTable = "graphics.pokemon.palettes.normal",
EvolutionTableName = "data.pokemon.evolutions",
TypeChartTableName = "data.pokemon.type.chart",
ShinyPalettesTable = "graphics.pokemon.palettes.shiny",
TrainerSpritesName = "graphics.trainers.sprites.front",
ItemImagesTableName = "graphics.items.sprites",
LevelMovesTableName = "data.pokemon.moves.levelup",
ItemEffectsTableName = "data.items.effects",
RegionalDexTableName = "data.pokedex.regional",
NationalDexTableName = "data.pokedex.national",
DecorationsTableName = "data.decorations.stats",
MultichoiceTableName = "scripts.text.multichoice",
MoveDescriptionsName = "data.pokemon.moves.descriptions",
BackupFontWidthTable = "graphics.text.font.default.width",
PokeIconPalettesTable = "graphics.pokemon.icons.palettes",
DefaultSpriteNamespace = "graphics.new.sprite",
TrainerClassNamesTable = "data.trainers.classes.names",
ConversionDexTableName = "data.pokedex.hoennToNational",
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",
FireRedFr = "BPRF0",
FireRedIt = "BPRI0",
EmeraldFr = "BPEF0",
EmeraldIt = "BPEI0";
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;
/// <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 List<string> LoadingMessages { get; } = new();
public HardcodeTablesModel(Singletons singletons, byte[] data, StoredMetadata metadata = null, bool devMode = false) : base(data, metadata, singletons, devMode) {
gameCode = this.GetGameCode();
if (metadata != null && !metadata.IsEmpty) {
InitializationWorkload = (singletons?.WorkDispatcher ?? InstantDispatch.Instance).RunBackgroundWork(() => Initialize(metadata));
return;
}
InitializationWorkload = (singletons?.WorkDispatcher ?? InstantDispatch.Instance).RunBackgroundWork(() => {
{
var noChangeDelta = new NoDataChangeDeltaModel();
if (singletons.GameReferenceConstants.TryGetValue(gameCode, out var referenceConstants)) {
metadata = DecodeConstantsFromReference(this, singletons.MetadataInfo, metadata, referenceConstants);
}
Initialize(metadata);
isCFRU = GetIsCFRU(this);
// 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,
FireRedFr,
FireRedIt,
EmeraldFr,
EmeraldIt,
"ABCD0", // for tests
};
foreach (var defaultMetadata in GetDefaultMetadatas(gameCode.PadRight(4).Substring(0, 4).ToLower(), gameCode.ToLower())) {
this.LoadMetadata(defaultMetadata);
}
if (gamesToDecode.Contains(gameCode)) {
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) {
// add evolution types
if (isCFRU && TryGetList(EvolutionMethodListName, out var evolutionmethods)) {
var newList = new List<string>(evolutionmethods);
newList.Add("Rain Or Fog");
newList.Add("Move Type"); // 17 type
newList.Add("Type in Party"); // 18 type
newList.Add("Map");
newList.Add("Male");
newList.Add("Female");
newList.Add("Level Night");
newList.Add("Level Day");
newList.Add("Hold Item Night"); // 24 hold item
newList.Add("Hold Item Day"); // 25 hold item
newList.Add("Move Name"); // 26 move name
newList.Add("Mon in Party"); // 27 species
newList.Add("Level Time Range");
newList.Add("Flag Set");
newList.Add("3 Critical Hits In One Battle");
newList.Add("Nature High");
newList.Add("Nature Low");
newList.Add("Damage Location");
newList.Add("Item Location");
while (newList.Count <= 252) newList.Add(null); // skip to gigantamax = 253 mega = 254
newList.Add("Gigantamax");
newList.Add("Mega");
SetList(new NoDataChangeDeltaModel(), EvolutionMethodListName, newList, null, StoredList.GenerateHash(newList));
}
// add move effects
if (isCFRU && TryGetList(MoveEffectListName, out var moveeffectsoptions)) {
var newList = new List<string>(moveeffectsoptions);
newList[12] = "RaiseSpeed1Primary";
newList[14] = "RaiseSpDefese1Primary";
newList[15] = "RaiseAccuracy1Primary";
newList[21] = "LowerSpAtk1Primary";
newList[22] = "LowerSpDef1Primary";
newList[25] = "RemoveStatChanges";
newList[55] = "RaiseAccuracy2Primary";
newList[56] = "RaiseEvasion2Primary";
newList[61] = "LowerSpAttack2Primary";
newList[63] = "LowerAccuracy2Primary";
newList[64] = "LowerEvasion2Primary";
newList[74] = "LowerEvasion1HitChance";
newList[77] = "Unused4D";
newList[78] = "Unused4E";
newList[96] = "RaiseSpeed1HitChance";
newList[110] = "RaiseSpAtk1HitChance";
newList[121] = "Unused79";
newList[123] = "Unused7B";
newList[125] = "BurnUp";
newList[135] = "RaiseDefense2HitChance";
newList[150] = "Unused96";
newList[169] = "UnusedA9";
newList[174] = "BoostNextElectricMoveAndRaiseSpDef";
newList[185] = "UnusedB9";
newList[196] = "UnusedC4";
newList[198] = "RaiseAttack1SpAtk1";
newList[199] = "RaiseAttack1Accuracy1";
newList[200] = "UnusedC8";
newList[202] = "UnusedCA";
newList[203] = "UnusedCB";
newList[207] = "RaiseAllStatsPrimary";
newList[213] = "Stat Swap or Split";
newList.AddRange(new string[] {null, null, null, null, "Me First", "Eat Berry", "Natural Gift", "Smack Down", "Remove Target Stat Changes", "SleepHitChance", null, null,
"Set Terrain", "Pledge", "Field Effects", "Fling", "Feint", "Attack Blockers", "Type Changes", "Heal Target", "Topsy Turvy Electrify", "Fairy Lock Happy Hour",
"Instruct After You Quash", "Sucker Punch", "Ignore Redirection", "Team Effects", "Camouflage", "Flame Burst", "Last Resort Sky Drop", "Damage Set Terrain", "Teatime",
"RaiseTarget\'sAttackSpAtk2"});
for (var k = 0; k < 7; k++) newList.Add(null); // 246 to 252 are unused.
newList.AddRange(new string[] { "Max Move", "Synchronoise" });
SetList(new NoDataChangeDeltaModel(), MoveEffectListName, newList, null, StoredList.GenerateHash(newList));
}
// add item hold effects
if (isCFRU && TryGetList("holdeffects", out var holdeffects)) {
var newList = new List<string>(holdeffects);
newList.AddRange(new[] {
"UnusedA", "RockyHelmet", "UnusedB", "AssaultVest",
"Eviolite", "Plate", "MegaStone", "LifeOrb",
"ToxicOrb", "FlameOrb", "BlackSludge", "SmoothRock",
"DampRock", "HeatRock", "IcyRock", "LightClay",
"WideLens", "SafetyGoggles", "WeaknessPolicy", "Drive",
"Memory", "AdamantOrb", "LustrousOrb", "GriseousOrb",
"DestinyKnot", "ExpertBelt", "PrimalOrb", "Gem",
"WeaknessBerry", "CustapBerry", "LaggingTail", "IronBall",
"BindingBand", "UnusedC", "ProtectivePads", "AbsorbBulb",
"AirBalloon", "Bigroot", "CellBattery", "EjectButton",
"FloatStone", "GripClaw", "LuminousMoss", "UnusedD",
"Metronome", "MuscleBand", "RedCard", "RingTarget",
"ShedShell", "Snowball", "StickyBarb", "TerrainExtender",
"WiseGlasses", "Seeds", "JabocaRowapBerry", "KeeBerry",
"MarangaBerry", "ZoomLens", "AdrenalineOrb", "PowerHerb",
"MicleBerry", "EnigmaBerry", "TypeBoosters", "ZCrystal",
"AbilityCapsule", "EjectPack", "RoomService", "BlunderPolicy",
"HeavyDutyBoots", "UtilityUmbrella", "ThroatSpray",
});
SetList(new NoDataChangeDeltaModel(), "holdeffects", newList, null, StoredList.GenerateHash(newList));
}
// Update the type chart effectivenesses list
if (isCFRU && TryGetList("effectiveness", out var multipliers)) {
var newMultipliers = new string[21]; // 0 to 20
newMultipliers[0] = "1x";
newMultipliers[1] = "0x";
newMultipliers[5] = "0.5x";
newMultipliers[20] = "2x";
SetList(new NoDataChangeDeltaModel(), "effectiveness", newMultipliers, null, StoredList.GenerateHash(newMultipliers));
}
foreach (var table in tables) {
// some tables have been removed from CFRU
if (isCFRU && CfruIgnoreTables.Contains(table.Name)) continue;
using (ModelCacheScope.CreateScope(this)) {
var format = table.Format;
AddTable(table.Address, table.Offset, table.Name, format);
}
}
if (isCFRU) SetupCFRUSpecificTablesAndConstants();
}
public static readonly IReadOnlyList<string> CfruIgnoreTables = new List<string>() {
"graphics.pokemon.sprites.coordinates.front",
"data.pokedex.hoennToNational", // causes problems with pokename count
"graphics.pokemon.sprites.anchor", // causes problems with pokename count
"data.pokedex.search.alpha" // causes problems with shiny palettes
};
public void SetupCFRUSpecificTablesAndConstants() {
ShowRawIVByteForTrainer = true;
// class-based pokeballs
var balls = new List<string> {
"Master Ball", "Ultra Ball", "Great Ball", "Poke Ball",
"Safari Ball", "Net Ball", "Dive Ball", "Nest Ball",
"Repeat Ball", "Timer Ball", "Luxury Ball", "Premier Ball",
"Dusk Ball", "Heal Ball", "Quick Ball", "Cherish Ball",
"Park Ball", "Fast Ball", "Level Ball", "Lure Ball",
"Heavy Ball", "Love Ball", "Friend Ball", "Moon Ball",
"Sport Ball", "Beast Ball", "Dream Ball",
};
while (balls.Count < 0xFE) balls.Add(null);
balls.Add("Class Based");
balls.Add("Random");
SetList(new NoDataChangeDeltaModel(), "trainerballs", balls, null, StoredList.GenerateHash(balls));
AddTable(0x1456790, 0, "data.trainers.classes.balls", "[ball.trainerballs]data.trainers.classes.names");
// randomizer restrictions
AddTable(0x1453828, 0, "data.randomizer.species.banlist", $"[species:{PokemonNameTable}]!FEFE");
AddTable(0x1454730, 0, "data.randomizer.ability.banlist", $"[ability.{AbilityNamesTable}]!FF");
// physical/special/split list
var pss = new[] { "Physical", "Special", "Status" };
SetList(new NoDataChangeDeltaModel(), "movecategory", pss, null, StoredList.GenerateHash(pss));
// trainers-with-EVs table
var trainerabilities = new List<string> { "Hidden", "Ability1", "Ability2", "RandomNormal", "RandomAny" };
SetList(new NoDataChangeDeltaModel(), "trainerabilities", trainerabilities, null, StoredList.GenerateHash(trainerabilities));
AddTable(0x1456798, 0, "data.trainers.evs", "[nature.data.pokemon.natures.names ivs. hpEv. atkEv. defEv. spdEv. spAtkEv. spDefEv. ball.data.trainers.classes.names ability.trainerabilities]121");
// trainer class-based encounter music
AddTable(0x144C110, 0, "data.trainers.classes.music", "[song:songnames]data.trainers.classes.names");
// trainer sprite-based mugshots
AddTable(0x144FC94, 0, "data.trainers.sprites.mugshots", "[sprite<`lzs4x8x8|data.trainers.sprites.mugshots`> pal<`lzp4`> size: x:|z y:|z unused:]graphics.trainers.sprites.front-0-1");
// kanto dex
// first hword is actually the length of the table - 1
// every hword after that is what pokemon appears in that slot of the dex (slot 1, slot 2, etc)
AddTable(0x160A52C, 0, "data.pokedex.kanto", "[mon:data.pokemon.names]152");
// PSS icons/palette
if (0x143B0EC < Count - 4) {
var spriteStart = ReadPointer(0x143B0EC);
AddTableDirect(spriteStart, "graphics.pokemon.moves.category.icons", "`ucs4x3x18|graphics.pokemon.moves.category.palette`");
AddTableDirect(spriteStart + 0x6C0, "graphics.pokemon.moves.category.palette", "`ucp4`");
}
// z-effects
var newList = new List<string>();
newList.Add("None");
newList.Add("Reset Stats");
newList.Add("All Stats Up 1");
newList.Add("Boost Crits");
newList.Add("Follow Me");
newList.Add("Curse");
newList.Add("Recover Hp");
newList.Add("Restore Replacement Hp");
newList.Add("Atk Up 1");
newList.Add("Def Up 1");
newList.Add("Spd Up 1");
newList.Add("Spatk Up 1");
newList.Add("Spdef Up 1");
newList.Add("Acc Up 1");
newList.Add("Evsn Up 1");
newList.Add("Atk Up 2");
newList.Add("Def Up 2");
newList.Add("Spd Up 2");
newList.Add("Spatk Up 2");
newList.Add("Spdef Up 2");
newList.Add("Acc Up 2");
newList.Add("Evsn Up 2");
newList.Add("Atk Up 3");
newList.Add("Def Up 3");
newList.Add("Spd Up 3");
newList.Add("Spatk Up 3");
newList.Add("Spdef Up 3");
newList.Add("Acc Up 3");
newList.Add("Evsn Up 3");
SetList(new NoDataChangeDeltaModel(), "zeffects", newList, null, StoredList.GenerateHash(newList));
}
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) {
if (constant.Any(c => c.Address + c.Length > model.Count)) continue;
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,
new StoredMetadataFields {
FreeSpaceSearch = metadata.FreeSpaceSearch,
FreeSpaceBuffer = metadata.FreeSpaceBuffer,
NextExportID = metadata.NextExportID,
ShowRawIVByteForTrainer = metadata.ShowRawIVByteForTrainer
});
}
/// <summary>
/// Find a table given a pointer to that table
/// The pointer at the source may not point directly to the table: it may point to an offset from the start of the table.
/// </summary>
private void AddTable(int source, int offset, string name, string format) {
var noChangeDelta = new NoDataChangeDeltaModel();
format = AdjustFormatForCFRU(name, format, ref source);
if (source < 0 || source > RawData.Length) return;
var destination = ReadPointer(source) - offset;
if (destination < 0 || destination > RawData.Length) return;
var interruptingSourceRun = GetNextRun(source);
if (interruptingSourceRun.Start < source && interruptingSourceRun.Start + interruptingSourceRun.Length > source) {
if (interruptingSourceRun is ITableRun tableRun) {
var tableOffset = tableRun.ConvertByteOffsetToArrayOffset(source);
if (tableOffset.SegmentOffset != 0) {
LoadingMessages.Add($"Failed to add table {name}: it conflicted with an existing table found at {tableRun.Start.ToAddress()}.");
return;
}
if (tableRun.ElementContent[tableOffset.SegmentIndex].Type != ElementContentType.Pointer) {
LoadingMessages.Add($"Failed to add table {name} at {destination.ToAddress()}: data in the table didn't match pointers in the file.");
return;
}
} else {
// the source isn't actually a pointer, we shouldn't write anything
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
LoadingMessages.Add($"When trying to add table {name}, had to clear existing format from {array.Start.ToAddress()} to {(array.Start + array.Length).ToAddress()} because a formatting conflict was found.");
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) {
var noChangeDelta = new NoDataChangeDeltaModel();
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 = 0x00051A, CFRU_Check_Value = 0x46C0, CFRU_ValueRepeateCount = 5;
public static bool GetIsCFRU(IDataModel model) {
var gameCode = model.GetGameCode();
if (gameCode != FireRed) return false;
if (model.RawData.Length < CFRU_Check_Address + 3) return false;
for (int i = 0; i < CFRU_ValueRepeateCount; i++) {
if (model.ReadMultiByteValue(CFRU_Check_Address + i * 2, 2) != CFRU_Check_Value) return false;
}
return true;
}
private string AdjustFormatForCFRU(string name, string format, ref int source) {
if (!isCFRU) return format;
// type names
if (name == TypesTableName) format = format.Split("]")[0] + "]24";
// type icons
if (name == "graphics.pokemon.type.icons") format = format.Replace("ucs4x16x16", "ucs4x16x18");
// remove the extra +28 from pokemon-related tables
if (format.EndsWith(PokemonNameTable + "+28")) format = format.Substring(0, format.Length - 3);
// remove the extra +1 from pokemon-related tables
if (format.EndsWith(PokemonNameTable + "+1")) format = format.Substring(0, format.Length - 2);
// hidden abilities stored in pokemon stats
if (name == PokemonStatsTable) format = format.Replace("padding:", $"hiddenAbility.{AbilityNamesTable} padding.");
// items: no constant
if (name == ItemsTableName) format = format.Replace("data.items.count", string.Empty);
// moves
if (name == MoveNamesTable) format += "896";
if (name == MoveDataTable) format = format.Replace("unused. unused:", "zMovePower. category.movecategory zMoveEffect.zeffects");
// level-up moves uses Jambo format
if (name == LevelMovesTableName) return $"[movesFromLevel<[move:{MoveNamesTable} level.]!0000FF>]{PokemonNameTable}";
// tms / tutors
if (name == MoveTutors) format = format.Replace("]15", "]128");
if (name == TmMoves) format = format.Replace("]58", "]128");
// graphics.pokemon.type.map[width. height. xy:|t|:|x:|.|y::]data.pokemon.type.names+1+2
if (name == "graphics.pokemon.type.map") format = format.Replace("data.pokemon.type.names+1+5", "data.pokemon.type.names+1+2");
// overworld sprites
if (name == OverworldSprites) format = format.Replace("graphics.overworld.tablelength", "240");
// 16 evolutions per pokemon
if (name == EvolutionTableName) {
var vars = "|".Join(new[] {
"6=" + ItemsTableName,
"7=" + ItemsTableName,
"17=" + TypesTableName,
"18=" + TypesTableName,
"24=" + ItemsTableName,
"25=" + ItemsTableName,
"26=" + MoveNamesTable,
"27=" + PokemonNameTable,
"254=" + ItemsTableName,
});
var methods = " ".Join("0123456789ABCDEF".Select(i => $"method{i}:evolutionmethods arg{i}:|s=method{i}({vars}) species{i}:{PokemonNameTable} value{i}:"));
return $"[{methods}]{PokemonNameTable}";
}
// roaming locations
if (name == "data.maps.roaming.sets") source = 0x14889B0;
if (name == ItemEffectsTableName) format = format.Replace("-199", string.Empty); // item effects are as long as the items
// type chart
if (name == "data.pokemon.type.chart") {
source = 0x145BB74;
format = "Norm Fight Fly Poison Grd Rock Bug Ghost Steel arg9 Fire Water Grass Elec Psych Ice Drag Dark arg18 arg19 arg20 arg21 arg22 Fairy";
format = " ".Join(format.Split(' ').Select(type => $"{type}.effectiveness"));
format = $"[{format}]data.pokemon.type.names";
}
return format;
}
}
}