diff options
author | Rafael Marçalo <public@rafaelmarcalo.xyz> | 2024-09-05 16:31:33 +0100 |
---|---|---|
committer | Rafael Marçalo <public@rafaelmarcalo.xyz> | 2024-09-05 16:31:33 +0100 |
commit | 8b67572ad7e1508341345dc46a2597e9fa170cbb (patch) | |
tree | 8f37c4d60ce0f07b9eaf30be34f39298da97b242 /src/com/pkrandom/romhandlers/Gen3RomHandler.java | |
parent | b65f4a80da28e7ec4de16c8b1abf906e8d7be2c5 (diff) |
Diffstat (limited to 'src/com/pkrandom/romhandlers/Gen3RomHandler.java')
-rwxr-xr-x | src/com/pkrandom/romhandlers/Gen3RomHandler.java | 4473 |
1 files changed, 4473 insertions, 0 deletions
diff --git a/src/com/pkrandom/romhandlers/Gen3RomHandler.java b/src/com/pkrandom/romhandlers/Gen3RomHandler.java new file mode 100755 index 0000000..838315d --- /dev/null +++ b/src/com/pkrandom/romhandlers/Gen3RomHandler.java @@ -0,0 +1,4473 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- Gen3RomHandler.java - randomizer handler for R/S/E/FR/LG. --*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.pkrandom.*; +import com.pkrandom.constants.*; +import com.pkrandom.exceptions.RandomizationException; +import com.pkrandom.exceptions.RandomizerIOException; +import com.pkrandom.pokemon.*; +import compressors.DSDecmp; + +public class Gen3RomHandler extends AbstractGBRomHandler { + + public static class Factory extends RomHandler.Factory { + + @Override + public Gen3RomHandler create(Random random, PrintStream logStream) { + return new Gen3RomHandler(random, logStream); + } + + public boolean isLoadable(String filename) { + long fileLength = new File(filename).length(); + if (fileLength > 32 * 1024 * 1024) { + return false; + } + byte[] loaded = loadFilePartial(filename, 0x100000); + // nope + return loaded.length != 0 && detectRomInner(loaded, (int) fileLength); + } + } + + public Gen3RomHandler(Random random) { + super(random, null); + } + + public Gen3RomHandler(Random random, PrintStream logStream) { + super(random, logStream); + } + + private static class RomEntry { + private String name; + private String romCode; + private String tableFile; + private int version; + private int romType; + private boolean copyStaticPokemon; + private Map<String, Integer> entries = new HashMap<>(); + private Map<String, int[]> arrayEntries = new HashMap<>(); + private Map<String, String> strings = new HashMap<>(); + private List<StaticPokemon> staticPokemon = new ArrayList<>(); + private List<StaticPokemon> roamingPokemon = new ArrayList<>(); + private List<TMOrMTTextEntry> tmmtTexts = new ArrayList<>(); + private Map<String, String> codeTweaks = new HashMap<String, String>(); + private long expectedCRC32 = -1; + + public RomEntry() { + + } + + public RomEntry(RomEntry toCopy) { + this.name = toCopy.name; + this.romCode = toCopy.romCode; + this.tableFile = toCopy.tableFile; + this.version = toCopy.version; + this.romType = toCopy.romType; + this.copyStaticPokemon = toCopy.copyStaticPokemon; + this.entries.putAll(toCopy.entries); + this.arrayEntries.putAll(toCopy.arrayEntries); + this.strings.putAll(toCopy.strings); + this.staticPokemon.addAll(toCopy.staticPokemon); + this.roamingPokemon.addAll(toCopy.roamingPokemon); + this.tmmtTexts.addAll(toCopy.tmmtTexts); + this.codeTweaks.putAll(toCopy.codeTweaks); + this.expectedCRC32 = toCopy.expectedCRC32; + } + + private int getValue(String key) { + if (!entries.containsKey(key)) { + entries.put(key, 0); + } + return entries.get(key); + } + + private String getString(String key) { + if (!strings.containsKey(key)) { + strings.put(key, ""); + } + return strings.get(key); + } + } + + private static class TMOrMTTextEntry { + private int number; + private int mapBank, mapNumber; + private int personNum; + private int offsetInScript; + private int actualOffset; + private String template; + private boolean isMoveTutor; + } + + private static List<RomEntry> roms; + + static { + loadROMInfo(); + } + + private static void loadROMInfo() { + roms = new ArrayList<>(); + RomEntry current = null; + try { + Scanner sc = new Scanner(FileFunctions.openConfig("gen3_offsets.ini"), "UTF-8"); + while (sc.hasNextLine()) { + String q = sc.nextLine().trim(); + if (q.contains("//")) { + q = q.substring(0, q.indexOf("//")).trim(); + } + if (!q.isEmpty()) { + if (q.startsWith("[") && q.endsWith("]")) { + // New rom + current = new RomEntry(); + current.name = q.substring(1, q.length() - 1); + roms.add(current); + } else { + String[] r = q.split("=", 2); + if (r.length == 1) { + System.err.println("invalid entry " + q); + continue; + } + if (r[1].endsWith("\r\n")) { + r[1] = r[1].substring(0, r[1].length() - 2); + } + r[1] = r[1].trim(); + // Static Pokemon? + if (r[0].equals("StaticPokemon{}")) { + current.staticPokemon.add(parseStaticPokemon(r[1])); + } else if (r[0].equals("RoamingPokemon{}")) { + current.roamingPokemon.add(parseStaticPokemon(r[1])); + } else if (r[0].equals("TMText[]")) { + if (r[1].startsWith("[") && r[1].endsWith("]")) { + String[] parts = r[1].substring(1, r[1].length() - 1).split(",", 6); + TMOrMTTextEntry tte = new TMOrMTTextEntry(); + tte.number = parseRIInt(parts[0]); + tte.mapBank = parseRIInt(parts[1]); + tte.mapNumber = parseRIInt(parts[2]); + tte.personNum = parseRIInt(parts[3]); + tte.offsetInScript = parseRIInt(parts[4]); + tte.template = parts[5]; + tte.isMoveTutor = false; + current.tmmtTexts.add(tte); + } + } else if (r[0].equals("MoveTutorText[]")) { + if (r[1].startsWith("[") && r[1].endsWith("]")) { + String[] parts = r[1].substring(1, r[1].length() - 1).split(",", 6); + TMOrMTTextEntry tte = new TMOrMTTextEntry(); + tte.number = parseRIInt(parts[0]); + tte.mapBank = parseRIInt(parts[1]); + tte.mapNumber = parseRIInt(parts[2]); + tte.personNum = parseRIInt(parts[3]); + tte.offsetInScript = parseRIInt(parts[4]); + tte.template = parts[5]; + tte.isMoveTutor = true; + current.tmmtTexts.add(tte); + } + } else if (r[0].equals("Game")) { + current.romCode = r[1]; + } else if (r[0].equals("Version")) { + current.version = parseRIInt(r[1]); + } else if (r[0].equals("Type")) { + if (r[1].equalsIgnoreCase("Ruby")) { + current.romType = Gen3Constants.RomType_Ruby; + } else if (r[1].equalsIgnoreCase("Sapp")) { + current.romType = Gen3Constants.RomType_Sapp; + } else if (r[1].equalsIgnoreCase("Em")) { + current.romType = Gen3Constants.RomType_Em; + } else if (r[1].equalsIgnoreCase("FRLG")) { + current.romType = Gen3Constants.RomType_FRLG; + } else { + System.err.println("unrecognised rom type: " + r[1]); + } + } else if (r[0].equals("TableFile")) { + current.tableFile = r[1]; + } else if (r[0].equals("CopyStaticPokemon")) { + int csp = parseRIInt(r[1]); + current.copyStaticPokemon = (csp > 0); + } else if (r[0].equals("CRC32")) { + current.expectedCRC32 = parseRILong("0x" + r[1]); + } else if (r[0].endsWith("Tweak")) { + current.codeTweaks.put(r[0], r[1]); + } else if (r[0].equals("CopyFrom")) { + for (RomEntry otherEntry : roms) { + if (r[1].equalsIgnoreCase(otherEntry.name)) { + // copy from here + current.arrayEntries.putAll(otherEntry.arrayEntries); + current.entries.putAll(otherEntry.entries); + current.strings.putAll(otherEntry.strings); + boolean cTT = (current.getValue("CopyTMText") == 1); + if (current.copyStaticPokemon) { + current.staticPokemon.addAll(otherEntry.staticPokemon); + current.roamingPokemon.addAll(otherEntry.roamingPokemon); + current.entries.put("StaticPokemonSupport", 1); + } else { + current.entries.put("StaticPokemonSupport", 0); + } + if (cTT) { + current.tmmtTexts.addAll(otherEntry.tmmtTexts); + } + current.tableFile = otherEntry.tableFile; + } + } + } else if (r[0].endsWith("Locator") || r[0].endsWith("Prefix")) { + current.strings.put(r[0], r[1]); + } else { + if (r[1].startsWith("[") && r[1].endsWith("]")) { + String[] offsets = r[1].substring(1, r[1].length() - 1).split(","); + if (offsets.length == 1 && offsets[0].trim().isEmpty()) { + current.arrayEntries.put(r[0], new int[0]); + } else { + int[] offs = new int[offsets.length]; + int c = 0; + for (String off : offsets) { + offs[c++] = parseRIInt(off); + } + current.arrayEntries.put(r[0], offs); + } + } else { + int offs = parseRIInt(r[1]); + current.entries.put(r[0], offs); + } + } + } + } + } + sc.close(); + } catch (FileNotFoundException e) { + System.err.println("File not found!"); + } + + } + + private static int parseRIInt(String off) { + int radix = 10; + off = off.trim().toLowerCase(); + if (off.startsWith("0x") || off.startsWith("&h")) { + radix = 16; + off = off.substring(2); + } + try { + return Integer.parseInt(off, radix); + } catch (NumberFormatException ex) { + System.err.println("invalid base " + radix + "number " + off); + return 0; + } + } + + private static long parseRILong(String off) { + int radix = 10; + off = off.trim().toLowerCase(); + if (off.startsWith("0x") || off.startsWith("&h")) { + radix = 16; + off = off.substring(2); + } + try { + return Long.parseLong(off, radix); + } catch (NumberFormatException ex) { + System.err.println("invalid base " + radix + "number " + off); + return 0; + } + } + + private static StaticPokemon parseStaticPokemon(String staticPokemonString) { + StaticPokemon sp = new StaticPokemon(); + String pattern = "[A-z]+=\\[(0x[0-9a-fA-F]+,?\\s?)+]"; + Pattern r = Pattern.compile(pattern); + Matcher m = r.matcher(staticPokemonString); + while (m.find()) { + String[] segments = m.group().split("="); + String[] romOffsets = segments[1].substring(1, segments[1].length() - 1).split(","); + int[] offsets = new int [romOffsets.length]; + for (int i = 0; i < offsets.length; i++) { + offsets[i] = parseRIInt(romOffsets[i]); + } + switch (segments[0]) { + case "Species": + sp.speciesOffsets = offsets; + break; + case "Level": + sp.levelOffsets = offsets; + break; + } + } + return sp; + } + + private void loadTextTable(String filename) { + try { + Scanner sc = new Scanner(FileFunctions.openConfig(filename + ".tbl"), "UTF-8"); + while (sc.hasNextLine()) { + String q = sc.nextLine(); + if (!q.trim().isEmpty()) { + String[] r = q.split("=", 2); + if (r[1].endsWith("\r\n")) { + r[1] = r[1].substring(0, r[1].length() - 2); + } + tb[Integer.parseInt(r[0], 16)] = r[1]; + d.put(r[1], (byte) Integer.parseInt(r[0], 16)); + } + } + sc.close(); + } catch (FileNotFoundException e) { + System.err.println("File not found!"); + } + + } + + // This ROM's data + private Pokemon[] pokes, pokesInternal; + private List<Pokemon> pokemonList; + private int numRealPokemon; + private Move[] moves; + private boolean jamboMovesetHack; + private RomEntry romEntry; + private boolean havePatchedObedience; + private String[] tb; + public Map<String, Byte> d; + private String[] abilityNames; + private String[] itemNames; + private boolean mapLoadingDone; + private List<Integer> itemOffs; + private String[][] mapNames; + private boolean isRomHack; + private int[] internalToPokedex, pokedexToInternal; + private int pokedexCount; + private String[] pokeNames; + private ItemList allowedItems, nonBadItems; + private int pickupItemsTableOffset; + private long actualCRC32; + private boolean effectivenessUpdated; + + @Override + public boolean detectRom(byte[] rom) { + return detectRomInner(rom, rom.length); + } + + private static boolean detectRomInner(byte[] rom, int romSize) { + if (romSize != Gen3Constants.size8M && romSize != Gen3Constants.size16M && romSize != Gen3Constants.size32M) { + return false; // size check + } + // Special case for Emerald unofficial translation + if (romName(rom, Gen3Constants.unofficialEmeraldROMName)) { + // give it a rom code so it can be detected + rom[Gen3Constants.romCodeOffset] = 'B'; + rom[Gen3Constants.romCodeOffset + 1] = 'P'; + rom[Gen3Constants.romCodeOffset + 2] = 'E'; + rom[Gen3Constants.romCodeOffset + 3] = 'T'; + rom[Gen3Constants.headerChecksumOffset] = 0x66; + } + // Wild Pokemon header + if (find(rom, Gen3Constants.wildPokemonPointerPrefix) == -1) { + return false; + } + // Map Banks header + if (find(rom, Gen3Constants.mapBanksPointerPrefix) == -1) { + return false; + } + // Pokedex Order header + if (findMultiple(rom, Gen3Constants.pokedexOrderPointerPrefix).size() != 3) { + return false; + } + for (RomEntry re : roms) { + if (romCode(rom, re.romCode) && (rom[Gen3Constants.romVersionOffset] & 0xFF) == re.version) { + return true; // match + } + } + return false; // GBA rom we don't support yet + } + + @Override + public void loadedRom() { + for (RomEntry re : roms) { + if (romCode(rom, re.romCode) && (rom[0xBC] & 0xFF) == re.version) { + romEntry = new RomEntry(re); // clone so we can modify + break; + } + } + + tb = new String[256]; + d = new HashMap<>(); + isRomHack = false; + jamboMovesetHack = false; + + // Pokemon count stuff, needs to be available first + List<Integer> pokedexOrderPrefixes = findMultiple(rom, Gen3Constants.pokedexOrderPointerPrefix); + romEntry.entries.put("PokedexOrder", readPointer(pokedexOrderPrefixes.get(1) + 16)); + + // Pokemon names offset + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + int baseNomOffset = find(rom, Gen3Constants.rsPokemonNamesPointerSuffix); + romEntry.entries.put("PokemonNames", readPointer(baseNomOffset - 4)); + romEntry.entries.put( + "FrontSprites", + readPointer(findPointerPrefixAndSuffix(Gen3Constants.rsFrontSpritesPointerPrefix, + Gen3Constants.rsFrontSpritesPointerSuffix))); + romEntry.entries.put( + "PokemonPalettes", + readPointer(findPointerPrefixAndSuffix(Gen3Constants.rsPokemonPalettesPointerPrefix, + Gen3Constants.rsPokemonPalettesPointerSuffix))); + } else { + romEntry.entries.put("PokemonNames", readPointer(Gen3Constants.efrlgPokemonNamesPointer)); + romEntry.entries.put("MoveNames", readPointer(Gen3Constants.efrlgMoveNamesPointer)); + romEntry.entries.put("AbilityNames", readPointer(Gen3Constants.efrlgAbilityNamesPointer)); + romEntry.entries.put("ItemData", readPointer(Gen3Constants.efrlgItemDataPointer)); + romEntry.entries.put("MoveData", readPointer(Gen3Constants.efrlgMoveDataPointer)); + romEntry.entries.put("PokemonStats", readPointer(Gen3Constants.efrlgPokemonStatsPointer)); + romEntry.entries.put("FrontSprites", readPointer(Gen3Constants.efrlgFrontSpritesPointer)); + romEntry.entries.put("PokemonPalettes", readPointer(Gen3Constants.efrlgPokemonPalettesPointer)); + romEntry.entries.put("MoveTutorCompatibility", + romEntry.getValue("MoveTutorData") + romEntry.getValue("MoveTutorMoves") * 2); + } + + loadTextTable(romEntry.tableFile); + + if (romEntry.romCode.equals("BPRE") && romEntry.version == 0) { + basicBPRE10HackSupport(); + } + + loadPokemonNames(); + loadPokedex(); + loadPokemonStats(); + constructPokemonList(); + populateEvolutions(); + loadMoves(); + + // Get wild Pokemon offset + int baseWPOffset = findMultiple(rom, Gen3Constants.wildPokemonPointerPrefix).get(0); + romEntry.entries.put("WildPokemon", readPointer(baseWPOffset + 12)); + + // map banks + int baseMapsOffset = findMultiple(rom, Gen3Constants.mapBanksPointerPrefix).get(0); + romEntry.entries.put("MapHeaders", readPointer(baseMapsOffset + 12)); + this.determineMapBankSizes(); + + // map labels + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + int baseMLOffset = find(rom, Gen3Constants.frlgMapLabelsPointerPrefix); + romEntry.entries.put("MapLabels", readPointer(baseMLOffset + 12)); + } else { + int baseMLOffset = find(rom, Gen3Constants.rseMapLabelsPointerPrefix); + romEntry.entries.put("MapLabels", readPointer(baseMLOffset + 12)); + } + + mapLoadingDone = false; + loadAbilityNames(); + loadItemNames(); + + allowedItems = Gen3Constants.allowedItems.copy(); + nonBadItems = Gen3Constants.getNonBadItems(romEntry.romType).copy(); + + actualCRC32 = FileFunctions.getCRC32(rom); + } + + private int findPointerPrefixAndSuffix(String prefix, String suffix) { + if (prefix.length() % 2 != 0 || suffix.length() % 2 != 0) { + return -1; + } + byte[] searchPref = new byte[prefix.length() / 2]; + for (int i = 0; i < searchPref.length; i++) { + searchPref[i] = (byte) Integer.parseInt(prefix.substring(i * 2, i * 2 + 2), 16); + } + byte[] searchSuff = new byte[suffix.length() / 2]; + for (int i = 0; i < searchSuff.length; i++) { + searchSuff[i] = (byte) Integer.parseInt(suffix.substring(i * 2, i * 2 + 2), 16); + } + if (searchPref.length >= searchSuff.length) { + // Prefix first + List<Integer> offsets = RomFunctions.search(rom, searchPref); + if (offsets.size() == 0) { + return -1; + } + for (int prefOffset : offsets) { + if (prefOffset + 4 + searchSuff.length > rom.length) { + continue; // not enough room for this to be valid + } + int ptrOffset = prefOffset + searchPref.length; + int pointerValue = readPointer(ptrOffset); + if (pointerValue < 0 || pointerValue >= rom.length) { + // Not a valid pointer + continue; + } + boolean suffixMatch = true; + for (int i = 0; i < searchSuff.length; i++) { + if (rom[ptrOffset + 4 + i] != searchSuff[i]) { + suffixMatch = false; + break; + } + } + if (suffixMatch) { + return ptrOffset; + } + } + return -1; // No match + } else { + // Suffix first + List<Integer> offsets = RomFunctions.search(rom, searchSuff); + if (offsets.size() == 0) { + return -1; + } + for (int suffOffset : offsets) { + if (suffOffset - 4 - searchPref.length < 0) { + continue; // not enough room for this to be valid + } + int ptrOffset = suffOffset - 4; + int pointerValue = readPointer(ptrOffset); + if (pointerValue < 0 || pointerValue >= rom.length) { + // Not a valid pointer + continue; + } + boolean prefixMatch = true; + for (int i = 0; i < searchPref.length; i++) { + if (rom[ptrOffset - searchPref.length + i] != searchPref[i]) { + prefixMatch = false; + break; + } + } + if (prefixMatch) { + return ptrOffset; + } + } + return -1; // No match + } + } + + private void basicBPRE10HackSupport() { + if (basicBPRE10HackDetection()) { + this.isRomHack = true; + // NUMBER OF POKEMON DETECTION + + // this is the most annoying bit + // we'll try to get it from the pokemon names, + // and sanity check it using other things + // this of course means we can't support + // any hack with extended length names + + int iPokemonCount = 0; + int namesOffset = romEntry.getValue("PokemonNames"); + int nameLen = romEntry.getValue("PokemonNameLength"); + while (true) { + int nameOffset = namesOffset + (iPokemonCount + 1) * nameLen; + int nameStrLen = lengthOfStringAt(nameOffset); + if (nameStrLen > 0 && nameStrLen < nameLen && rom[nameOffset] != 0) { + iPokemonCount++; + } else { + break; + } + } + + // Is there an unused egg slot at the end? + String lastName = readVariableLengthString(namesOffset + iPokemonCount * nameLen); + if (lastName.equals("?") || lastName.equals("-")) { + iPokemonCount--; + } + + // Jambo's Moves Learnt table hack? + // need to check this before using moveset pointers + int movesetsTable; + if (readLong(0x3EB20) == 0x47084918) { + // Hack applied, adjust accordingly + int firstRoutinePtr = readPointer(0x3EB84); + movesetsTable = readPointer(firstRoutinePtr + 75); + jamboMovesetHack = true; + } else { + movesetsTable = readPointer(0x3EA7C); + jamboMovesetHack = false; + } + + // secondary check: moveset pointers + // if a slot has an invalid moveset pointer, it's not a real slot + // Before that, grab the moveset table from a known pointer to it. + romEntry.entries.put("PokemonMovesets", movesetsTable); + while (iPokemonCount >= 0) { + int movesetPtr = readPointer(movesetsTable + iPokemonCount * 4); + if (movesetPtr < 0 || movesetPtr >= rom.length) { + iPokemonCount--; + } else { + break; + } + } + + // sanity check: pokedex order + // pokedex entries have to be within 0-1023 + // even after extending the dex + // (at least with conventional methods) + // so if we run into an invalid one + // then we can cut off the count + int pdOffset = romEntry.getValue("PokedexOrder"); + for (int i = 1; i <= iPokemonCount; i++) { + int pdEntry = readWord(pdOffset + (i - 1) * 2); + if (pdEntry > 1023) { + iPokemonCount = i - 1; + break; + } + } + + // write new pokemon count + romEntry.entries.put("PokemonCount", iPokemonCount); + + // update some key offsets from known pointers + romEntry.entries.put("PokemonTMHMCompat", readPointer(0x43C68)); + romEntry.entries.put("PokemonEvolutions", readPointer(0x42F6C)); + romEntry.entries.put("MoveTutorCompatibility", readPointer(0x120C30)); + int descsTable = readPointer(0xE5440); + romEntry.entries.put("MoveDescriptions", descsTable); + int trainersTable = readPointer(0xFC00); + romEntry.entries.put("TrainerData", trainersTable); + + // try to detect number of moves using the descriptions + int moveCount = 0; + while (true) { + int descPointer = readPointer(descsTable + (moveCount) * 4); + if (descPointer >= 0 && descPointer < rom.length) { + int descStrLen = lengthOfStringAt(descPointer); + if (descStrLen > 0 && descStrLen < 100) { + // okay, this does seem fine + moveCount++; + continue; + } + } + break; + } + romEntry.entries.put("MoveCount", moveCount); + + // attempt to detect number of trainers using various tells + int trainerCount = 1; + int tEntryLen = romEntry.getValue("TrainerEntrySize"); + int tNameLen = romEntry.getValue("TrainerNameLength"); + while (true) { + int trOffset = trainersTable + tEntryLen * trainerCount; + int pokeDataType = rom[trOffset] & 0xFF; + if (pokeDataType >= 4) { + // only allowed 0-3 + break; + } + int numPokes = rom[trOffset + (tEntryLen - 8)] & 0xFF; + if (numPokes == 0 || numPokes > 6) { + break; + } + int pointerToPokes = readPointer(trOffset + (tEntryLen - 4)); + if (pointerToPokes < 0 || pointerToPokes >= rom.length) { + break; + } + int nameLength = lengthOfStringAt(trOffset + 4); + if (nameLength >= tNameLen) { + break; + } + // found a valid trainer entry, recognize it + trainerCount++; + } + romEntry.entries.put("TrainerCount", trainerCount); + } + + } + + private boolean basicBPRE10HackDetection() { + if (rom.length != Gen3Constants.size16M) { + return true; + } + long csum = FileFunctions.getCRC32(rom); + return csum != 3716707868L; + } + + @Override + public void savingRom() { + savePokemonStats(); + saveMoves(); + } + + private void loadPokedex() { + int pdOffset = romEntry.getValue("PokedexOrder"); + int numInternalPokes = romEntry.getValue("PokemonCount"); + int maxPokedex = 0; + internalToPokedex = new int[numInternalPokes + 1]; + pokedexToInternal = new int[numInternalPokes + 1]; + for (int i = 1; i <= numInternalPokes; i++) { + int dexEntry = readWord(rom, pdOffset + (i - 1) * 2); + if (dexEntry != 0) { + internalToPokedex[i] = dexEntry; + // take the first pokemon only for each dex entry + if (pokedexToInternal[dexEntry] == 0) { + pokedexToInternal[dexEntry] = i; + } + maxPokedex = Math.max(maxPokedex, dexEntry); + } + } + if (maxPokedex == Gen3Constants.unhackedMaxPokedex) { + // see if the slots between johto and hoenn are in use + // old rom hacks use them instead of expanding pokes + int offs = romEntry.getValue("PokemonStats"); + int usedSlots = 0; + for (int i = 0; i < Gen3Constants.unhackedMaxPokedex - Gen3Constants.unhackedRealPokedex; i++) { + int pokeSlot = Gen3Constants.hoennPokesStart + i; + int pokeOffs = offs + pokeSlot * Gen3Constants.baseStatsEntrySize; + String lowerName = pokeNames[pokeSlot].toLowerCase(); + if (!this.matches(rom, pokeOffs, Gen3Constants.emptyPokemonSig) && !lowerName.contains("unused") + && !lowerName.equals("?") && !lowerName.equals("-")) { + usedSlots++; + pokedexToInternal[Gen3Constants.unhackedRealPokedex + usedSlots] = pokeSlot; + internalToPokedex[pokeSlot] = Gen3Constants.unhackedRealPokedex + usedSlots; + } else { + internalToPokedex[pokeSlot] = 0; + } + } + // remove the fake extra slots + for (int i = usedSlots + 1; i <= Gen3Constants.unhackedMaxPokedex - Gen3Constants.unhackedRealPokedex; i++) { + pokedexToInternal[Gen3Constants.unhackedRealPokedex + i] = 0; + } + // if any slots were used at all, this is a rom hack + if (usedSlots > 0) { + this.isRomHack = true; + } + this.pokedexCount = Gen3Constants.unhackedRealPokedex + usedSlots; + } else { + this.isRomHack = true; + this.pokedexCount = maxPokedex; + } + + } + + private void constructPokemonList() { + if (!this.isRomHack) { + // simple behavior: all pokes in the dex are valid + pokemonList = Arrays.asList(pokes); + } else { + // only include "valid" pokes + pokemonList = new ArrayList<>(); + pokemonList.add(null); + for (int i = 1; i < pokes.length; i++) { + Pokemon pk = pokes[i]; + if (pk != null) { + String lowerName = pk.name.toLowerCase(); + if (!lowerName.contains("unused") && !lowerName.equals("?")) { + pokemonList.add(pk); + } + } + } + } + numRealPokemon = pokemonList.size() - 1; + + } + + private void loadPokemonStats() { + pokes = new Pokemon[this.pokedexCount + 1]; + int numInternalPokes = romEntry.getValue("PokemonCount"); + pokesInternal = new Pokemon[numInternalPokes + 1]; + int offs = romEntry.getValue("PokemonStats"); + for (int i = 1; i <= numInternalPokes; i++) { + Pokemon pk = new Pokemon(); + pk.name = pokeNames[i]; + pk.number = internalToPokedex[i]; + if (pk.number != 0) { + pokes[pk.number] = pk; + } + pokesInternal[i] = pk; + int pkoffs = offs + i * Gen3Constants.baseStatsEntrySize; + loadBasicPokeStats(pk, pkoffs); + } + + // In these games, the alternate formes of Deoxys have hardcoded stats that are used 99% of the time; + // the only times these hardcoded stats are ignored are during Link Battles. Since not many people + // are using the randomizer to battle against others, let's just always use these stats. + if (romEntry.romType == Gen3Constants.RomType_FRLG || romEntry.romType == Gen3Constants.RomType_Em) { + String deoxysStatPrefix = romEntry.strings.get("DeoxysStatPrefix"); + int offset = find(deoxysStatPrefix); + if (offset > 0) { + offset += deoxysStatPrefix.length() / 2; // because it was a prefix + Pokemon deoxys = pokes[Species.deoxys]; + deoxys.hp = readWord(offset); + deoxys.attack = readWord(offset + 2); + deoxys.defense = readWord(offset + 4); + deoxys.speed = readWord(offset + 6); + deoxys.spatk = readWord(offset + 8); + deoxys.spdef = readWord(offset + 10); + } + } + } + + private void savePokemonStats() { + // Write pokemon names & stats + int offs = romEntry.getValue("PokemonNames"); + int nameLen = romEntry.getValue("PokemonNameLength"); + int offs2 = romEntry.getValue("PokemonStats"); + int numInternalPokes = romEntry.getValue("PokemonCount"); + for (int i = 1; i <= numInternalPokes; i++) { + Pokemon pk = pokesInternal[i]; + int stringOffset = offs + i * nameLen; + writeFixedLengthString(pk.name, stringOffset, nameLen); + saveBasicPokeStats(pk, offs2 + i * Gen3Constants.baseStatsEntrySize); + } + + // Make sure to write to the hardcoded Deoxys stat location, since otherwise it will just have vanilla + // stats no matter what settings the user selected. + if (romEntry.romType == Gen3Constants.RomType_FRLG || romEntry.romType == Gen3Constants.RomType_Em) { + String deoxysStatPrefix = romEntry.strings.get("DeoxysStatPrefix"); + int offset = find(deoxysStatPrefix); + if (offset > 0) { + offset += deoxysStatPrefix.length() / 2; // because it was a prefix + Pokemon deoxys = pokes[Species.deoxys]; + writeWord(offset, deoxys.hp); + writeWord(offset + 2, deoxys.attack); + writeWord(offset + 4, deoxys.defense); + writeWord(offset + 6, deoxys.speed); + writeWord(offset + 8, deoxys.spatk); + writeWord(offset + 10, deoxys.spdef); + } + } + + writeEvolutions(); + } + + private void loadMoves() { + int moveCount = romEntry.getValue("MoveCount"); + moves = new Move[moveCount + 1]; + int offs = romEntry.getValue("MoveData"); + int nameoffs = romEntry.getValue("MoveNames"); + int namelen = romEntry.getValue("MoveNameLength"); + for (int i = 1; i <= moveCount; i++) { + moves[i] = new Move(); + moves[i].name = readFixedLengthString(nameoffs + i * namelen, namelen); + moves[i].number = i; + moves[i].internalId = i; + moves[i].effectIndex = rom[offs + i * 0xC] & 0xFF; + moves[i].hitratio = ((rom[offs + i * 0xC + 3] & 0xFF)); + moves[i].power = rom[offs + i * 0xC + 1] & 0xFF; + moves[i].pp = rom[offs + i * 0xC + 4] & 0xFF; + moves[i].type = Gen3Constants.typeTable[rom[offs + i * 0xC + 2]]; + moves[i].target = rom[offs + i * 0xC + 6] & 0xFF; + moves[i].category = GBConstants.physicalTypes.contains(moves[i].type) ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL; + if (moves[i].power == 0 && !GlobalConstants.noPowerNonStatusMoves.contains(i)) { + moves[i].category = MoveCategory.STATUS; + } + moves[i].priority = rom[offs + i * 0xC + 7]; + int flags = rom[offs + i * 0xC + 8] & 0xFF; + moves[i].makesContact = (flags & 1) != 0; + moves[i].isSoundMove = Gen3Constants.soundMoves.contains(moves[i].number); + + if (i == Moves.swift) { + perfectAccuracy = (int)moves[i].hitratio; + } + + if (GlobalConstants.normalMultihitMoves.contains(i)) { + moves[i].hitCount = 3; + } else if (GlobalConstants.doubleHitMoves.contains(i)) { + moves[i].hitCount = 2; + } else if (i == Moves.tripleKick) { + moves[i].hitCount = 2.71; // this assumes the first hit lands + } + + int secondaryEffectChance = rom[offs + i * 0xC + 5] & 0xFF; + loadStatChangesFromEffect(moves[i], secondaryEffectChance); + loadStatusFromEffect(moves[i], secondaryEffectChance); + loadMiscMoveInfoFromEffect(moves[i], secondaryEffectChance); + } + } + + private void loadStatChangesFromEffect(Move move, int secondaryEffectChance) { + switch (move.effectIndex) { + case Gen3Constants.noDamageAtkPlusOneEffect: + case Gen3Constants.noDamageDefPlusOneEffect: + case Gen3Constants.noDamageSpAtkPlusOneEffect: + case Gen3Constants.noDamageEvasionPlusOneEffect: + case Gen3Constants.noDamageAtkMinusOneEffect: + case Gen3Constants.noDamageDefMinusOneEffect: + case Gen3Constants.noDamageSpeMinusOneEffect: + case Gen3Constants.noDamageAccuracyMinusOneEffect: + case Gen3Constants.noDamageEvasionMinusOneEffect: + case Gen3Constants.noDamageAtkPlusTwoEffect: + case Gen3Constants.noDamageDefPlusTwoEffect: + case Gen3Constants.noDamageSpePlusTwoEffect: + case Gen3Constants.noDamageSpAtkPlusTwoEffect: + case Gen3Constants.noDamageSpDefPlusTwoEffect: + case Gen3Constants.noDamageAtkMinusTwoEffect: + case Gen3Constants.noDamageDefMinusTwoEffect: + case Gen3Constants.noDamageSpeMinusTwoEffect: + case Gen3Constants.noDamageSpDefMinusTwoEffect: + case Gen3Constants.minimizeEffect: + case Gen3Constants.swaggerEffect: + case Gen3Constants.defenseCurlEffect: + case Gen3Constants.flatterEffect: + case Gen3Constants.chargeEffect: + case Gen3Constants.noDamageAtkAndDefMinusOneEffect: + case Gen3Constants.noDamageDefAndSpDefPlusOneEffect: + case Gen3Constants.noDamageAtkAndDefPlusOneEffect: + case Gen3Constants.noDamageSpAtkAndSpDefPlusOneEffect: + case Gen3Constants.noDamageAtkAndSpePlusOneEffect: + if (move.target == 16) { + move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER; + } else { + move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET; + } + break; + + case Gen3Constants.damageAtkMinusOneEffect: + case Gen3Constants.damageDefMinusOneEffect: + case Gen3Constants.damageSpeMinusOneEffect: + case Gen3Constants.damageSpAtkMinusOneEffect: + case Gen3Constants.damageSpDefMinusOneEffect: + case Gen3Constants.damageAccuracyMinusOneEffect: + move.statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET; + break; + + case Gen3Constants.damageUserDefPlusOneEffect: + case Gen3Constants.damageUserAtkPlusOneEffect: + case Gen3Constants.damageUserAllPlusOneEffect: + case Gen3Constants.damageUserAtkAndDefMinusOneEffect: + case Gen3Constants.damageUserSpAtkMinusTwoEffect: + move.statChangeMoveType = StatChangeMoveType.DAMAGE_USER; + break; + + default: + // Move does not have a stat-changing effect + return; + } + + switch (move.effectIndex) { + case Gen3Constants.noDamageAtkPlusOneEffect: + case Gen3Constants.damageUserAtkPlusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.noDamageDefPlusOneEffect: + case Gen3Constants.damageUserDefPlusOneEffect: + case Gen3Constants.defenseCurlEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.noDamageSpAtkPlusOneEffect: + case Gen3Constants.flatterEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.noDamageEvasionPlusOneEffect: + case Gen3Constants.minimizeEffect: + move.statChanges[0].type = StatChangeType.EVASION; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.noDamageAtkMinusOneEffect: + case Gen3Constants.damageAtkMinusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageDefMinusOneEffect: + case Gen3Constants.damageDefMinusOneEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageSpeMinusOneEffect: + case Gen3Constants.damageSpeMinusOneEffect: + move.statChanges[0].type = StatChangeType.SPEED; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageAccuracyMinusOneEffect: + case Gen3Constants.damageAccuracyMinusOneEffect: + move.statChanges[0].type = StatChangeType.ACCURACY; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageEvasionMinusOneEffect: + move.statChanges[0].type = StatChangeType.EVASION; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageAtkPlusTwoEffect: + case Gen3Constants.swaggerEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageDefPlusTwoEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageSpePlusTwoEffect: + move.statChanges[0].type = StatChangeType.SPEED; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageSpAtkPlusTwoEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageSpDefPlusTwoEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageAtkMinusTwoEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.noDamageDefMinusTwoEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.noDamageSpeMinusTwoEffect: + move.statChanges[0].type = StatChangeType.SPEED; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.noDamageSpDefMinusTwoEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.damageSpAtkMinusOneEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.damageSpDefMinusOneEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.damageUserAllPlusOneEffect: + move.statChanges[0].type = StatChangeType.ALL; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.chargeEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.damageUserAtkAndDefMinusOneEffect: + case Gen3Constants.noDamageAtkAndDefMinusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = -1; + move.statChanges[1].type = StatChangeType.DEFENSE; + move.statChanges[1].stages = -1; + break; + case Gen3Constants.damageUserSpAtkMinusTwoEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.noDamageDefAndSpDefPlusOneEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = 1; + move.statChanges[1].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[1].stages = 1; + break; + case Gen3Constants.noDamageAtkAndDefPlusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = 1; + move.statChanges[1].type = StatChangeType.DEFENSE; + move.statChanges[1].stages = 1; + break; + case Gen3Constants.noDamageSpAtkAndSpDefPlusOneEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = 1; + move.statChanges[1].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[1].stages = 1; + break; + case Gen3Constants.noDamageAtkAndSpePlusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = 1; + move.statChanges[1].type = StatChangeType.SPEED; + move.statChanges[1].stages = 1; + break; + } + + if (move.statChangeMoveType == StatChangeMoveType.DAMAGE_TARGET || move.statChangeMoveType == StatChangeMoveType.DAMAGE_USER) { + for (int i = 0; i < move.statChanges.length; i++) { + if (move.statChanges[i].type != StatChangeType.NONE) { + move.statChanges[i].percentChance = secondaryEffectChance; + if (move.statChanges[i].percentChance == 0.0) { + move.statChanges[i].percentChance = 100.0; + } + } + } + } + } + + private void loadStatusFromEffect(Move move, int secondaryEffectChance) { + if (move.number == Moves.bounce) { + // GF hardcoded this, so we have to as well + move.statusMoveType = StatusMoveType.DAMAGE; + move.statusType = StatusType.PARALYZE; + move.statusPercentChance = secondaryEffectChance; + return; + } + + switch (move.effectIndex) { + case Gen3Constants.noDamageSleepEffect: + case Gen3Constants.toxicEffect: + case Gen3Constants.noDamageConfusionEffect: + case Gen3Constants.noDamagePoisonEffect: + case Gen3Constants.noDamageParalyzeEffect: + case Gen3Constants.noDamageBurnEffect: + case Gen3Constants.swaggerEffect: + case Gen3Constants.flatterEffect: + case Gen3Constants.teeterDanceEffect: + move.statusMoveType = StatusMoveType.NO_DAMAGE; + break; + + case Gen3Constants.damagePoisonEffect: + case Gen3Constants.damageBurnEffect: + case Gen3Constants.damageFreezeEffect: + case Gen3Constants.damageParalyzeEffect: + case Gen3Constants.damageConfusionEffect: + case Gen3Constants.twineedleEffect: + case Gen3Constants.damageBurnAndThawUserEffect: + case Gen3Constants.thunderEffect: + case Gen3Constants.blazeKickEffect: + case Gen3Constants.poisonFangEffect: + case Gen3Constants.poisonTailEffect: + move.statusMoveType = StatusMoveType.DAMAGE; + break; + + default: + // Move does not have a status effect + return; + } + + switch (move.effectIndex) { + case Gen3Constants.noDamageSleepEffect: + move.statusType = StatusType.SLEEP; + break; + case Gen3Constants.damagePoisonEffect: + case Gen3Constants.noDamagePoisonEffect: + case Gen3Constants.twineedleEffect: + case Gen3Constants.poisonTailEffect: + move.statusType = StatusType.POISON; + break; + case Gen3Constants.damageBurnEffect: + case Gen3Constants.damageBurnAndThawUserEffect: + case Gen3Constants.noDamageBurnEffect: + case Gen3Constants.blazeKickEffect: + move.statusType = StatusType.BURN; + break; + case Gen3Constants.damageFreezeEffect: + move.statusType = StatusType.FREEZE; + break; + case Gen3Constants.damageParalyzeEffect: + case Gen3Constants.noDamageParalyzeEffect: + case Gen3Constants.thunderEffect: + move.statusType = StatusType.PARALYZE; + break; + case Gen3Constants.toxicEffect: + case Gen3Constants.poisonFangEffect: + move.statusType = StatusType.TOXIC_POISON; + break; + case Gen3Constants.noDamageConfusionEffect: + case Gen3Constants.damageConfusionEffect: + case Gen3Constants.swaggerEffect: + case Gen3Constants.flatterEffect: + case Gen3Constants.teeterDanceEffect: + move.statusType = StatusType.CONFUSION; + break; + } + + if (move.statusMoveType == StatusMoveType.DAMAGE) { + move.statusPercentChance = secondaryEffectChance; + if (move.statusPercentChance == 0.0) { + move.statusPercentChance = 100.0; + } + } + } + + private void loadMiscMoveInfoFromEffect(Move move, int secondaryEffectChance) { + switch (move.effectIndex) { + case Gen3Constants.increasedCritEffect: + case Gen3Constants.blazeKickEffect: + case Gen3Constants.poisonTailEffect: + move.criticalChance = CriticalChance.INCREASED; + break; + + case Gen3Constants.futureSightAndDoomDesireEffect: + case Gen3Constants.spitUpEffect: + move.criticalChance = CriticalChance.NONE; + + case Gen3Constants.flinchEffect: + case Gen3Constants.snoreEffect: + case Gen3Constants.twisterEffect: + case Gen3Constants.flinchWithMinimizeBonusEffect: + case Gen3Constants.fakeOutEffect: + move.flinchPercentChance = secondaryEffectChance; + break; + + case Gen3Constants.damageAbsorbEffect: + case Gen3Constants.dreamEaterEffect: + move.absorbPercent = 50; + break; + + case Gen3Constants.damageRecoil25PercentEffect: + move.recoilPercent = 25; + break; + + case Gen3Constants.damageRecoil33PercentEffect: + move.recoilPercent = 33; + break; + + case Gen3Constants.bindingEffect: + case Gen3Constants.trappingEffect: + move.isTrapMove = true; + break; + + case Gen3Constants.razorWindEffect: + case Gen3Constants.skullBashEffect: + case Gen3Constants.solarbeamEffect: + case Gen3Constants.semiInvulnerableEffect: + move.isChargeMove = true; + break; + + case Gen3Constants.rechargeEffect: + move.isRechargeMove = true; + break; + + case Gen3Constants.skyAttackEffect: + move.criticalChance = CriticalChance.INCREASED; + move.flinchPercentChance = secondaryEffectChance; + move.isChargeMove = true; + break; + } + } + + private void saveMoves() { + int moveCount = romEntry.getValue("MoveCount"); + int offs = romEntry.getValue("MoveData"); + for (int i = 1; i <= moveCount; i++) { + rom[offs + i * 0xC] = (byte) moves[i].effectIndex; + rom[offs + i * 0xC + 1] = (byte) moves[i].power; + rom[offs + i * 0xC + 2] = Gen3Constants.typeToByte(moves[i].type); + int hitratio = (int) Math.round(moves[i].hitratio); + if (hitratio < 0) { + hitratio = 0; + } + if (hitratio > 100) { + hitratio = 100; + } + rom[offs + i * 0xC + 3] = (byte) hitratio; + rom[offs + i * 0xC + 4] = (byte) moves[i].pp; + } + } + + public List<Move> getMoves() { + return Arrays.asList(moves); + } + + private void loadBasicPokeStats(Pokemon pkmn, int offset) { + pkmn.hp = rom[offset + Gen3Constants.bsHPOffset] & 0xFF; + pkmn.attack = rom[offset + Gen3Constants.bsAttackOffset] & 0xFF; + pkmn.defense = rom[offset + Gen3Constants.bsDefenseOffset] & 0xFF; + pkmn.speed = rom[offset + Gen3Constants.bsSpeedOffset] & 0xFF; + pkmn.spatk = rom[offset + Gen3Constants.bsSpAtkOffset] & 0xFF; + pkmn.spdef = rom[offset + Gen3Constants.bsSpDefOffset] & 0xFF; + // Type + pkmn.primaryType = Gen3Constants.typeTable[rom[offset + Gen3Constants.bsPrimaryTypeOffset] & 0xFF]; + pkmn.secondaryType = Gen3Constants.typeTable[rom[offset + Gen3Constants.bsSecondaryTypeOffset] & 0xFF]; + // Only one type? + if (pkmn.secondaryType == pkmn.primaryType) { + pkmn.secondaryType = null; + } + pkmn.catchRate = rom[offset + Gen3Constants.bsCatchRateOffset] & 0xFF; + pkmn.growthCurve = ExpCurve.fromByte(rom[offset + Gen3Constants.bsGrowthCurveOffset]); + // Abilities + pkmn.ability1 = rom[offset + Gen3Constants.bsAbility1Offset] & 0xFF; + pkmn.ability2 = rom[offset + Gen3Constants.bsAbility2Offset] & 0xFF; + + // Held Items? + int item1 = readWord(offset + Gen3Constants.bsCommonHeldItemOffset); + int item2 = readWord(offset + Gen3Constants.bsRareHeldItemOffset); + + if (item1 == item2) { + // guaranteed + pkmn.guaranteedHeldItem = item1; + pkmn.commonHeldItem = 0; + pkmn.rareHeldItem = 0; + } else { + pkmn.guaranteedHeldItem = 0; + pkmn.commonHeldItem = item1; + pkmn.rareHeldItem = item2; + } + pkmn.darkGrassHeldItem = -1; + + pkmn.genderRatio = rom[offset + Gen3Constants.bsGenderRatioOffset] & 0xFF; + } + + private void saveBasicPokeStats(Pokemon pkmn, int offset) { + rom[offset + Gen3Constants.bsHPOffset] = (byte) pkmn.hp; + rom[offset + Gen3Constants.bsAttackOffset] = (byte) pkmn.attack; + rom[offset + Gen3Constants.bsDefenseOffset] = (byte) pkmn.defense; + rom[offset + Gen3Constants.bsSpeedOffset] = (byte) pkmn.speed; + rom[offset + Gen3Constants.bsSpAtkOffset] = (byte) pkmn.spatk; + rom[offset + Gen3Constants.bsSpDefOffset] = (byte) pkmn.spdef; + rom[offset + Gen3Constants.bsPrimaryTypeOffset] = Gen3Constants.typeToByte(pkmn.primaryType); + if (pkmn.secondaryType == null) { + rom[offset + Gen3Constants.bsSecondaryTypeOffset] = rom[offset + Gen3Constants.bsPrimaryTypeOffset]; + } else { + rom[offset + Gen3Constants.bsSecondaryTypeOffset] = Gen3Constants.typeToByte(pkmn.secondaryType); + } + rom[offset + Gen3Constants.bsCatchRateOffset] = (byte) pkmn.catchRate; + rom[offset + Gen3Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte(); + + rom[offset + Gen3Constants.bsAbility1Offset] = (byte) pkmn.ability1; + if (pkmn.ability2 == 0) { + // required to not break evos with random ability + rom[offset + Gen3Constants.bsAbility2Offset] = (byte) pkmn.ability1; + } else { + rom[offset + Gen3Constants.bsAbility2Offset] = (byte) pkmn.ability2; + } + + // Held items + if (pkmn.guaranteedHeldItem > 0) { + writeWord(offset + Gen3Constants.bsCommonHeldItemOffset, pkmn.guaranteedHeldItem); + writeWord(offset + Gen3Constants.bsRareHeldItemOffset, pkmn.guaranteedHeldItem); + } else { + writeWord(offset + Gen3Constants.bsCommonHeldItemOffset, pkmn.commonHeldItem); + writeWord(offset + Gen3Constants.bsRareHeldItemOffset, pkmn.rareHeldItem); + } + + rom[offset + Gen3Constants.bsGenderRatioOffset] = (byte) pkmn.genderRatio; + } + + private void loadPokemonNames() { + int offs = romEntry.getValue("PokemonNames"); + int nameLen = romEntry.getValue("PokemonNameLength"); + int numInternalPokes = romEntry.getValue("PokemonCount"); + pokeNames = new String[numInternalPokes + 1]; + for (int i = 1; i <= numInternalPokes; i++) { + pokeNames[i] = readFixedLengthString(offs + i * nameLen, nameLen); + } + } + + private String readString(int offset, int maxLength) { + StringBuilder string = new StringBuilder(); + for (int c = 0; c < maxLength; c++) { + int currChar = rom[offset + c] & 0xFF; + if (tb[currChar] != null) { + string.append(tb[currChar]); + } else { + if (currChar == Gen3Constants.textTerminator) { + break; + } else if (currChar == Gen3Constants.textVariable) { + int nextChar = rom[offset + c + 1] & 0xFF; + string.append("\\v").append(String.format("%02X", nextChar)); + c++; + } else { + string.append("\\x").append(String.format("%02X", currChar)); + } + } + } + return string.toString(); + } + + private byte[] translateString(String text) { + List<Byte> data = new ArrayList<>(); + while (text.length() != 0) { + int i = Math.max(0, 4 - text.length()); + if (text.charAt(0) == '\\' && text.charAt(1) == 'x') { + data.add((byte) Integer.parseInt(text.substring(2, 4), 16)); + text = text.substring(4); + } else if (text.charAt(0) == '\\' && text.charAt(1) == 'v') { + data.add((byte) Gen3Constants.textVariable); + data.add((byte) Integer.parseInt(text.substring(2, 4), 16)); + text = text.substring(4); + } else { + while (!(d.containsKey(text.substring(0, 4 - i)) || (i == 4))) { + i++; + } + if (i == 4) { + text = text.substring(1); + } else { + data.add(d.get(text.substring(0, 4 - i))); + text = text.substring(4 - i); + } + } + } + byte[] ret = new byte[data.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = data.get(i); + } + return ret; + } + + private String readFixedLengthString(int offset, int length) { + return readString(offset, length); + } + + private String readVariableLengthString(int offset) { + return readString(offset, Integer.MAX_VALUE); + } + + private void writeFixedLengthString(String str, int offset, int length) { + byte[] translated = translateString(str); + int len = Math.min(translated.length, length); + System.arraycopy(translated, 0, rom, offset, len); + if (len < length) { + rom[offset + len] = (byte) Gen3Constants.textTerminator; + len++; + } + while (len < length) { + rom[offset + len] = 0; + len++; + } + } + + private void writeVariableLengthString(String str, int offset) { + byte[] translated = translateString(str); + System.arraycopy(translated, 0, rom, offset, translated.length); + rom[offset + translated.length] = (byte) 0xFF; + } + + private int lengthOfStringAt(int offset) { + int len = 0; + while ((rom[offset + (len++)] & 0xFF) != 0xFF) { + } + return len - 1; + } + + private static boolean romName(byte[] rom, String name) { + try { + int sigOffset = Gen3Constants.romNameOffset; + byte[] sigBytes = name.getBytes("US-ASCII"); + for (int i = 0; i < sigBytes.length; i++) { + if (rom[sigOffset + i] != sigBytes[i]) { + return false; + } + } + return true; + } catch (UnsupportedEncodingException ex) { + return false; + } + + } + + private static boolean romCode(byte[] rom, String codeToCheck) { + try { + int sigOffset = Gen3Constants.romCodeOffset; + byte[] sigBytes = codeToCheck.getBytes("US-ASCII"); + for (int i = 0; i < sigBytes.length; i++) { + if (rom[sigOffset + i] != sigBytes[i]) { + return false; + } + } + return true; + } catch (UnsupportedEncodingException ex) { + return false; + } + + } + + private int readPointer(int offset) { + return readLong(offset) - 0x8000000; + } + + private int readLong(int offset) { + return (rom[offset] & 0xFF) + ((rom[offset + 1] & 0xFF) << 8) + ((rom[offset + 2] & 0xFF) << 16) + + (((rom[offset + 3] & 0xFF)) << 24); + } + + private void writePointer(int offset, int pointer) { + writeLong(offset, pointer + 0x8000000); + } + + private void writeLong(int offset, int value) { + rom[offset] = (byte) (value & 0xFF); + rom[offset + 1] = (byte) ((value >> 8) & 0xFF); + rom[offset + 2] = (byte) ((value >> 16) & 0xFF); + rom[offset + 3] = (byte) (((value >> 24) & 0xFF)); + } + + @Override + public List<Pokemon> getStarters() { + List<Pokemon> starters = new ArrayList<>(); + int baseOffset = romEntry.getValue("StarterPokemon"); + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp + || romEntry.romType == Gen3Constants.RomType_Em) { + // do something + Pokemon starter1 = pokesInternal[readWord(baseOffset)]; + Pokemon starter2 = pokesInternal[readWord(baseOffset + Gen3Constants.rseStarter2Offset)]; + Pokemon starter3 = pokesInternal[readWord(baseOffset + Gen3Constants.rseStarter3Offset)]; + starters.add(starter1); + starters.add(starter2); + starters.add(starter3); + } else { + // do something else + Pokemon starter1 = pokesInternal[readWord(baseOffset)]; + Pokemon starter2 = pokesInternal[readWord(baseOffset + Gen3Constants.frlgStarter2Offset)]; + Pokemon starter3 = pokesInternal[readWord(baseOffset + Gen3Constants.frlgStarter3Offset)]; + starters.add(starter1); + starters.add(starter2); + starters.add(starter3); + } + return starters; + } + + @Override + public boolean setStarters(List<Pokemon> newStarters) { + if (newStarters.size() != 3) { + return false; + } + + // Support Deoxys/Mew starters in E/FR/LG + attemptObedienceEvolutionPatches(); + int baseOffset = romEntry.getValue("StarterPokemon"); + + int starter0 = pokedexToInternal[newStarters.get(0).number]; + int starter1 = pokedexToInternal[newStarters.get(1).number]; + int starter2 = pokedexToInternal[newStarters.get(2).number]; + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp + || romEntry.romType == Gen3Constants.RomType_Em) { + + // US + // order: 0, 1, 2 + writeWord(baseOffset, starter0); + writeWord(baseOffset + Gen3Constants.rseStarter2Offset, starter1); + writeWord(baseOffset + Gen3Constants.rseStarter3Offset, starter2); + + } else { + // frlg: + // order: 0, 1, 2 + writeWord(baseOffset, starter0); + writeWord(baseOffset + Gen3Constants.frlgStarterRepeatOffset, starter1); + + writeWord(baseOffset + Gen3Constants.frlgStarter2Offset, starter1); + writeWord(baseOffset + Gen3Constants.frlgStarter2Offset + Gen3Constants.frlgStarterRepeatOffset, starter2); + + writeWord(baseOffset + Gen3Constants.frlgStarter3Offset, starter2); + writeWord(baseOffset + Gen3Constants.frlgStarter3Offset + Gen3Constants.frlgStarterRepeatOffset, starter0); + + if (romEntry.romCode.charAt(3) != 'J' && romEntry.romCode.charAt(3) != 'B') { + // Update PROF. Oak's descriptions for each starter + // First result for each STARTERNAME is the text we need + List<Integer> bulbasaurFoundTexts = RomFunctions.search(rom, translateString(pokes[Gen3Constants.frlgBaseStarter1].name.toUpperCase())); + List<Integer> charmanderFoundTexts = RomFunctions.search(rom, translateString(pokes[Gen3Constants.frlgBaseStarter2].name.toUpperCase())); + List<Integer> squirtleFoundTexts = RomFunctions.search(rom, translateString(pokes[Gen3Constants.frlgBaseStarter3].name.toUpperCase())); + writeFRLGStarterText(bulbasaurFoundTexts, newStarters.get(0), "you want to go with\\nthe "); + writeFRLGStarterText(charmanderFoundTexts, newStarters.get(1), "you’re claiming the\\n"); + writeFRLGStarterText(squirtleFoundTexts, newStarters.get(2), "you’ve decided on the\\n"); + } + } + return true; + + } + + @Override + public boolean hasStarterAltFormes() { + return false; + } + + @Override + public int starterCount() { + return 3; + } + + @Override + public Map<Integer, StatChange> getUpdatedPokemonStats(int generation) { + return GlobalConstants.getStatChanges(generation); + } + + @Override + public boolean supportsStarterHeldItems() { + return true; + } + + @Override + public List<Integer> getStarterHeldItems() { + List<Integer> sHeldItems = new ArrayList<>(); + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // offset from normal starter offset as a word + int baseOffset = romEntry.getValue("StarterPokemon"); + sHeldItems.add(readWord(baseOffset + Gen3Constants.frlgStarterItemsOffset)); + } else { + int baseOffset = romEntry.getValue("StarterItems"); + int i1 = rom[baseOffset] & 0xFF; + int i2 = rom[baseOffset + 2] & 0xFF; + if (i2 == 0) { + sHeldItems.add(i1); + } else { + sHeldItems.add(i2 + 0xFF); + } + } + return sHeldItems; + } + + @Override + public void setStarterHeldItems(List<Integer> items) { + if (items.size() != 1) { + return; + } + int item = items.get(0); + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // offset from normal starter offset as a word + int baseOffset = romEntry.getValue("StarterPokemon"); + writeWord(baseOffset + Gen3Constants.frlgStarterItemsOffset, item); + } else { + int baseOffset = romEntry.getValue("StarterItems"); + if (item <= 0xFF) { + rom[baseOffset] = (byte) item; + rom[baseOffset + 2] = 0; + rom[baseOffset + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR2; + } else { + rom[baseOffset] = (byte) 0xFF; + rom[baseOffset + 2] = (byte) (item - 0xFF); + rom[baseOffset + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR2; + } + } + } + + private void writeFRLGStarterText(List<Integer> foundTexts, Pokemon pkmn, String oakText) { + if (foundTexts.size() > 0) { + int offset = foundTexts.get(0); + String pokeName = pkmn.name; + String pokeType = pkmn.primaryType == null ? "???" : pkmn.primaryType.toString(); + if (pokeType.equals("NORMAL") && pkmn.secondaryType != null) { + pokeType = pkmn.secondaryType.toString(); + } + String speech = pokeName + " is your choice.\\pSo, \\v01, " + oakText + pokeType + " POKéMON " + pokeName + + "?"; + writeFixedLengthString(speech, offset, lengthOfStringAt(offset) + 1); + } + } + + @Override + public List<EncounterSet> getEncounters(boolean useTimeOfDay) { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + + int startOffs = romEntry.getValue("WildPokemon"); + List<EncounterSet> encounterAreas = new ArrayList<>(); + Set<Integer> seenOffsets = new TreeSet<>(); + int offs = startOffs; + while (true) { + // Read pointers + int bank = rom[offs] & 0xFF; + int map = rom[offs + 1] & 0xFF; + if (bank == 0xFF && map == 0xFF) { + break; + } + + String mapName = mapNames[bank][map]; + + int grassPokes = readPointer(offs + 4); + int waterPokes = readPointer(offs + 8); + int treePokes = readPointer(offs + 12); + int fishPokes = readPointer(offs + 16); + + // Add pokemanz + if (grassPokes >= 0 && grassPokes < rom.length && rom[grassPokes] != 0 + && !seenOffsets.contains(readPointer(grassPokes + 4))) { + encounterAreas.add(readWildArea(grassPokes, Gen3Constants.grassSlots, mapName + " Grass/Cave")); + seenOffsets.add(readPointer(grassPokes + 4)); + } + if (waterPokes >= 0 && waterPokes < rom.length && rom[waterPokes] != 0 + && !seenOffsets.contains(readPointer(waterPokes + 4))) { + encounterAreas.add(readWildArea(waterPokes, Gen3Constants.surfingSlots, mapName + " Surfing")); + seenOffsets.add(readPointer(waterPokes + 4)); + } + if (treePokes >= 0 && treePokes < rom.length && rom[treePokes] != 0 + && !seenOffsets.contains(readPointer(treePokes + 4))) { + encounterAreas.add(readWildArea(treePokes, Gen3Constants.rockSmashSlots, mapName + " Rock Smash")); + seenOffsets.add(readPointer(treePokes + 4)); + } + if (fishPokes >= 0 && fishPokes < rom.length && rom[fishPokes] != 0 + && !seenOffsets.contains(readPointer(fishPokes + 4))) { + encounterAreas.add(readWildArea(fishPokes, Gen3Constants.fishingSlots, mapName + " Fishing")); + seenOffsets.add(readPointer(fishPokes + 4)); + } + + offs += 20; + } + if (romEntry.arrayEntries.containsKey("BattleTrappersBanned")) { + // Some encounter sets aren't allowed to have Pokemon + // with Arena Trap, Shadow Tag etc. + int[] bannedAreas = romEntry.arrayEntries.get("BattleTrappersBanned"); + Set<Pokemon> battleTrappers = new HashSet<>(); + for (Pokemon pk : getPokemon()) { + if (hasBattleTrappingAbility(pk)) { + battleTrappers.add(pk); + } + } + for (int areaIdx : bannedAreas) { + encounterAreas.get(areaIdx).bannedPokemon.addAll(battleTrappers); + } + } + return encounterAreas; + } + + private boolean hasBattleTrappingAbility(Pokemon pokemon) { + return pokemon != null + && (GlobalConstants.battleTrappingAbilities.contains(pokemon.ability1) || GlobalConstants.battleTrappingAbilities + .contains(pokemon.ability2)); + } + + private EncounterSet readWildArea(int offset, int numOfEntries, String setName) { + EncounterSet thisSet = new EncounterSet(); + thisSet.rate = rom[offset]; + thisSet.displayName = setName; + // Grab the *real* pointer to data + int dataOffset = readPointer(offset + 4); + // Read the entries + for (int i = 0; i < numOfEntries; i++) { + // min, max, species, species + Encounter enc = new Encounter(); + enc.level = rom[dataOffset + i * 4]; + enc.maxLevel = rom[dataOffset + i * 4 + 1]; + try { + enc.pokemon = pokesInternal[readWord(dataOffset + i * 4 + 2)]; + } catch (ArrayIndexOutOfBoundsException ex) { + throw ex; + } + thisSet.encounters.add(enc); + } + return thisSet; + } + + @Override + public void setEncounters(boolean useTimeOfDay, List<EncounterSet> encounters) { + // Support Deoxys/Mew catches in E/FR/LG + attemptObedienceEvolutionPatches(); + + int startOffs = romEntry.getValue("WildPokemon"); + Iterator<EncounterSet> encounterAreas = encounters.iterator(); + Set<Integer> seenOffsets = new TreeSet<>(); + int offs = startOffs; + while (true) { + // Read pointers + int bank = rom[offs] & 0xFF; + int map = rom[offs + 1] & 0xFF; + if (bank == 0xFF && map == 0xFF) { + break; + } + + int grassPokes = readPointer(offs + 4); + int waterPokes = readPointer(offs + 8); + int treePokes = readPointer(offs + 12); + int fishPokes = readPointer(offs + 16); + + // Add pokemanz + if (grassPokes >= 0 && grassPokes < rom.length && rom[grassPokes] != 0 + && !seenOffsets.contains(readPointer(grassPokes + 4))) { + writeWildArea(grassPokes, Gen3Constants.grassSlots, encounterAreas.next()); + seenOffsets.add(readPointer(grassPokes + 4)); + } + if (waterPokes >= 0 && waterPokes < rom.length && rom[waterPokes] != 0 + && !seenOffsets.contains(readPointer(waterPokes + 4))) { + writeWildArea(waterPokes, Gen3Constants.surfingSlots, encounterAreas.next()); + seenOffsets.add(readPointer(waterPokes + 4)); + } + if (treePokes >= 0 && treePokes < rom.length && rom[treePokes] != 0 + && !seenOffsets.contains(readPointer(treePokes + 4))) { + writeWildArea(treePokes, Gen3Constants.rockSmashSlots, encounterAreas.next()); + seenOffsets.add(readPointer(treePokes + 4)); + } + if (fishPokes >= 0 && fishPokes < rom.length && rom[fishPokes] != 0 + && !seenOffsets.contains(readPointer(fishPokes + 4))) { + writeWildArea(fishPokes, Gen3Constants.fishingSlots, encounterAreas.next()); + seenOffsets.add(readPointer(fishPokes + 4)); + } + + offs += 20; + } + } + + @Override + public boolean hasWildAltFormes() { + return false; + } + + @Override + public List<Pokemon> bannedForWildEncounters() { + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // Ban Unown in FRLG because the game crashes if it is encountered outside of Tanoby Ruins. + // See GenerateWildMon in wild_encounter.c in pokefirered + return new ArrayList<>(Collections.singletonList(pokes[Species.unown])); + } + return new ArrayList<>(); + } + + @Override + public List<Trainer> getTrainers() { + int baseOffset = romEntry.getValue("TrainerData"); + int amount = romEntry.getValue("TrainerCount"); + int entryLen = romEntry.getValue("TrainerEntrySize"); + List<Trainer> theTrainers = new ArrayList<>(); + List<String> tcnames = this.getTrainerClassNames(); + for (int i = 1; i < amount; i++) { + // Trainer entries are 40 bytes + // Team flags; 1 byte; 0x01 = custom moves, 0x02 = held item + // Class; 1 byte + // Encounter Music and gender; 1 byte + // Battle Sprite; 1 byte + // Name; 12 bytes; 0xff terminated + // Items; 2 bytes each, 4 item slots + // Battle Mode; 1 byte; 0 means single, 1 means double. + // 3 bytes not used + // AI Flags; 1 byte + // 3 bytes not used + // Number of pokemon in team; 1 byte + // 3 bytes not used + // Pointer to pokemon; 4 bytes + // https://github.com/pret/pokefirered/blob/3dce3407d5f9bca69d61b1cf1b314fb1e921d572/include/battle.h#L111 + int trOffset = baseOffset + i * entryLen; + Trainer tr = new Trainer(); + tr.offset = trOffset; + tr.index = i; + int trainerclass = rom[trOffset + 1] & 0xFF; + tr.trainerclass = (rom[trOffset + 2] & 0x80) > 0 ? 1 : 0; + + int pokeDataType = rom[trOffset] & 0xFF; + boolean doubleBattle = rom[trOffset + (entryLen - 16)] == 0x01; + int numPokes = rom[trOffset + (entryLen - 8)] & 0xFF; + int pointerToPokes = readPointer(trOffset + (entryLen - 4)); + tr.poketype = pokeDataType; + tr.name = this.readVariableLengthString(trOffset + 4); + tr.fullDisplayName = tcnames.get(trainerclass) + " " + tr.name; + // Pokemon structure data is like + // IV IV LV SP SP + // (HI HI) + // (M1 M1 M2 M2 M3 M3 M4 M4) + // IV is a "difficulty" level between 0 and 255 to represent 0 to 31 IVs. + // These IVs affect all attributes. For the vanilla games, the majority + // of trainers have 0 IVs; Elite Four members will have 31 IVs. + // https://github.com/pret/pokeemerald/blob/6c38837b266c0dd36ccdd04559199282daa7a8a0/include/data.h#L22 + if (pokeDataType == 0) { + // blocks of 8 bytes + for (int poke = 0; poke < numPokes; poke++) { + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.IVs = ((readWord(pointerToPokes + poke * 8) & 0xFF) * 31) / 255; + thisPoke.level = readWord(pointerToPokes + poke * 8 + 2); + thisPoke.pokemon = pokesInternal[readWord(pointerToPokes + poke * 8 + 4)]; + tr.pokemon.add(thisPoke); + } + } else if (pokeDataType == 2) { + // blocks of 8 bytes + for (int poke = 0; poke < numPokes; poke++) { + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.IVs = ((readWord(pointerToPokes + poke * 8) & 0xFF) * 31) / 255; + thisPoke.level = readWord(pointerToPokes + poke * 8 + 2); + thisPoke.pokemon = pokesInternal[readWord(pointerToPokes + poke * 8 + 4)]; + thisPoke.heldItem = readWord(pointerToPokes + poke * 8 + 6); + tr.pokemon.add(thisPoke); + } + } else if (pokeDataType == 1) { + // blocks of 16 bytes + for (int poke = 0; poke < numPokes; poke++) { + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.IVs = ((readWord(pointerToPokes + poke * 16) & 0xFF) * 31) / 255; + thisPoke.level = readWord(pointerToPokes + poke * 16 + 2); + thisPoke.pokemon = pokesInternal[readWord(pointerToPokes + poke * 16 + 4)]; + for (int move = 0; move < 4; move++) { + thisPoke.moves[move] = readWord(pointerToPokes + poke * 16 + 6 + (move*2)); + } + tr.pokemon.add(thisPoke); + } + } else if (pokeDataType == 3) { + // blocks of 16 bytes + for (int poke = 0; poke < numPokes; poke++) { + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.IVs = ((readWord(pointerToPokes + poke * 16) & 0xFF) * 31) / 255; + thisPoke.level = readWord(pointerToPokes + poke * 16 + 2); + thisPoke.pokemon = pokesInternal[readWord(pointerToPokes + poke * 16 + 4)]; + thisPoke.heldItem = readWord(pointerToPokes + poke * 16 + 6); + for (int move = 0; move < 4; move++) { + thisPoke.moves[move] = readWord(pointerToPokes + poke * 16 + 8 + (move*2)); + } + tr.pokemon.add(thisPoke); + } + } + theTrainers.add(tr); + } + + if (romEntry.romType == Gen3Constants.RomType_Em) { + int mossdeepStevenOffset = romEntry.getValue("MossdeepStevenTeamOffset"); + Trainer mossdeepSteven = new Trainer(); + mossdeepSteven.offset = mossdeepStevenOffset; + mossdeepSteven.index = amount; + mossdeepSteven.poketype = 1; // Custom moves, but no held items + + // This is literally how the game does it too, lol. Have to subtract one because the + // trainers internally are one-indexed, but then theTrainers is zero-indexed. + Trainer meteorFallsSteven = theTrainers.get(Gen3Constants.emMeteorFallsStevenIndex - 1); + mossdeepSteven.trainerclass = meteorFallsSteven.trainerclass; + mossdeepSteven.name = meteorFallsSteven.name; + mossdeepSteven.fullDisplayName = meteorFallsSteven.fullDisplayName; + + for (int i = 0; i < 3; i++) { + int currentOffset = mossdeepStevenOffset + (i * 20); + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.pokemon = pokesInternal[readWord(currentOffset)]; + thisPoke.IVs = rom[currentOffset + 2]; + thisPoke.level = rom[currentOffset + 3]; + for (int move = 0; move < 4; move++) { + thisPoke.moves[move] = readWord(currentOffset + 12 + (move * 2)); + } + mossdeepSteven.pokemon.add(thisPoke); + } + + theTrainers.add(mossdeepSteven); + } + + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + Gen3Constants.trainerTagsRS(theTrainers, romEntry.romType); + } else if (romEntry.romType == Gen3Constants.RomType_Em) { + Gen3Constants.trainerTagsE(theTrainers); + Gen3Constants.setMultiBattleStatusEm(theTrainers); + } else { + Gen3Constants.trainerTagsFRLG(theTrainers); + } + return theTrainers; + } + + @Override + public List<Integer> getEvolutionItems() { + return Gen3Constants.evolutionItems; + } + + @Override + public List<Integer> getXItems() { + return Gen3Constants.xItems; + } + + @Override + public List<Integer> getMainPlaythroughTrainers() { + return new ArrayList<>(); // Not implemented + } + + @Override + public List<Integer> getEliteFourTrainers(boolean isChallengeMode) { + return Arrays.stream(romEntry.arrayEntries.get("EliteFourIndices")).boxed().collect(Collectors.toList()); + } + + + @Override + public void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode) { + int baseOffset = romEntry.getValue("TrainerData"); + int amount = romEntry.getValue("TrainerCount"); + int entryLen = romEntry.getValue("TrainerEntrySize"); + Iterator<Trainer> theTrainers = trainerData.iterator(); + int fso = romEntry.getValue("FreeSpace"); + + // Get current movesets in case we need to reset them for certain + // trainer mons. + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + + for (int i = 1; i < amount; i++) { + int trOffset = baseOffset + i * entryLen; + Trainer tr = theTrainers.next(); + // Do we need to repoint this trainer's data? + int oldPokeType = rom[trOffset] & 0xFF; + int oldPokeCount = rom[trOffset + (entryLen - 8)] & 0xFF; + int newPokeCount = tr.pokemon.size(); + int newDataSize = newPokeCount * ((tr.poketype & 1) == 1 ? 16 : 8); + int oldDataSize = oldPokeCount * ((oldPokeType & 1) == 1 ? 16 : 8); + + // write out new data first... + rom[trOffset] = (byte) tr.poketype; + rom[trOffset + (entryLen - 8)] = (byte) newPokeCount; + if (doubleBattleMode) { + if (!tr.skipImportant()) { + rom[trOffset + (entryLen - 16)] = 0x01; + } + } + + // now, do we need to repoint? + int pointerToPokes; + if (newDataSize > oldDataSize) { + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, newDataSize, fso, true); + if (writeSpace < fso) { + throw new RandomizerIOException("ROM is full"); + } + writePointer(trOffset + (entryLen - 4), writeSpace); + pointerToPokes = writeSpace; + } else { + pointerToPokes = readPointer(trOffset + (entryLen - 4)); + } + + Iterator<TrainerPokemon> pokes = tr.pokemon.iterator(); + + // Write out Pokemon data! + if (tr.pokemonHaveCustomMoves()) { + // custom moves, blocks of 16 bytes + for (int poke = 0; poke < newPokeCount; poke++) { + TrainerPokemon tp = pokes.next(); + // Add 1 to offset integer division truncation + writeWord(pointerToPokes + poke * 16, Math.min(255, 1 + (tp.IVs * 255) / 31)); + writeWord(pointerToPokes + poke * 16 + 2, tp.level); + writeWord(pointerToPokes + poke * 16 + 4, pokedexToInternal[tp.pokemon.number]); + int movesStart; + if (tr.pokemonHaveItems()) { + writeWord(pointerToPokes + poke * 16 + 6, tp.heldItem); + movesStart = 8; + } else { + movesStart = 6; + writeWord(pointerToPokes + poke * 16 + 14, 0); + } + if (tp.resetMoves) { + int[] pokeMoves = RomFunctions.getMovesAtLevel(tp.pokemon.number, movesets, tp.level); + for (int m = 0; m < 4; m++) { + writeWord(pointerToPokes + poke * 16 + movesStart + m * 2, pokeMoves[m]); + } + } else { + writeWord(pointerToPokes + poke * 16 + movesStart, tp.moves[0]); + writeWord(pointerToPokes + poke * 16 + movesStart + 2, tp.moves[1]); + writeWord(pointerToPokes + poke * 16 + movesStart + 4, tp.moves[2]); + writeWord(pointerToPokes + poke * 16 + movesStart + 6, tp.moves[3]); + } + } + } else { + // no moves, blocks of 8 bytes + for (int poke = 0; poke < newPokeCount; poke++) { + TrainerPokemon tp = pokes.next(); + writeWord(pointerToPokes + poke * 8, Math.min(255, 1 + (tp.IVs * 255) / 31)); + writeWord(pointerToPokes + poke * 8 + 2, tp.level); + writeWord(pointerToPokes + poke * 8 + 4, pokedexToInternal[tp.pokemon.number]); + if (tr.pokemonHaveItems()) { + writeWord(pointerToPokes + poke * 8 + 6, tp.heldItem); + } else { + writeWord(pointerToPokes + poke * 8 + 6, 0); + } + } + } + } + + if (romEntry.romType == Gen3Constants.RomType_Em) { + int mossdeepStevenOffset = romEntry.getValue("MossdeepStevenTeamOffset"); + Trainer mossdeepSteven = trainerData.get(amount - 1); + + for (int i = 0; i < 3; i++) { + int currentOffset = mossdeepStevenOffset + (i * 20); + TrainerPokemon tp = mossdeepSteven.pokemon.get(i); + writeWord(currentOffset, pokedexToInternal[tp.pokemon.number]); + rom[currentOffset + 2] = (byte)tp.IVs; + rom[currentOffset + 3] = (byte)tp.level; + for (int move = 0; move < 4; move++) { + writeWord(currentOffset + 12 + (move * 2), tp.moves[move]); + } + } + } + } + + private void writeWildArea(int offset, int numOfEntries, EncounterSet encounters) { + // Grab the *real* pointer to data + int dataOffset = readPointer(offset + 4); + // Write the entries + for (int i = 0; i < numOfEntries; i++) { + Encounter enc = encounters.encounters.get(i); + // min, max, species, species + int levels = enc.level | (enc.maxLevel << 8); + writeWord(dataOffset + i * 4, levels); + writeWord(dataOffset + i * 4 + 2, pokedexToInternal[enc.pokemon.number]); + } + } + + @Override + public List<Pokemon> getPokemon() { + return pokemonList; + } + + @Override + public List<Pokemon> getPokemonInclFormes() { + return pokemonList; // No alt formes for now, should include Deoxys formes in the future + } + + @Override + public List<Pokemon> getAltFormes() { + return new ArrayList<>(); + } + + @Override + public List<MegaEvolution> getMegaEvolutions() { + return new ArrayList<>(); + } + + @Override + public Pokemon getAltFormeOfPokemon(Pokemon pk, int forme) { + return pk; + } + + @Override + public List<Pokemon> getIrregularFormes() { + return new ArrayList<>(); + } + + @Override + public boolean hasFunctionalFormes() { + return false; + } + + @Override + public Map<Integer, List<MoveLearnt>> getMovesLearnt() { + Map<Integer, List<MoveLearnt>> movesets = new TreeMap<>(); + int baseOffset = romEntry.getValue("PokemonMovesets"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pkmn = pokemonList.get(i); + int offsToPtr = baseOffset + (pokedexToInternal[pkmn.number]) * 4; + int moveDataLoc = readPointer(offsToPtr); + List<MoveLearnt> moves = new ArrayList<>(); + if (jamboMovesetHack) { + while ((rom[moveDataLoc] & 0xFF) != 0x00 || (rom[moveDataLoc + 1] & 0xFF) != 0x00 + || (rom[moveDataLoc + 2] & 0xFF) != 0xFF) { + MoveLearnt ml = new MoveLearnt(); + ml.level = rom[moveDataLoc + 2] & 0xFF; + ml.move = readWord(moveDataLoc); + moves.add(ml); + moveDataLoc += 3; + } + } else { + while ((rom[moveDataLoc] & 0xFF) != 0xFF || (rom[moveDataLoc + 1] & 0xFF) != 0xFF) { + int move = (rom[moveDataLoc] & 0xFF); + int level = (rom[moveDataLoc + 1] & 0xFE) >> 1; + if ((rom[moveDataLoc + 1] & 0x01) == 0x01) { + move += 0x100; + } + MoveLearnt ml = new MoveLearnt(); + ml.level = level; + ml.move = move; + moves.add(ml); + moveDataLoc += 2; + } + } + movesets.put(pkmn.number, moves); + } + return movesets; + } + + @Override + public void setMovesLearnt(Map<Integer, List<MoveLearnt>> movesets) { + int baseOffset = romEntry.getValue("PokemonMovesets"); + int fso = romEntry.getValue("FreeSpace"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pkmn = pokemonList.get(i); + int offsToPtr = baseOffset + (pokedexToInternal[pkmn.number]) * 4; + int moveDataLoc = readPointer(offsToPtr); + List<MoveLearnt> moves = movesets.get(pkmn.number); + int newMoveCount = moves.size(); + int mloc = moveDataLoc; + int entrySize; + if (jamboMovesetHack) { + while ((rom[mloc] & 0xFF) != 0x00 || (rom[mloc + 1] & 0xFF) != 0x00 || (rom[mloc + 2] & 0xFF) != 0xFF) { + mloc += 3; + } + entrySize = 3; + } else { + while ((rom[mloc] & 0xFF) != 0xFF || (rom[mloc + 1] & 0xFF) != 0xFF) { + mloc += 2; + } + entrySize = 2; + } + int currentMoveCount = (mloc - moveDataLoc) / entrySize; + + if (newMoveCount > currentMoveCount) { + // Repoint for more space + int newBytesNeeded = newMoveCount * entrySize + entrySize * 2; + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, newBytesNeeded, fso); + if (writeSpace < fso) { + throw new RandomizerIOException("ROM is full"); + } + writePointer(offsToPtr, writeSpace); + moveDataLoc = writeSpace; + } + + // Write new moveset now that space is ensured. + for (MoveLearnt ml : moves) { + moveDataLoc += writeMLToOffset(moveDataLoc, ml); + } + + // If move count changed, new terminator is required + // In the repoint enough space was reserved to add some padding to + // make sure the terminator isn't detected as free space. + // If no repoint, the padding goes over the old moves/terminator. + if (newMoveCount != currentMoveCount) { + if (jamboMovesetHack) { + rom[moveDataLoc] = 0x00; + rom[moveDataLoc + 1] = 0x00; + rom[moveDataLoc + 2] = (byte) 0xFF; + rom[moveDataLoc + 3] = 0x00; + rom[moveDataLoc + 4] = 0x00; + rom[moveDataLoc + 5] = 0x00; + } else { + rom[moveDataLoc] = (byte) 0xFF; + rom[moveDataLoc + 1] = (byte) 0xFF; + rom[moveDataLoc + 2] = 0x00; + rom[moveDataLoc + 3] = 0x00; + } + } + + } + + } + + private int writeMLToOffset(int offset, MoveLearnt ml) { + if (jamboMovesetHack) { + writeWord(offset, ml.move); + rom[offset + 2] = (byte) ml.level; + return 3; + } else { + rom[offset] = (byte) (ml.move & 0xFF); + int levelPart = (ml.level << 1) & 0xFE; + if (ml.move > 255) { + levelPart++; + } + rom[offset + 1] = (byte) levelPart; + return 2; + } + } + + @Override + public Map<Integer, List<Integer>> getEggMoves() { + Map<Integer, List<Integer>> eggMoves = new TreeMap<>(); + int baseOffset = romEntry.getValue("EggMoves"); + int currentOffset = baseOffset; + int currentSpecies = 0; + List<Integer> currentMoves = new ArrayList<>(); + int val = FileFunctions.read2ByteInt(rom, currentOffset); + + // Check egg_moves.h in the Gen 3 decomps for more info on how this algorithm works. + while (val != 0xFFFF) { + if (val > 20000) { + int species = val - 20000; + if (currentMoves.size() > 0) { + eggMoves.put(internalToPokedex[currentSpecies], currentMoves); + } + currentSpecies = species; + currentMoves = new ArrayList<>(); + } else { + currentMoves.add(val); + } + currentOffset += 2; + val = FileFunctions.read2ByteInt(rom, currentOffset); + } + + // Need to make sure the last entry gets recorded too + if (currentMoves.size() > 0) { + eggMoves.put(internalToPokedex[currentSpecies], currentMoves); + } + return eggMoves; + } + + @Override + public void setEggMoves(Map<Integer, List<Integer>> eggMoves) { + int baseOffset = romEntry.getValue("EggMoves"); + int currentOffset = baseOffset; + for (int species : eggMoves.keySet()) { + FileFunctions.write2ByteInt(rom, currentOffset, pokedexToInternal[species] + 20000); + currentOffset += 2; + for (int move : eggMoves.get(species)) { + FileFunctions.write2ByteInt(rom, currentOffset, move); + currentOffset += 2; + } + } + } + + private static class StaticPokemon { + private int[] speciesOffsets; + private int[] levelOffsets; + + public StaticPokemon() { + this.speciesOffsets = new int[0]; + this.levelOffsets = new int[0]; + } + + public Pokemon getPokemon(Gen3RomHandler parent) { + return parent.pokesInternal[parent.readWord(speciesOffsets[0])]; + } + + public void setPokemon(Gen3RomHandler parent, Pokemon pkmn) { + int value = parent.pokedexToInternal[pkmn.number]; + for (int offset : speciesOffsets) { + parent.writeWord(offset, value); + } + } + + public int getLevel(byte[] rom, int i) { + if (levelOffsets.length <= i) { + return 1; + } + return rom[levelOffsets[i]]; + } + + public void setLevel(byte[] rom, int level, int i) { + if (levelOffsets.length > i) { // Might not have a level entry e.g., it's an egg + rom[levelOffsets[i]] = (byte) level; + } + } + } + + @Override + public List<StaticEncounter> getStaticPokemon() { + List<StaticEncounter> statics = new ArrayList<>(); + List<StaticPokemon> staticsHere = romEntry.staticPokemon; + int[] staticEggOffsets = new int[0]; + if (romEntry.arrayEntries.containsKey("StaticEggPokemonOffsets")) { + staticEggOffsets = romEntry.arrayEntries.get("StaticEggPokemonOffsets"); + } + for (int i = 0; i < staticsHere.size(); i++) { + int currentOffset = i; + StaticPokemon staticPK = staticsHere.get(i); + StaticEncounter se = new StaticEncounter(); + se.pkmn = staticPK.getPokemon(this); + se.level = staticPK.getLevel(rom, 0); + se.isEgg = Arrays.stream(staticEggOffsets).anyMatch(x-> x == currentOffset); + statics.add(se); + } + + if (romEntry.codeTweaks.get("StaticFirstBattleTweak") != null) { + // Read in and randomize the static starting Poochyena/Zigzagoon fight in RSE + int startingSpeciesOffset = romEntry.getValue("StaticFirstBattleSpeciesOffset"); + int species = readWord(startingSpeciesOffset); + if (species == 0xFFFF) { + // Patch hasn't been applied, so apply it first + try { + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("StaticFirstBattleTweak")); + species = readWord(startingSpeciesOffset); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + Pokemon pkmn = pokesInternal[species]; + int startingLevelOffset = romEntry.getValue("StaticFirstBattleLevelOffset"); + int level = rom[startingLevelOffset]; + StaticEncounter se = new StaticEncounter(); + se.pkmn = pkmn; + se.level = level; + statics.add(se); + } else if (romEntry.codeTweaks.get("GhostMarowakTweak") != null) { + // Read in and randomize the static Ghost Marowak fight in FRLG + int[] ghostMarowakOffsets = romEntry.arrayEntries.get("GhostMarowakSpeciesOffsets"); + int species = readWord(ghostMarowakOffsets[0]); + if (species == 0xFFFF) { + // Patch hasn't been applied, so apply it first + try { + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("GhostMarowakTweak")); + species = readWord(ghostMarowakOffsets[0]); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + Pokemon pkmn = pokesInternal[species]; + int[] startingLevelOffsets = romEntry.arrayEntries.get("GhostMarowakLevelOffsets"); + int level = rom[startingLevelOffsets[0]]; + StaticEncounter se = new StaticEncounter(); + se.pkmn = pkmn; + se.level = level; + statics.add(se); + } + + try { + getRoamers(statics); + } catch (Exception e) { + throw new RandomizerIOException(e); + } + + return statics; + } + + @Override + public boolean setStaticPokemon(List<StaticEncounter> staticPokemon) { + // Support Deoxys/Mew gifts/catches in E/FR/LG + attemptObedienceEvolutionPatches(); + + List<StaticPokemon> staticsHere = romEntry.staticPokemon; + int roamerSize = romEntry.roamingPokemon.size(); + if (romEntry.romType == Gen3Constants.RomType_Em) { + // Emerald roamers are set as linkedEncounters to their respective + // Southern Island statics and thus don't count. + roamerSize = 0; + } + int hardcodedStaticSize = 0; + if (romEntry.codeTweaks.get("StaticFirstBattleTweak") != null || romEntry.codeTweaks.get("GhostMarowakTweak") != null) { + hardcodedStaticSize = 1; + } + + if (staticPokemon.size() != staticsHere.size() + hardcodedStaticSize + roamerSize) { + return false; + } + + for (int i = 0; i < staticsHere.size(); i++) { + staticsHere.get(i).setPokemon(this, staticPokemon.get(i).pkmn); + staticsHere.get(i).setLevel(rom, staticPokemon.get(i).level, 0); + } + + if (romEntry.codeTweaks.get("StaticFirstBattleTweak") != null) { + StaticEncounter startingFirstBattle = staticPokemon.get(romEntry.getValue("StaticFirstBattleOffset")); + int startingSpeciesOffset = romEntry.getValue("StaticFirstBattleSpeciesOffset"); + writeWord(startingSpeciesOffset, pokedexToInternal[startingFirstBattle.pkmn.number]); + int startingLevelOffset = romEntry.getValue("StaticFirstBattleLevelOffset"); + rom[startingLevelOffset] = (byte) startingFirstBattle.level; + } else if (romEntry.codeTweaks.get("GhostMarowakTweak") != null) { + StaticEncounter ghostMarowak = staticPokemon.get(romEntry.getValue("GhostMarowakOffset")); + int[] ghostMarowakSpeciesOffsets = romEntry.arrayEntries.get("GhostMarowakSpeciesOffsets"); + for (int i = 0; i < ghostMarowakSpeciesOffsets.length; i++) { + writeWord(ghostMarowakSpeciesOffsets[i], pokedexToInternal[ghostMarowak.pkmn.number]); + } + int[] ghostMarowakLevelOffsets = romEntry.arrayEntries.get("GhostMarowakLevelOffsets"); + for (int i = 0; i < ghostMarowakLevelOffsets.length; i++) { + rom[ghostMarowakLevelOffsets[i]] = (byte) ghostMarowak.level; + } + + // The code for creating Ghost Marowak tries to ensure the Pokemon is female. If the Pokemon + // cannot be female (because they are always male or an indeterminate gender), then the game + // will infinite loop trying and failing to make the Pokemon female. For Pokemon that cannot + // be female, change the specified gender to something that actually works. + int ghostMarowakGenderOffset = romEntry.getValue("GhostMarowakGenderOffset"); + if (ghostMarowak.pkmn.genderRatio == 0 || ghostMarowak.pkmn.genderRatio == 0xFF) { + // 0x00 is 100% male, and 0xFF is indeterminate gender + rom[ghostMarowakGenderOffset] = (byte) ghostMarowak.pkmn.genderRatio; + } + } + + setRoamers(staticPokemon); + return true; + } + + private void getRoamers(List<StaticEncounter> statics) throws IOException { + if (romEntry.romType == Gen3Constants.RomType_Ruby) { + int firstSpecies = readWord(rom, romEntry.roamingPokemon.get(0).speciesOffsets[0]); + if (firstSpecies == 0) { + // Before applying the patch, the first species offset will be pointing to + // the lower bytes of 0x2000000, so when it reads a word, it will be 0. + applyRubyRoamerPatch(); + } + StaticPokemon roamer = romEntry.roamingPokemon.get(0); + StaticEncounter se = new StaticEncounter(); + se.pkmn = roamer.getPokemon(this); + se.level = roamer.getLevel(rom, 0); + statics.add(se); + } else if (romEntry.romType == Gen3Constants.RomType_Sapp) { + StaticPokemon roamer = romEntry.roamingPokemon.get(0); + StaticEncounter se = new StaticEncounter(); + se.pkmn = roamer.getPokemon(this); + se.level = roamer.getLevel(rom, 0); + statics.add(se); + } else if (romEntry.romType == Gen3Constants.RomType_FRLG && romEntry.codeTweaks.get("RoamingPokemonTweak") != null) { + int firstSpecies = readWord(rom, romEntry.roamingPokemon.get(0).speciesOffsets[0]); + if (firstSpecies == 0xFFFF) { + // This means that the IPS patch hasn't been applied yet, since the first species + // ID location is free space. + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("RoamingPokemonTweak")); + } + for (int i = 0; i < romEntry.roamingPokemon.size(); i++) { + StaticPokemon roamer = romEntry.roamingPokemon.get(i); + StaticEncounter se = new StaticEncounter(); + se.pkmn = roamer.getPokemon(this); + se.level = roamer.getLevel(rom, 0); + statics.add(se); + } + } else if (romEntry.romType == Gen3Constants.RomType_Em) { + int firstSpecies = readWord(rom, romEntry.roamingPokemon.get(0).speciesOffsets[0]); + if (firstSpecies >= pokesInternal.length) { + // Before applying the patch, the first species offset is a pointer with a huge value. + // Thus, this check is a good indicator that the patch needs to be applied. + applyEmeraldRoamerPatch(); + } + int[] southernIslandOffsets = romEntry.arrayEntries.get("StaticSouthernIslandOffsets"); + for (int i = 0; i < romEntry.roamingPokemon.size(); i++) { + StaticPokemon roamer = romEntry.roamingPokemon.get(i); + StaticEncounter se = new StaticEncounter(); + se.pkmn = roamer.getPokemon(this); + se.level = roamer.getLevel(rom, 0); + + // Link each roamer to their respective Southern Island static encounter so that + // they randomize to the same species. + StaticEncounter southernIslandEncounter = statics.get(southernIslandOffsets[i]); + southernIslandEncounter.linkedEncounters.add(se); + } + } + } + + private void setRoamers(List<StaticEncounter> statics) { + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + StaticEncounter roamerEncounter = statics.get(statics.size() - 1); + StaticPokemon roamer = romEntry.roamingPokemon.get(0); + roamer.setPokemon(this, roamerEncounter.pkmn); + for (int i = 0; i < roamer.levelOffsets.length; i++) { + roamer.setLevel(rom, roamerEncounter.level, i); + } + } else if (romEntry.romType == Gen3Constants.RomType_FRLG && romEntry.codeTweaks.get("RoamingPokemonTweak") != null) { + for (int i = 0; i < romEntry.roamingPokemon.size(); i++) { + int offsetInStaticList = statics.size() - 3 + i; + StaticEncounter roamerEncounter = statics.get(offsetInStaticList); + StaticPokemon roamer = romEntry.roamingPokemon.get(i); + roamer.setPokemon(this, roamerEncounter.pkmn); + for (int j = 0; j < roamer.levelOffsets.length; j++) { + roamer.setLevel(rom, roamerEncounter.level, j); + } + } + } else if (romEntry.romType == Gen3Constants.RomType_Em) { + int[] southernIslandOffsets = romEntry.arrayEntries.get("StaticSouthernIslandOffsets"); + for (int i = 0; i < romEntry.roamingPokemon.size(); i++) { + StaticEncounter southernIslandEncounter = statics.get(southernIslandOffsets[i]); + StaticEncounter roamerEncounter = southernIslandEncounter.linkedEncounters.get(0); + StaticPokemon roamer = romEntry.roamingPokemon.get(i); + roamer.setPokemon(this, roamerEncounter.pkmn); + for (int j = 0; j < roamer.levelOffsets.length; j++) { + roamer.setLevel(rom, roamerEncounter.level, j); + } + } + } + } + + private void applyRubyRoamerPatch() { + int offset = romEntry.getValue("FindMapsWithMonFunctionStartOffset"); + + // The constant 0x2000000 is actually in the function twice, so we'll replace the first instance + // with Latios's ID. First, change the "ldr r2, [pc, #0x68]" near the start of the function to + // "ldr r2, [pc, #0x15C]" so it points to the second usage of 0x2000000 + rom[offset + 22] = 0x57; + + // In the space formerly occupied by the first 0x2000000, write Latios's ID + FileFunctions.writeFullInt(rom, offset + 128, pokedexToInternal[Species.latios]); + + // Where the original function computes Latios's ID by setting r0 to 0xCC << 1, just pc-relative + // load our constant. We have four bytes of space to play with, and we need to make sure the offset + // from the pc is 4-byte aligned; we need to nop for alignment and then perform the load. + rom[offset + 12] = 0x00; + rom[offset + 13] = 0x00; + rom[offset + 14] = 0x1C; + rom[offset + 15] = 0x48; + + offset = romEntry.getValue("CreateInitialRoamerMonFunctionStartOffset"); + + // At the very end of the function, the game pops the lr from the stack and stores it in r0, then + // it does "bx r0" to jump back to the caller, and then it has two bytes of padding afterwards. For + // some reason, Ruby very rarely does "pop { pc }" even though that seemingly works fine. By doing + // that, we only need one instruction to return to the caller, giving us four bytes to write + // Latios's species ID. + rom[offset + 182] = 0x00; + rom[offset + 183] = (byte) 0xBD; + FileFunctions.writeFullInt(rom, offset + 184, pokedexToInternal[Species.latios]); + + // Now write a pc-relative load to this new species ID constant over the original move and lsl. Similar + // to before, we need to write a nop first for alignment, then pc-relative load into r6. + rom[offset + 10] = 0x00; + rom[offset + 11] = 0x00; + rom[offset + 12] = 0x2A; + rom[offset + 13] = 0x4E; + } + + private void applyEmeraldRoamerPatch() { + int offset = romEntry.getValue("CreateInitialRoamerMonFunctionStartOffset"); + + // Latias's species ID is already a pc-relative loaded constant, but Latios's isn't. We need to make + // some room for it; the constant 0x03005D8C is actually in the function twice, so we'll replace the first + // instance with Latios's ID. First, change the "ldr r0, [pc, #0xC]" at the start of the function to + // "ldr r0, [pc, #0x104]", so it points to the second usage of 0x03005D8C + rom[offset + 14] = 0x41; + + // In the space formerly occupied by the first 0x03005D8C, write Latios's ID + FileFunctions.writeFullInt(rom, offset + 28, pokedexToInternal[Species.latios]); + + // In the original function, we "lsl r0, r0, #0x10" then compare r0 to 0. The thing is, this left + // shift doesn't actually matter, because 0 << 0x10 = 0, and [non-zero] << 0x10 = [non-zero]. + // Let's move the compare up to take its place and then load Latios's ID into r3 for use in another + // branch later. + rom[offset + 8] = 0x00; + rom[offset + 9] = 0x28; + rom[offset + 10] = 0x04; + rom[offset + 11] = 0x4B; + + // Lastly, in the branch that normally does r2 = 0xCC << 0x1 to compute Latios's ID, just mov r3 + // into r2, since it was loaded with his ID with the above code. + rom[offset + 48] = 0x1A; + rom[offset + 49] = 0x46; + rom[offset + 50] = 0x00; + rom[offset + 51] = 0x00; + } + + @Override + public List<Integer> getTMMoves() { + List<Integer> tms = new ArrayList<>(); + int offset = romEntry.getValue("TmMoves"); + for (int i = 1; i <= Gen3Constants.tmCount; i++) { + tms.add(readWord(offset + (i - 1) * 2)); + } + return tms; + } + + @Override + public List<Integer> getHMMoves() { + return Gen3Constants.hmMoves; + } + + @Override + public void setTMMoves(List<Integer> moveIndexes) { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + int offset = romEntry.getValue("TmMoves"); + for (int i = 1; i <= Gen3Constants.tmCount; i++) { + writeWord(offset + (i - 1) * 2, moveIndexes.get(i - 1)); + } + int otherOffset = romEntry.getValue("TmMovesDuplicate"); + if (otherOffset > 0) { + // Emerald/FR/LG have *two* TM tables + System.arraycopy(rom, offset, rom, otherOffset, Gen3Constants.tmCount * 2); + } + + int iiOffset = romEntry.getValue("ItemImages"); + if (iiOffset > 0) { + int[] pals = romEntry.arrayEntries.get("TmPals"); + // Update the item image palettes + // Gen3 TMs are 289-338 + for (int i = 0; i < 50; i++) { + Move mv = moves[moveIndexes.get(i)]; + int typeID = Gen3Constants.typeToByte(mv.type); + writePointer(iiOffset + (Gen3Constants.tmItemOffset + i) * 8 + 4, pals[typeID]); + } + } + + int fsOffset = romEntry.getValue("FreeSpace"); + + // Item descriptions + if (romEntry.getValue("MoveDescriptions") > 0) { + // JP blocked for now - uses different item structure anyway + int idOffset = romEntry.getValue("ItemData"); + int mdOffset = romEntry.getValue("MoveDescriptions"); + int entrySize = romEntry.getValue("ItemEntrySize"); + int limitPerLine = (romEntry.romType == Gen3Constants.RomType_FRLG) ? Gen3Constants.frlgItemDescCharsPerLine + : Gen3Constants.rseItemDescCharsPerLine; + for (int i = 0; i < Gen3Constants.tmCount; i++) { + int itemBaseOffset = idOffset + (i + Gen3Constants.tmItemOffset) * entrySize; + int moveBaseOffset = mdOffset + (moveIndexes.get(i) - 1) * 4; + int moveTextPointer = readPointer(moveBaseOffset); + String moveDesc = readVariableLengthString(moveTextPointer); + String newItemDesc = RomFunctions.rewriteDescriptionForNewLineSize(moveDesc, "\\n", limitPerLine, ssd); + // Find freespace + int fsBytesNeeded = translateString(newItemDesc).length + 1; + int newItemDescOffset = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, fsBytesNeeded, + fsOffset); + if (newItemDescOffset < fsOffset) { + String nl = System.getProperty("line.separator"); + log("Couldn't insert new item description." + nl); + return; + } + writeVariableLengthString(newItemDesc, newItemDescOffset); + writePointer(itemBaseOffset + Gen3Constants.itemDataDescriptionOffset, newItemDescOffset); + } + } + + // TM Text? + for (TMOrMTTextEntry tte : romEntry.tmmtTexts) { + if (tte.actualOffset > 0 && !tte.isMoveTutor) { + // create the new TM text + int oldPointer = readPointer(tte.actualOffset); + if (oldPointer < 0 || oldPointer >= rom.length) { + String nl = System.getProperty("line.separator"); + log("Couldn't insert new TM text. Skipping remaining TM text updates." + nl); + return; + } + String moveName = this.moves[moveIndexes.get(tte.number - 1)].name; + // temporarily use underscores to stop the move name being split + String tmpMoveName = moveName.replace(' ', '_'); + String unformatted = tte.template.replace("[move]", tmpMoveName); + String newText = RomFunctions.formatTextWithReplacements(unformatted, null, "\\n", "\\l", "\\p", + Gen3Constants.regularTextboxCharsPerLine, ssd); + // get rid of the underscores + newText = newText.replace(tmpMoveName, moveName); + // insert the new text into free space + int fsBytesNeeded = translateString(newText).length + 1; + int newOffset = RomFunctions.freeSpaceFinder(rom, (byte) 0xFF, fsBytesNeeded, fsOffset); + if (newOffset < fsOffset) { + String nl = System.getProperty("line.separator"); + log("Couldn't insert new TM text." + nl); + return; + } + writeVariableLengthString(newText, newOffset); + // search for copies of the pointer: + // make a needle of the pointer + byte[] searchNeedle = new byte[4]; + System.arraycopy(rom, tte.actualOffset, searchNeedle, 0, 4); + // find copies within 500 bytes either way of actualOffset + int minOffset = Math.max(0, tte.actualOffset - Gen3Constants.pointerSearchRadius); + int maxOffset = Math.min(rom.length, tte.actualOffset + Gen3Constants.pointerSearchRadius); + List<Integer> pointerLocs = RomFunctions.search(rom, minOffset, maxOffset, searchNeedle); + for (int pointerLoc : pointerLocs) { + // write the new pointer + writePointer(pointerLoc, newOffset); + } + } + } + } + + private RomFunctions.StringSizeDeterminer ssd = encodedText -> translateString(encodedText).length; + + @Override + public int getTMCount() { + return Gen3Constants.tmCount; + } + + @Override + public int getHMCount() { + return Gen3Constants.hmCount; + } + + @Override + public Map<Pokemon, boolean[]> getTMHMCompatibility() { + Map<Pokemon, boolean[]> compat = new TreeMap<>(); + int offset = romEntry.getValue("PokemonTMHMCompat"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pkmn = pokemonList.get(i); + int compatOffset = offset + (pokedexToInternal[pkmn.number]) * 8; + boolean[] flags = new boolean[Gen3Constants.tmCount + Gen3Constants.hmCount + 1]; + for (int j = 0; j < 8; j++) { + readByteIntoFlags(flags, j * 8 + 1, compatOffset + j); + } + compat.put(pkmn, flags); + } + return compat; + } + + @Override + public void setTMHMCompatibility(Map<Pokemon, boolean[]> compatData) { + int offset = romEntry.getValue("PokemonTMHMCompat"); + for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) { + Pokemon pkmn = compatEntry.getKey(); + boolean[] flags = compatEntry.getValue(); + int compatOffset = offset + (pokedexToInternal[pkmn.number]) * 8; + for (int j = 0; j < 8; j++) { + rom[compatOffset + j] = getByteFromFlags(flags, j * 8 + 1); + } + } + } + + @Override + public boolean hasMoveTutors() { + return (romEntry.romType == Gen3Constants.RomType_Em || romEntry.romType == Gen3Constants.RomType_FRLG); + } + + @Override + public List<Integer> getMoveTutorMoves() { + if (!hasMoveTutors()) { + return new ArrayList<>(); + } + List<Integer> mts = new ArrayList<>(); + int moveCount = romEntry.getValue("MoveTutorMoves"); + int offset = romEntry.getValue("MoveTutorData"); + for (int i = 0; i < moveCount; i++) { + mts.add(readWord(offset + i * 2)); + } + return mts; + } + + @Override + public void setMoveTutorMoves(List<Integer> moves) { + if (!hasMoveTutors()) { + return; + } + int moveCount = romEntry.getValue("MoveTutorMoves"); + int offset = romEntry.getValue("MoveTutorData"); + if (moveCount != moves.size()) { + return; + } + for (int i = 0; i < moveCount; i++) { + writeWord(offset + i * 2, moves.get(i)); + } + int fsOffset = romEntry.getValue("FreeSpace"); + + // Move Tutor Text? + for (TMOrMTTextEntry tte : romEntry.tmmtTexts) { + if (tte.actualOffset > 0 && tte.isMoveTutor) { + // create the new MT text + int oldPointer = readPointer(tte.actualOffset); + if (oldPointer < 0 || oldPointer >= rom.length) { + throw new RandomizationException( + "Move Tutor Text update failed: couldn't read a move tutor text pointer."); + } + String moveName = this.moves[moves.get(tte.number)].name; + // temporarily use underscores to stop the move name being split + String tmpMoveName = moveName.replace(' ', '_'); + String unformatted = tte.template.replace("[move]", tmpMoveName); + String newText = RomFunctions.formatTextWithReplacements(unformatted, null, "\\n", "\\l", "\\p", + Gen3Constants.regularTextboxCharsPerLine, ssd); + // get rid of the underscores + newText = newText.replace(tmpMoveName, moveName); + // insert the new text into free space + int fsBytesNeeded = translateString(newText).length + 1; + int newOffset = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, fsBytesNeeded, fsOffset); + if (newOffset < fsOffset) { + String nl = System.getProperty("line.separator"); + log("Couldn't insert new Move Tutor text." + nl); + return; + } + writeVariableLengthString(newText, newOffset); + // search for copies of the pointer: + // make a needle of the pointer + byte[] searchNeedle = new byte[4]; + System.arraycopy(rom, tte.actualOffset, searchNeedle, 0, 4); + // find copies within 500 bytes either way of actualOffset + int minOffset = Math.max(0, tte.actualOffset - Gen3Constants.pointerSearchRadius); + int maxOffset = Math.min(rom.length, tte.actualOffset + Gen3Constants.pointerSearchRadius); + List<Integer> pointerLocs = RomFunctions.search(rom, minOffset, maxOffset, searchNeedle); + for (int pointerLoc : pointerLocs) { + // write the new pointer + writePointer(pointerLoc, newOffset); + } + } + } + } + + @Override + public Map<Pokemon, boolean[]> getMoveTutorCompatibility() { + if (!hasMoveTutors()) { + return new TreeMap<>(); + } + Map<Pokemon, boolean[]> compat = new TreeMap<>(); + int moveCount = romEntry.getValue("MoveTutorMoves"); + int offset = romEntry.getValue("MoveTutorCompatibility"); + int bytesRequired = ((moveCount + 7) & ~7) / 8; + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pkmn = pokemonList.get(i); + int compatOffset = offset + pokedexToInternal[pkmn.number] * bytesRequired; + boolean[] flags = new boolean[moveCount + 1]; + for (int j = 0; j < bytesRequired; j++) { + readByteIntoFlags(flags, j * 8 + 1, compatOffset + j); + } + compat.put(pkmn, flags); + } + return compat; + } + + @Override + public void setMoveTutorCompatibility(Map<Pokemon, boolean[]> compatData) { + if (!hasMoveTutors()) { + return; + } + int moveCount = romEntry.getValue("MoveTutorMoves"); + int offset = romEntry.getValue("MoveTutorCompatibility"); + int bytesRequired = ((moveCount + 7) & ~7) / 8; + for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) { + Pokemon pkmn = compatEntry.getKey(); + boolean[] flags = compatEntry.getValue(); + int compatOffset = offset + pokedexToInternal[pkmn.number] * bytesRequired; + for (int j = 0; j < bytesRequired; j++) { + rom[compatOffset + j] = getByteFromFlags(flags, j * 8 + 1); + } + } + } + + @Override + public String getROMName() { + return romEntry.name; + } + + @Override + public String getROMCode() { + return romEntry.romCode; + } + + @Override + public String getSupportLevel() { + return (romEntry.getValue("StaticPokemonSupport") > 0) ? "Complete" : "No Static Pokemon"; + } + + // For dynamic offsets later + private int find(String hexString) { + return find(rom, hexString); + } + + private static int find(byte[] haystack, String hexString) { + if (hexString.length() % 2 != 0) { + return -3; // error + } + byte[] searchFor = new byte[hexString.length() / 2]; + for (int i = 0; i < searchFor.length; i++) { + searchFor[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16); + } + List<Integer> found = RomFunctions.search(haystack, searchFor); + if (found.size() == 0) { + return -1; // not found + } else if (found.size() > 1) { + return -2; // not unique + } else { + return found.get(0); + } + } + + private List<Integer> findMultiple(String hexString) { + return findMultiple(rom, hexString); + } + + private static List<Integer> findMultiple(byte[] haystack, String hexString) { + if (hexString.length() % 2 != 0) { + return new ArrayList<>(); // error + } + byte[] searchFor = new byte[hexString.length() / 2]; + for (int i = 0; i < searchFor.length; i++) { + searchFor[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16); + } + return RomFunctions.search(haystack, searchFor); + } + + private void writeHexString(String hexString, int offset) { + if (hexString.length() % 2 != 0) { + return; // error + } + for (int i = 0; i < hexString.length() / 2; i++) { + rom[offset + i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16); + } + } + + private void attemptObedienceEvolutionPatches() { + if (havePatchedObedience) { + return; + } + + havePatchedObedience = true; + // This routine *appears* to only exist in E/FR/LG... + // Look for the deoxys part which is + // MOVS R1, 0x19A + // CMP R0, R1 + // BEQ <mew/deoxys case> + // Hex is CD214900 8842 0FD0 + int deoxysObOffset = find(Gen3Constants.deoxysObeyCode); + if (deoxysObOffset > 0) { + // We found the deoxys check... + // Replacing it with MOVS R1, 0x0 would work fine. + // This would make it so species 0x0 (glitch only) would disobey. + // But MOVS R1, 0x0 (the version I know) is 2-byte + // So we just use it twice... + // the equivalent of nop'ing the second time. + rom[deoxysObOffset] = 0x00; + rom[deoxysObOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + rom[deoxysObOffset + 2] = 0x00; + rom[deoxysObOffset + 3] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + // Look for the mew check too... it's 0x16 ahead + if (readWord(deoxysObOffset + Gen3Constants.mewObeyOffsetFromDeoxysObey) == (((Gen3Constants.gbaCmpRxOpcode | Gen3Constants.gbaR0) << 8) | (Species.mew))) { + // Bingo, thats CMP R0, 0x97 + // change to CMP R0, 0x0 + writeWord(deoxysObOffset + Gen3Constants.mewObeyOffsetFromDeoxysObey, + (((Gen3Constants.gbaCmpRxOpcode | Gen3Constants.gbaR0) << 8) | (0))); + } + } + + // Look for evolutions too + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + int evoJumpOffset = find(Gen3Constants.levelEvoKantoDexCheckCode); + if (evoJumpOffset > 0) { + // This currently compares species to 0x97 and then allows + // evolution if it's <= that. + // Allow it regardless by using an unconditional jump instead + writeWord(evoJumpOffset, Gen3Constants.gbaNopOpcode); + writeWord(evoJumpOffset + 2, + ((Gen3Constants.gbaUnconditionalJumpOpcode << 8) | (Gen3Constants.levelEvoKantoDexJumpAmount))); + } + + int stoneJumpOffset = find(Gen3Constants.stoneEvoKantoDexCheckCode); + if (stoneJumpOffset > 0) { + // same as the above, but for stone evos + writeWord(stoneJumpOffset, Gen3Constants.gbaNopOpcode); + writeWord(stoneJumpOffset + 2, + ((Gen3Constants.gbaUnconditionalJumpOpcode << 8) | (Gen3Constants.stoneEvoKantoDexJumpAmount))); + } + } + } + + private void patchForNationalDex() { + log("--Patching for National Dex at Start of Game--"); + String nl = System.getProperty("line.separator"); + int fso = romEntry.getValue("FreeSpace"); + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + // Find the original pokedex script + int pkDexOffset = find(Gen3Constants.rsPokedexScriptIdentifier); + if (pkDexOffset < 0) { + log("Patch unsuccessful." + nl); + return; + } + int textPointer = readPointer(pkDexOffset - 4); + int realScriptLocation = pkDexOffset - 8; + int pointerLocToScript = find(pointerToHexString(realScriptLocation)); + if (pointerLocToScript < 0) { + log("Patch unsuccessful." + nl); + return; + } + // Find free space for our new routine + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, 44, fso); + if (writeSpace < fso) { + log("Patch unsuccessful." + nl); + // Somehow this ROM is full + return; + } + writePointer(pointerLocToScript, writeSpace); + writeHexString(Gen3Constants.rsNatDexScriptPart1, writeSpace); + writePointer(writeSpace + 4, textPointer); + writeHexString(Gen3Constants.rsNatDexScriptPart2, writeSpace + 8); + + } else if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // Find the original pokedex script + int pkDexOffset = find(Gen3Constants.frlgPokedexScriptIdentifier); + if (pkDexOffset < 0) { + log("Patch unsuccessful." + nl); + return; + } + // Find free space for our new routine + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, 10, fso); + if (writeSpace < fso) { + // Somehow this ROM is full + log("Patch unsuccessful." + nl); + return; + } + rom[pkDexOffset] = 4; + writePointer(pkDexOffset + 1, writeSpace); + rom[pkDexOffset + 5] = 0; // NOP + + // Now write our new routine + writeHexString(Gen3Constants.frlgNatDexScript, writeSpace); + + // Fix people using the national dex flag + List<Integer> ndexChecks = findMultiple(Gen3Constants.frlgNatDexFlagChecker); + for (int ndexCheckOffset : ndexChecks) { + // change to a flag-check + // 82C = "beaten e4/gary once" + writeHexString(Gen3Constants.frlgE4FlagChecker, ndexCheckOffset); + } + + // Fix oak in his lab + int oakLabCheckOffs = find(Gen3Constants.frlgOaksLabKantoDexChecker); + if (oakLabCheckOffs > 0) { + // replace it + writeHexString(Gen3Constants.frlgOaksLabFix, oakLabCheckOffs); + } + + // Fix oak outside your house + int oakHouseCheckOffs = find(Gen3Constants.frlgOakOutsideHouseCheck); + if (oakHouseCheckOffs > 0) { + // fix him to use ndex count + writeHexString(Gen3Constants.frlgOakOutsideHouseFix, oakHouseCheckOffs); + } + + // Fix Oak's aides so they look for your National Dex seen/caught, + // not your Kanto Dex seen/caught + int oakAideCheckOffs = find(Gen3Constants.frlgOakAideCheckPrefix); + if (oakAideCheckOffs > 0) { + oakAideCheckOffs += Gen3Constants.frlgOakAideCheckPrefix.length() / 2; // because it was a prefix + // Change the bne instruction to an unconditional branch to always use National Dex + rom[oakAideCheckOffs + 1] = (byte) 0xE0; + } + } else { + // Find the original pokedex script + int pkDexOffset = find(Gen3Constants.ePokedexScriptIdentifier); + if (pkDexOffset < 0) { + log("Patch unsuccessful." + nl); + return; + } + int textPointer = readPointer(pkDexOffset - 4); + int realScriptLocation = pkDexOffset - 8; + int pointerLocToScript = find(pointerToHexString(realScriptLocation)); + if (pointerLocToScript < 0) { + log("Patch unsuccessful." + nl); + return; + } + // Find free space for our new routine + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, 27, fso); + if (writeSpace < fso) { + // Somehow this ROM is full + log("Patch unsuccessful." + nl); + return; + } + writePointer(pointerLocToScript, writeSpace); + writeHexString(Gen3Constants.eNatDexScriptPart1, writeSpace); + writePointer(writeSpace + 4, textPointer); + writeHexString(Gen3Constants.eNatDexScriptPart2, writeSpace + 8); + } + log("Patch successful!" + nl); + } + + private String pointerToHexString(int pointer) { + String hex = String.format("%08X", pointer + 0x08000000); + return new String(new char[] { hex.charAt(6), hex.charAt(7), hex.charAt(4), hex.charAt(5), hex.charAt(2), + hex.charAt(3), hex.charAt(0), hex.charAt(1) }); + } + + private void populateEvolutions() { + for (Pokemon pkmn : pokes) { + if (pkmn != null) { + pkmn.evolutionsFrom.clear(); + pkmn.evolutionsTo.clear(); + } + } + + int baseOffset = romEntry.getValue("PokemonEvolutions"); + int numInternalPokes = romEntry.getValue("PokemonCount"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pk = pokemonList.get(i); + int idx = pokedexToInternal[pk.number]; + int evoOffset = baseOffset + (idx) * 0x28; + for (int j = 0; j < 5; j++) { + int method = readWord(evoOffset + j * 8); + int evolvingTo = readWord(evoOffset + j * 8 + 4); + if (method >= 1 && method <= Gen3Constants.evolutionMethodCount && evolvingTo >= 1 + && evolvingTo <= numInternalPokes) { + int extraInfo = readWord(evoOffset + j * 8 + 2); + EvolutionType et = EvolutionType.fromIndex(3, method); + Evolution evo = new Evolution(pk, pokesInternal[evolvingTo], true, et, extraInfo); + if (!pk.evolutionsFrom.contains(evo)) { + pk.evolutionsFrom.add(evo); + pokesInternal[evolvingTo].evolutionsTo.add(evo); + } + } + } + // Split evos shouldn't carry stats unless the evo is Nincada's + // In that case, we should have Ninjask carry stats + if (pk.evolutionsFrom.size() > 1) { + for (Evolution e : pk.evolutionsFrom) { + if (e.type != EvolutionType.LEVEL_CREATE_EXTRA) { + e.carryStats = false; + } + } + } + } + } + + private void writeEvolutions() { + int baseOffset = romEntry.getValue("PokemonEvolutions"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pk = pokemonList.get(i); + int idx = pokedexToInternal[pk.number]; + int evoOffset = baseOffset + (idx) * 0x28; + int evosWritten = 0; + for (Evolution evo : pk.evolutionsFrom) { + writeWord(evoOffset, evo.type.toIndex(3)); + writeWord(evoOffset + 2, evo.extraInfo); + writeWord(evoOffset + 4, pokedexToInternal[evo.to.number]); + writeWord(evoOffset + 6, 0); + evoOffset += 8; + evosWritten++; + if (evosWritten == 5) { + break; + } + } + while (evosWritten < 5) { + writeWord(evoOffset, 0); + writeWord(evoOffset + 2, 0); + writeWord(evoOffset + 4, 0); + writeWord(evoOffset + 6, 0); + evoOffset += 8; + evosWritten++; + } + } + } + + @Override + public void removeImpossibleEvolutions(Settings settings) { + attemptObedienceEvolutionPatches(); + + // no move evos, so no need to check for those + for (Pokemon pkmn : pokes) { + if (pkmn != null) { + for (Evolution evo : pkmn.evolutionsFrom) { + // Not trades, but impossible without trading + if (evo.type == EvolutionType.HAPPINESS_DAY && romEntry.romType == Gen3Constants.RomType_FRLG) { + // happiness day change to Sun Stone + evo.type = EvolutionType.STONE; + evo.extraInfo = Gen3Items.sunStone; + addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames[Gen3Items.sunStone]); + } + if (evo.type == EvolutionType.HAPPINESS_NIGHT && romEntry.romType == Gen3Constants.RomType_FRLG) { + // happiness night change to Moon Stone + evo.type = EvolutionType.STONE; + evo.extraInfo = Gen3Items.moonStone; + addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames[Gen3Items.moonStone]); + } + if (evo.type == EvolutionType.LEVEL_HIGH_BEAUTY && romEntry.romType == Gen3Constants.RomType_FRLG) { + // beauty change to level 35 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 35; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } + // Pure Trade + if (evo.type == EvolutionType.TRADE) { + // Haunter, Machoke, Kadabra, Graveler + // Make it into level 37, we're done. + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 37; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } + // Trade w/ Held Item + if (evo.type == EvolutionType.TRADE_ITEM) { + if (evo.from.number == Species.poliwhirl) { + // Poliwhirl: Lv 37 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 37; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } else if (evo.from.number == Species.slowpoke) { + // Slowpoke: Water Stone + evo.type = EvolutionType.STONE; + evo.extraInfo = Gen3Items.waterStone; + addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames[Gen3Items.waterStone]); + } else if (evo.from.number == Species.seadra) { + // Seadra: Lv 40 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 40; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } else if (evo.from.number == Species.clamperl + && evo.extraInfo == Gen3Items.deepSeaTooth) { + // Clamperl -> Huntail: Lv30 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 30; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } else if (evo.from.number == Species.clamperl + && evo.extraInfo == Gen3Items.deepSeaScale) { + // Clamperl -> Gorebyss: Water Stone + evo.type = EvolutionType.STONE; + evo.extraInfo = Gen3Items.waterStone; + addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames[Gen3Items.waterStone]); + } else { + // Onix, Scyther or Porygon: Lv30 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 30; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } + } + } + } + } + + } + + @Override + public void makeEvolutionsEasier(Settings settings) { + // Reduce the amount of happiness required to evolve. + int offset = find(rom, Gen3Constants.friendshipValueForEvoLocator); + if (offset > 0) { + // Amount of required happiness for HAPPINESS evolutions. + if (rom[offset] == (byte)219) { + rom[offset] = (byte)159; + } + // FRLG doesn't have code to handle time-based evolutions. + if (romEntry.romType != Gen3Constants.RomType_FRLG) { + // Amount of required happiness for HAPPINESS_DAY evolutions. + if (rom[offset + 38] == (byte)219) { + rom[offset + 38] = (byte)159; + } + // Amount of required happiness for HAPPINESS_NIGHT evolutions. + if (rom[offset + 66] == (byte)219) { + rom[offset + 66] = (byte)159; + } + } + } + } + + @Override + public void removeTimeBasedEvolutions() { + for (Pokemon pkmn : pokes) { + if (pkmn != null) { + for (Evolution evol : pkmn.evolutionsFrom) { + // In Gen 3, only Eevee has a time-based evolution. + if (evol.type == EvolutionType.HAPPINESS_DAY) { + // Eevee: Make sun stone => Espeon + evol.type = EvolutionType.STONE; + evol.extraInfo = Gen3Items.sunStone; + addEvoUpdateStone(timeBasedEvolutionUpdates, evol, itemNames[evol.extraInfo]); + } else if (evol.type == EvolutionType.HAPPINESS_NIGHT) { + // Eevee: Make moon stone => Umbreon + evol.type = EvolutionType.STONE; + evol.extraInfo = Gen3Items.moonStone; + addEvoUpdateStone(timeBasedEvolutionUpdates, evol, itemNames[evol.extraInfo]); + } + } + } + } + } + + @Override + public boolean hasShopRandomization() { + return true; + } + + @Override + public Map<Integer, Shop> getShopItems() { + List<String> shopNames = Gen3Constants.getShopNames(romEntry.romType); + List<Integer> mainGameShops = Arrays.stream(romEntry.arrayEntries.get("MainGameShops")).boxed().collect(Collectors.toList()); + List<Integer> skipShops = Arrays.stream(romEntry.arrayEntries.get("SkipShops")).boxed().collect(Collectors.toList()); + Map<Integer, Shop> shopItemsMap = new TreeMap<>(); + int[] shopItemOffsets = romEntry.arrayEntries.get("ShopItemOffsets"); + for (int i = 0; i < shopItemOffsets.length; i++) { + if (!skipShops.contains(i)) { + int offset = shopItemOffsets[i]; + List<Integer> items = new ArrayList<>(); + int val = FileFunctions.read2ByteInt(rom, offset); + while (val != 0x0000) { + items.add(val); + offset += 2; + val = FileFunctions.read2ByteInt(rom, offset); + } + Shop shop = new Shop(); + shop.items = items; + shop.name = shopNames.get(i); + shop.isMainGame = mainGameShops.contains(i); + shopItemsMap.put(i, shop); + } + } + return shopItemsMap; + } + + @Override + public void setShopItems(Map<Integer, Shop> shopItems) { + int[] shopItemOffsets = romEntry.arrayEntries.get("ShopItemOffsets"); + for (int i = 0; i < shopItemOffsets.length; i++) { + Shop thisShop = shopItems.get(i); + if (thisShop != null && thisShop.items != null) { + int offset = shopItemOffsets[i]; + Iterator<Integer> iterItems = thisShop.items.iterator(); + while (iterItems.hasNext()) { + FileFunctions.write2ByteInt(rom, offset, iterItems.next()); + offset += 2; + } + } + } + } + + @Override + public void setShopPrices() { + int itemDataOffset = romEntry.getValue("ItemData"); + int entrySize = romEntry.getValue("ItemEntrySize"); + int itemCount = romEntry.getValue("ItemCount"); + for (int i = 1; i < itemCount; i++) { + int balancedPrice = Gen3Constants.balancedItemPrices.get(i) * 10; + int offset = itemDataOffset + (i * entrySize) + 16; + FileFunctions.write2ByteInt(rom, offset, balancedPrice); + } + } + + @Override + public List<PickupItem> getPickupItems() { + List<PickupItem> pickupItems = new ArrayList<>(); + int pickupItemCount = romEntry.getValue("PickupItemCount"); + int sizeOfPickupEntry = romEntry.romType == Gen3Constants.RomType_Em ? 2 : 4; + + // If we haven't found the pickup table for this ROM already, find it. + if (pickupItemsTableOffset == 0) { + String pickupTableStartLocator = romEntry.getString("PickupTableStartLocator"); + int offset = find(pickupTableStartLocator); + if (offset > 0) { + pickupItemsTableOffset = offset; + } + } + + // Assuming we've found the pickup table, extract the items out of it. + if (pickupItemsTableOffset > 0) { + for (int i = 0; i < pickupItemCount; i++) { + int itemOffset = pickupItemsTableOffset + (sizeOfPickupEntry * i); + int item = FileFunctions.read2ByteInt(rom, itemOffset); + PickupItem pickupItem = new PickupItem(item); + pickupItems.add(pickupItem); + } + } + + // Assuming we got the items from the last step, fill out the probabilities based on the game. + if (pickupItems.size() > 0) { + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + for (int levelRange = 0; levelRange < 10; levelRange++) { + pickupItems.get(0).probabilities[levelRange] = 30; + pickupItems.get(7).probabilities[levelRange] = 5; + pickupItems.get(8).probabilities[levelRange] = 4; + pickupItems.get(9).probabilities[levelRange] = 1; + for (int i = 1; i < 7; i++) { + pickupItems.get(i).probabilities[levelRange] = 10; + } + } + } else if (romEntry.romType == Gen3Constants.RomType_FRLG) { + for (int levelRange = 0; levelRange < 10; levelRange++) { + pickupItems.get(0).probabilities[levelRange] = 15; + for (int i = 1; i < 7; i++) { + pickupItems.get(i).probabilities[levelRange] = 10; + } + for (int i = 7; i < 11; i++) { + pickupItems.get(i).probabilities[levelRange] = 5; + } + for (int i = 11; i < 16; i++) { + pickupItems.get(i).probabilities[levelRange] = 1; + } + } + } else { + for (int levelRange = 0; levelRange < 10; levelRange++) { + int startingCommonItemOffset = levelRange; + int startingRareItemOffset = 18 + levelRange; + pickupItems.get(startingCommonItemOffset).probabilities[levelRange] = 30; + for (int i = 1; i < 7; i++) { + pickupItems.get(startingCommonItemOffset + i).probabilities[levelRange] = 10; + } + pickupItems.get(startingCommonItemOffset + 7).probabilities[levelRange] = 4; + pickupItems.get(startingCommonItemOffset + 8).probabilities[levelRange] = 4; + pickupItems.get(startingRareItemOffset).probabilities[levelRange] = 1; + pickupItems.get(startingRareItemOffset + 1).probabilities[levelRange] = 1; + } + } + } + return pickupItems; + } + + @Override + public void setPickupItems(List<PickupItem> pickupItems) { + int sizeOfPickupEntry = romEntry.romType == Gen3Constants.RomType_Em ? 2 : 4; + if (pickupItemsTableOffset > 0) { + for (int i = 0; i < pickupItems.size(); i++) { + int itemOffset = pickupItemsTableOffset + (sizeOfPickupEntry * i); + FileFunctions.write2ByteInt(rom, itemOffset, pickupItems.get(i).item); + } + } + } + + @Override + public boolean canChangeTrainerText() { + return true; + } + + @Override + public List<String> getTrainerNames() { + int baseOffset = romEntry.getValue("TrainerData"); + int amount = romEntry.getValue("TrainerCount"); + int entryLen = romEntry.getValue("TrainerEntrySize"); + List<String> theTrainers = new ArrayList<>(); + for (int i = 1; i < amount; i++) { + theTrainers.add(readVariableLengthString(baseOffset + i * entryLen + 4)); + } + return theTrainers; + } + + @Override + public void setTrainerNames(List<String> trainerNames) { + int baseOffset = romEntry.getValue("TrainerData"); + int amount = romEntry.getValue("TrainerCount"); + int entryLen = romEntry.getValue("TrainerEntrySize"); + int nameLen = romEntry.getValue("TrainerNameLength"); + Iterator<String> theTrainers = trainerNames.iterator(); + for (int i = 1; i < amount; i++) { + String newName = theTrainers.next(); + writeFixedLengthString(newName, baseOffset + i * entryLen + 4, nameLen); + } + + } + + @Override + public TrainerNameMode trainerNameMode() { + return TrainerNameMode.MAX_LENGTH; + } + + @Override + public List<Integer> getTCNameLengthsByTrainer() { + // not needed + return new ArrayList<>(); + } + + @Override + public int maxTrainerNameLength() { + return romEntry.getValue("TrainerNameLength") - 1; + } + + @Override + public List<String> getTrainerClassNames() { + int baseOffset = romEntry.getValue("TrainerClassNames"); + int amount = romEntry.getValue("TrainerClassCount"); + int length = romEntry.getValue("TrainerClassNameLength"); + List<String> trainerClasses = new ArrayList<>(); + for (int i = 0; i < amount; i++) { + trainerClasses.add(readVariableLengthString(baseOffset + i * length)); + } + return trainerClasses; + } + + @Override + public void setTrainerClassNames(List<String> trainerClassNames) { + int baseOffset = romEntry.getValue("TrainerClassNames"); + int amount = romEntry.getValue("TrainerClassCount"); + int length = romEntry.getValue("TrainerClassNameLength"); + Iterator<String> trainerClasses = trainerClassNames.iterator(); + for (int i = 0; i < amount; i++) { + writeFixedLengthString(trainerClasses.next(), baseOffset + i * length, length); + } + } + + @Override + public int maxTrainerClassNameLength() { + return romEntry.getValue("TrainerClassNameLength") - 1; + } + + @Override + public boolean fixedTrainerClassNamesLength() { + return false; + } + + @Override + public List<Integer> getDoublesTrainerClasses() { + int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses"); + List<Integer> doubles = new ArrayList<>(); + for (int tClass : doublesClasses) { + doubles.add(tClass); + } + return doubles; + } + + @Override + public boolean canChangeStaticPokemon() { + return (romEntry.getValue("StaticPokemonSupport") > 0); + } + + @Override + public boolean hasStaticAltFormes() { + return false; + } + + @Override + public boolean hasMainGameLegendaries() { + return romEntry.arrayEntries.get("MainGameLegendaries") != null; + } + + @Override + public List<Integer> getMainGameLegendaries() { + if (this.hasMainGameLegendaries()) { + return Arrays.stream(romEntry.arrayEntries.get("MainGameLegendaries")).boxed().collect(Collectors.toList()); + } + return new ArrayList<>(); + } + + @Override + public List<Integer> getSpecialMusicStatics() { + return Arrays.stream(romEntry.arrayEntries.get("SpecialMusicStatics")).boxed().collect(Collectors.toList()); + } + + @Override + public void applyCorrectStaticMusic(Map<Integer, Integer> specialMusicStaticChanges) { + List<Integer> replaced = new ArrayList<>(); + int newIndexToMusicPoolOffset; + + if (romEntry.codeTweaks.get("NewIndexToMusicTweak") != null) { + try { + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("NewIndexToMusicTweak")); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + newIndexToMusicPoolOffset = romEntry.getValue("NewIndexToMusicPoolOffset"); + + if (newIndexToMusicPoolOffset > 0) { + + for (int oldStatic: specialMusicStaticChanges.keySet()) { + int i = newIndexToMusicPoolOffset; + int index = internalToPokedex[readWord(rom, i)]; + while (index != oldStatic || replaced.contains(i)) { + i += 4; + index = internalToPokedex[readWord(rom, i)]; + } + writeWord(rom, i, pokedexToInternal[specialMusicStaticChanges.get(oldStatic)]); + replaced.add(i); + } + } + } + } + + @Override + public boolean hasStaticMusicFix() { + return romEntry.codeTweaks.get("NewIndexToMusicTweak") != null; + } + + @Override + public List<TotemPokemon> getTotemPokemon() { + return new ArrayList<>(); + } + + @Override + public void setTotemPokemon(List<TotemPokemon> totemPokemon) { + + } + + @Override + public String getDefaultExtension() { + return "gba"; + } + + @Override + public int abilitiesPerPokemon() { + return 2; + } + + @Override + public int highestAbilityIndex() { + return Gen3Constants.highestAbilityIndex; + } + + private void loadAbilityNames() { + int nameoffs = romEntry.getValue("AbilityNames"); + int namelen = romEntry.getValue("AbilityNameLength"); + abilityNames = new String[Gen3Constants.highestAbilityIndex + 1]; + for (int i = 0; i <= Gen3Constants.highestAbilityIndex; i++) { + abilityNames[i] = readFixedLengthString(nameoffs + namelen * i, namelen); + } + } + + @Override + public String abilityName(int number) { + return abilityNames[number]; + } + + @Override + public Map<Integer, List<Integer>> getAbilityVariations() { + return Gen3Constants.abilityVariations; + } + + @Override + public List<Integer> getUselessAbilities() { + return new ArrayList<>(Gen3Constants.uselessAbilities); + } + + @Override + public int getAbilityForTrainerPokemon(TrainerPokemon tp) { + // In Gen 3, Trainer Pokemon *always* use the first Ability, no matter what + return tp.pokemon.ability1; + } + + @Override + public boolean hasMegaEvolutions() { + return false; + } + + @Override + public int internalStringLength(String string) { + return translateString(string).length; + } + + @Override + public void randomizeIntroPokemon() { + // FRLG + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // intro sprites : first 255 only due to size + Pokemon introPk = randomPokemonLimited(255, false); + if (introPk == null) { + return; + } + int introPokemon = pokedexToInternal[introPk.number]; + int frontSprites = romEntry.getValue("FrontSprites"); + int palettes = romEntry.getValue("PokemonPalettes"); + + rom[romEntry.getValue("IntroCryOffset")] = (byte) introPokemon; + rom[romEntry.getValue("IntroOtherOffset")] = (byte) introPokemon; + + int spriteBase = romEntry.getValue("IntroSpriteOffset"); + writePointer(spriteBase, frontSprites + introPokemon * 8); + writePointer(spriteBase + 4, palettes + introPokemon * 8); + } else if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + // intro sprites : any pokemon in the range 0-510 except bulbasaur + int introPokemon = pokedexToInternal[randomPokemon().number]; + while (introPokemon == 1 || introPokemon > 510) { + introPokemon = pokedexToInternal[randomPokemon().number]; + } + int frontSprites = romEntry.getValue("PokemonFrontSprites"); + int palettes = romEntry.getValue("PokemonNormalPalettes"); + int cryCommand = romEntry.getValue("IntroCryOffset"); + int otherCommand = romEntry.getValue("IntroOtherOffset"); + + if (introPokemon > 255) { + rom[cryCommand] = (byte) 0xFF; + rom[cryCommand + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR0; + + rom[cryCommand + 2] = (byte) (introPokemon - 0xFF); + rom[cryCommand + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR0; + + rom[otherCommand] = (byte) 0xFF; + rom[otherCommand + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR4; + + rom[otherCommand + 2] = (byte) (introPokemon - 0xFF); + rom[otherCommand + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR4; + } else { + rom[cryCommand] = (byte) introPokemon; + rom[cryCommand + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR0; + + writeWord(cryCommand + 2, Gen3Constants.gbaNopOpcode); + + rom[otherCommand] = (byte) introPokemon; + rom[otherCommand + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR4; + + writeWord(otherCommand + 2, Gen3Constants.gbaNopOpcode); + } + + writePointer(romEntry.getValue("IntroSpriteOffset"), frontSprites + introPokemon * 8); + writePointer(romEntry.getValue("IntroPaletteOffset"), palettes + introPokemon * 8); + } else { + // Emerald, intro sprite: any Pokemon. + int introPokemon = pokedexToInternal[randomPokemon().number]; + writeWord(romEntry.getValue("IntroSpriteOffset"), introPokemon); + writeWord(romEntry.getValue("IntroCryOffset"), introPokemon); + } + + } + + private Pokemon randomPokemonLimited(int maxValue, boolean blockNonMales) { + checkPokemonRestrictions(); + List<Pokemon> validPokemon = new ArrayList<>(); + for (Pokemon pk : this.mainPokemonList) { + if (pokedexToInternal[pk.number] <= maxValue && (!blockNonMales || pk.genderRatio <= 0xFD)) { + validPokemon.add(pk); + } + } + if (validPokemon.size() == 0) { + return null; + } else { + return validPokemon.get(random.nextInt(validPokemon.size())); + } + } + + private void determineMapBankSizes() { + int mbpsOffset = romEntry.getValue("MapHeaders"); + List<Integer> mapBankOffsets = new ArrayList<>(); + + int offset = mbpsOffset; + + // find map banks + while (true) { + boolean valid = true; + for (int mbOffset : mapBankOffsets) { + if (mbpsOffset < mbOffset && offset >= mbOffset) { + valid = false; + break; + } + } + if (!valid) { + break; + } + int newMBOffset = readPointer(offset); + if (newMBOffset < 0 || newMBOffset >= rom.length) { + break; + } + mapBankOffsets.add(newMBOffset); + offset += 4; + } + int bankCount = mapBankOffsets.size(); + int[] bankMapCounts = new int[bankCount]; + for (int bank = 0; bank < bankCount; bank++) { + int baseBankOffset = mapBankOffsets.get(bank); + int count = 0; + offset = baseBankOffset; + while (true) { + boolean valid = true; + for (int mbOffset : mapBankOffsets) { + if (baseBankOffset < mbOffset && offset >= mbOffset) { + valid = false; + break; + } + } + if (!valid) { + break; + } + if (baseBankOffset < mbpsOffset && offset >= mbpsOffset) { + break; + } + int newMapOffset = readPointer(offset); + if (newMapOffset < 0 || newMapOffset >= rom.length) { + break; + } + count++; + offset += 4; + } + bankMapCounts[bank] = count; + } + + romEntry.entries.put("MapBankCount", bankCount); + romEntry.arrayEntries.put("MapBankSizes", bankMapCounts); + } + + private void preprocessMaps() { + itemOffs = new ArrayList<>(); + int bankCount = romEntry.getValue("MapBankCount"); + int[] bankMapCounts = romEntry.arrayEntries.get("MapBankSizes"); + int itemBall = romEntry.getValue("ItemBallPic"); + mapNames = new String[bankCount][]; + int mbpsOffset = romEntry.getValue("MapHeaders"); + int mapLabels = romEntry.getValue("MapLabels"); + Map<Integer, String> mapLabelsM = new HashMap<>(); + for (int bank = 0; bank < bankCount; bank++) { + int bankOffset = readPointer(mbpsOffset + bank * 4); + mapNames[bank] = new String[bankMapCounts[bank]]; + for (int map = 0; map < bankMapCounts[bank]; map++) { + int mhOffset = readPointer(bankOffset + map * 4); + + // map name + int mapLabel = rom[mhOffset + 0x14] & 0xFF; + if (mapLabelsM.containsKey(mapLabel)) { + mapNames[bank][map] = mapLabelsM.get(mapLabel); + } else { + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + mapNames[bank][map] = readVariableLengthString(readPointer(mapLabels + + (mapLabel - Gen3Constants.frlgMapLabelsStart) * 4)); + } else { + mapNames[bank][map] = readVariableLengthString(readPointer(mapLabels + mapLabel * 8 + 4)); + } + mapLabelsM.put(mapLabel, mapNames[bank][map]); + } + + // events + int eventOffset = readPointer(mhOffset + 4); + if (eventOffset >= 0 && eventOffset < rom.length) { + + int pCount = rom[eventOffset] & 0xFF; + int spCount = rom[eventOffset + 3] & 0xFF; + + if (pCount > 0) { + int peopleOffset = readPointer(eventOffset + 4); + for (int p = 0; p < pCount; p++) { + int pSprite = rom[peopleOffset + p * 24 + 1]; + if (pSprite == itemBall && readPointer(peopleOffset + p * 24 + 16) >= 0) { + // Get script and look inside + int scriptOffset = readPointer(peopleOffset + p * 24 + 16); + if (rom[scriptOffset] == 0x1A && rom[scriptOffset + 1] == 0x00 + && (rom[scriptOffset + 2] & 0xFF) == 0x80 && rom[scriptOffset + 5] == 0x1A + && rom[scriptOffset + 6] == 0x01 && (rom[scriptOffset + 7] & 0xFF) == 0x80 + && rom[scriptOffset + 10] == 0x09 + && (rom[scriptOffset + 11] == 0x00 || rom[scriptOffset + 11] == 0x01)) { + // item ball script + itemOffs.add(scriptOffset + 3); + } + } + } + // TM Text? + for (TMOrMTTextEntry tte : romEntry.tmmtTexts) { + if (tte.mapBank == bank && tte.mapNumber == map) { + // process this one + int scriptOffset = readPointer(peopleOffset + (tte.personNum - 1) * 24 + 16); + if (scriptOffset >= 0) { + if (romEntry.romType == Gen3Constants.RomType_FRLG && tte.isMoveTutor + && (tte.number == 5 || (tte.number >= 8 && tte.number <= 11))) { + scriptOffset = readPointer(scriptOffset + 1); + } else if (romEntry.romType == Gen3Constants.RomType_FRLG && tte.isMoveTutor + && tte.number == 7) { + scriptOffset = readPointer(scriptOffset + 0x1F); + } + int lookAt = scriptOffset + tte.offsetInScript; + // make sure this actually looks like a text + // pointer + if (lookAt >= 0 && lookAt < rom.length - 2) { + if (rom[lookAt + 3] == 0x08 || rom[lookAt + 3] == 0x09) { + // okay, it passes the basic test + tte.actualOffset = lookAt; + } + } + } + } + } + } + + if (spCount > 0) { + int signpostsOffset = readPointer(eventOffset + 16); + for (int sp = 0; sp < spCount; sp++) { + int spType = rom[signpostsOffset + sp * 12 + 5]; + if (spType >= 5 && spType <= 7) { + // hidden item + int itemHere = readWord(signpostsOffset + sp * 12 + 8); + if (itemHere != 0) { + // itemid 0 is coins + itemOffs.add(signpostsOffset + sp * 12 + 8); + } + } + } + } + } + } + } + } + + @Override + public ItemList getAllowedItems() { + return allowedItems; + } + + @Override + public ItemList getNonBadItems() { + return nonBadItems; + } + + @Override + public List<Integer> getUniqueNoSellItems() { + return new ArrayList<>(); + } + + @Override + public List<Integer> getRegularShopItems() { + return Gen3Constants.regularShopItems; + } + + @Override + public List<Integer> getOPShopItems() { + return Gen3Constants.opShopItems; + } + + private void loadItemNames() { + int nameoffs = romEntry.getValue("ItemData"); + int structlen = romEntry.getValue("ItemEntrySize"); + int maxcount = romEntry.getValue("ItemCount"); + itemNames = new String[maxcount + 1]; + for (int i = 0; i <= maxcount; i++) { + itemNames[i] = readVariableLengthString(nameoffs + structlen * i); + } + } + + @Override + public String[] getItemNames() { + return itemNames; + } + + @Override + public List<Integer> getRequiredFieldTMs() { + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + return Gen3Constants.frlgRequiredFieldTMs; + } else if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + return Gen3Constants.rsRequiredFieldTMs; + } else { + // emerald has a few TMs from pickup + return Gen3Constants.eRequiredFieldTMs; + } + } + + @Override + public List<Integer> getCurrentFieldTMs() { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + List<Integer> fieldTMs = new ArrayList<>(); + + for (int offset : itemOffs) { + int itemHere = readWord(offset); + if (Gen3Constants.allowedItems.isTM(itemHere)) { + int thisTM = itemHere - Gen3Constants.tmItemOffset + 1; + // hack for repeat TMs + if (!fieldTMs.contains(thisTM)) { + fieldTMs.add(thisTM); + } + } + } + return fieldTMs; + } + + @Override + public void setFieldTMs(List<Integer> fieldTMs) { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + Iterator<Integer> iterTMs = fieldTMs.iterator(); + int[] givenTMs = new int[512]; + + for (int offset : itemOffs) { + int itemHere = readWord(offset); + if (Gen3Constants.allowedItems.isTM(itemHere)) { + // Cache replaced TMs to duplicate repeats + if (givenTMs[itemHere] != 0) { + rom[offset] = (byte) givenTMs[itemHere]; + } else { + // Replace this with a TM from the list + int tm = iterTMs.next(); + tm += Gen3Constants.tmItemOffset - 1; + givenTMs[itemHere] = tm; + writeWord(offset, tm); + } + } + } + } + + @Override + public List<Integer> getRegularFieldItems() { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + List<Integer> fieldItems = new ArrayList<>(); + + for (int offset : itemOffs) { + int itemHere = readWord(offset); + if (Gen3Constants.allowedItems.isAllowed(itemHere) && !(Gen3Constants.allowedItems.isTM(itemHere))) { + fieldItems.add(itemHere); + } + } + return fieldItems; + } + + @Override + public void setRegularFieldItems(List<Integer> items) { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + Iterator<Integer> iterItems = items.iterator(); + + for (int offset : itemOffs) { + int itemHere = readWord(offset); + if (Gen3Constants.allowedItems.isAllowed(itemHere) && !(Gen3Constants.allowedItems.isTM(itemHere))) { + // Replace it + writeWord(offset, iterItems.next()); + } + } + + } + + @Override + public List<IngameTrade> getIngameTrades() { + List<IngameTrade> trades = new ArrayList<>(); + + // info + int tableOffset = romEntry.getValue("TradeTableOffset"); + int tableSize = romEntry.getValue("TradeTableSize"); + int[] unused = romEntry.arrayEntries.get("TradesUnused"); + int unusedOffset = 0; + int entryLength = 60; + + for (int entry = 0; entry < tableSize; entry++) { + if (unusedOffset < unused.length && unused[unusedOffset] == entry) { + unusedOffset++; + continue; + } + IngameTrade trade = new IngameTrade(); + int entryOffset = tableOffset + entry * entryLength; + trade.nickname = readVariableLengthString(entryOffset); + trade.givenPokemon = pokesInternal[readWord(entryOffset + 12)]; + trade.ivs = new int[6]; + for (int i = 0; i < 6; i++) { + trade.ivs[i] = rom[entryOffset + 14 + i] & 0xFF; + } + trade.otId = readWord(entryOffset + 24); + trade.item = readWord(entryOffset + 40); + trade.otName = readVariableLengthString(entryOffset + 43); + trade.requestedPokemon = pokesInternal[readWord(entryOffset + 56)]; + trades.add(trade); + } + + return trades; + + } + + @Override + public void setIngameTrades(List<IngameTrade> trades) { + // info + int tableOffset = romEntry.getValue("TradeTableOffset"); + int tableSize = romEntry.getValue("TradeTableSize"); + int[] unused = romEntry.arrayEntries.get("TradesUnused"); + int unusedOffset = 0; + int entryLength = 60; + int tradeOffset = 0; + + for (int entry = 0; entry < tableSize; entry++) { + if (unusedOffset < unused.length && unused[unusedOffset] == entry) { + unusedOffset++; + continue; + } + IngameTrade trade = trades.get(tradeOffset++); + int entryOffset = tableOffset + entry * entryLength; + writeFixedLengthString(trade.nickname, entryOffset, 12); + writeWord(entryOffset + 12, pokedexToInternal[trade.givenPokemon.number]); + for (int i = 0; i < 6; i++) { + rom[entryOffset + 14 + i] = (byte) trade.ivs[i]; + } + writeWord(entryOffset + 24, trade.otId); + writeWord(entryOffset + 40, trade.item); + writeFixedLengthString(trade.otName, entryOffset + 43, 11); + writeWord(entryOffset + 56, pokedexToInternal[trade.requestedPokemon.number]); + } + } + + @Override + public boolean hasDVs() { + return false; + } + + @Override + public int generationOfPokemon() { + return 3; + } + + @Override + public void removeEvosForPokemonPool() { + List<Pokemon> pokemonIncluded = this.mainPokemonList; + Set<Evolution> keepEvos = new HashSet<>(); + for (Pokemon pk : pokes) { + if (pk != null) { + keepEvos.clear(); + for (Evolution evol : pk.evolutionsFrom) { + if (pokemonIncluded.contains(evol.from) && pokemonIncluded.contains(evol.to)) { + keepEvos.add(evol); + } else { + evol.to.evolutionsTo.remove(evol); + } + } + pk.evolutionsFrom.retainAll(keepEvos); + } + } + } + + @Override + public boolean supportsFourStartingMoves() { + return true; + } + + @Override + public List<Integer> getFieldMoves() { + // cut, fly, surf, strength, flash, + // dig, teleport, waterfall, + // rock smash, sweet scent + // not softboiled or milk drink + // dive and secret power in RSE only + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + return Gen3Constants.frlgFieldMoves; + } else { + return Gen3Constants.rseFieldMoves; + } + } + + @Override + public List<Integer> getEarlyRequiredHMMoves() { + // RSE: rock smash + // FRLG: cut + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + return Gen3Constants.frlgEarlyRequiredHMMoves; + } else { + return Gen3Constants.rseEarlyRequiredHMMoves; + } + } + + @Override + public int miscTweaksAvailable() { + int available = MiscTweak.LOWER_CASE_POKEMON_NAMES.getValue(); + available |= MiscTweak.NATIONAL_DEX_AT_START.getValue(); + available |= MiscTweak.UPDATE_TYPE_EFFECTIVENESS.getValue(); + if (romEntry.getValue("RunIndoorsTweakOffset") > 0) { + available |= MiscTweak.RUNNING_SHOES_INDOORS.getValue(); + } + if (romEntry.getValue("TextSpeedValuesOffset") > 0 || romEntry.codeTweaks.get("InstantTextTweak") != null) { + available |= MiscTweak.FASTEST_TEXT.getValue(); + } + if (romEntry.getValue("CatchingTutorialOpponentMonOffset") > 0 + || romEntry.getValue("CatchingTutorialPlayerMonOffset") > 0) { + available |= MiscTweak.RANDOMIZE_CATCHING_TUTORIAL.getValue(); + } + if (romEntry.getValue("PCPotionOffset") != 0) { + available |= MiscTweak.RANDOMIZE_PC_POTION.getValue(); + } + available |= MiscTweak.BAN_LUCKY_EGG.getValue(); + available |= MiscTweak.RUN_WITHOUT_RUNNING_SHOES.getValue(); + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + available |= MiscTweak.BALANCE_STATIC_LEVELS.getValue(); + } + return available; + } + + @Override + public void applyMiscTweak(MiscTweak tweak) { + if (tweak == MiscTweak.RUNNING_SHOES_INDOORS) { + applyRunningShoesIndoorsPatch(); + } else if (tweak == MiscTweak.FASTEST_TEXT) { + applyFastestTextPatch(); + } else if (tweak == MiscTweak.LOWER_CASE_POKEMON_NAMES) { + applyCamelCaseNames(); + } else if (tweak == MiscTweak.NATIONAL_DEX_AT_START) { + patchForNationalDex(); + } else if (tweak == MiscTweak.RANDOMIZE_CATCHING_TUTORIAL) { + randomizeCatchingTutorial(); + } else if (tweak == MiscTweak.BAN_LUCKY_EGG) { + allowedItems.banSingles(Gen3Items.luckyEgg); + nonBadItems.banSingles(Gen3Items.luckyEgg); + } else if (tweak == MiscTweak.RANDOMIZE_PC_POTION) { + randomizePCPotion(); + } else if (tweak == MiscTweak.RUN_WITHOUT_RUNNING_SHOES) { + applyRunWithoutRunningShoesPatch(); + } else if (tweak == MiscTweak.BALANCE_STATIC_LEVELS) { + int[] fossilLevelOffsets = romEntry.arrayEntries.get("FossilLevelOffsets"); + for (int fossilLevelOffset : fossilLevelOffsets) { + writeWord(rom, fossilLevelOffset, 30); + } + } else if (tweak == MiscTweak.UPDATE_TYPE_EFFECTIVENESS) { + updateTypeEffectiveness(); + } + } + + @Override + public boolean isEffectivenessUpdated() { + return effectivenessUpdated; + } + + private void randomizeCatchingTutorial() { + if (romEntry.getValue("CatchingTutorialOpponentMonOffset") > 0) { + int oppOffset = romEntry.getValue("CatchingTutorialOpponentMonOffset"); + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + Pokemon opponent = randomPokemonLimited(255, true); + if (opponent != null) { + + int oppValue = pokedexToInternal[opponent.number]; + rom[oppOffset] = (byte) oppValue; + rom[oppOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + } + } else { + Pokemon opponent = randomPokemonLimited(510, true); + if (opponent != null) { + int oppValue = pokedexToInternal[opponent.number]; + if (oppValue > 255) { + rom[oppOffset] = (byte) 0xFF; + rom[oppOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + + rom[oppOffset + 2] = (byte) (oppValue - 0xFF); + rom[oppOffset + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR1; + } else { + rom[oppOffset] = (byte) oppValue; + rom[oppOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + + writeWord(oppOffset + 2, Gen3Constants.gbaNopOpcode); + } + } + } + } + + if (romEntry.getValue("CatchingTutorialPlayerMonOffset") > 0) { + int playerOffset = romEntry.getValue("CatchingTutorialPlayerMonOffset"); + Pokemon playerMon = randomPokemonLimited(510, false); + if (playerMon != null) { + int plyValue = pokedexToInternal[playerMon.number]; + if (plyValue > 255) { + rom[playerOffset] = (byte) 0xFF; + rom[playerOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + + rom[playerOffset + 2] = (byte) (plyValue - 0xFF); + rom[playerOffset + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR1; + } else { + rom[playerOffset] = (byte) plyValue; + rom[playerOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + + writeWord(playerOffset + 2, Gen3Constants.gbaNopOpcode); + } + } + } + + } + + private void applyRunningShoesIndoorsPatch() { + if (romEntry.getValue("RunIndoorsTweakOffset") != 0) { + rom[romEntry.getValue("RunIndoorsTweakOffset")] = 0x00; + } + } + + private void applyFastestTextPatch() { + if(romEntry.codeTweaks.get("InstantTextTweak") != null) { + try { + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("InstantTextTweak")); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } else if (romEntry.getValue("TextSpeedValuesOffset") > 0) { + int tsvOffset = romEntry.getValue("TextSpeedValuesOffset"); + rom[tsvOffset] = 4; // slow = medium + rom[tsvOffset + 1] = 1; // medium = fast + rom[tsvOffset + 2] = 0; // fast = instant + } + } + + private void randomizePCPotion() { + if (romEntry.getValue("PCPotionOffset") != 0) { + writeWord(romEntry.getValue("PCPotionOffset"), this.getNonBadItems().randomNonTM(this.random)); + } + } + + private void applyRunWithoutRunningShoesPatch() { + String prefix = Gen3Constants.getRunningShoesCheckPrefix(romEntry.romType); + int offset = find(prefix); + if (offset != 0) { + // The prefix starts 0x12 bytes from what we want to patch because what comes + // between is region and revision dependent. To start running, the game checks: + // 1. That you're not underwater (RSE only) + // 2. That you're holding the B button + // 3. That the FLAG_SYS_B_DASH flag is set (aka, you've acquired Running Shoes) + // 4. That you're allowed to run in this location + // For #3, if the flag is unset, it jumps to a different part of the + // code to make you walk instead. This simply nops out this jump so the + // game stops caring about the FLAG_SYS_B_DASH flag entirely. + writeWord(offset + 0x12, 0); + } + } + + private void updateTypeEffectiveness() { + List<TypeRelationship> typeEffectivenessTable = readTypeEffectivenessTable(); + log("--Updating Type Effectiveness--"); + for (TypeRelationship relationship : typeEffectivenessTable) { + // Change Ghost 0.5x against Steel to Ghost 1x to Steel + if (relationship.attacker == Type.GHOST && relationship.defender == Type.STEEL) { + relationship.effectiveness = Effectiveness.NEUTRAL; + log("Replaced: Ghost not very effective vs Steel => Ghost neutral vs Steel"); + } + + // Change Dark 0.5x against Steel to Dark 1x to Steel + else if (relationship.attacker == Type.DARK && relationship.defender == Type.STEEL) { + relationship.effectiveness = Effectiveness.NEUTRAL; + log("Replaced: Dark not very effective vs Steel => Dark neutral vs Steel"); + } + } + logBlankLine(); + writeTypeEffectivenessTable(typeEffectivenessTable); + effectivenessUpdated = true; + } + + private List<TypeRelationship> readTypeEffectivenessTable() { + List<TypeRelationship> typeEffectivenessTable = new ArrayList<>(); + int currentOffset = romEntry.getValue("TypeEffectivenessOffset"); + int attackingType = rom[currentOffset]; + // 0xFE marks the end of the table *not* affected by Foresight, while 0xFF marks + // the actual end of the table. Since we don't care about Ghost immunities at all, + // just stop once we reach the Foresight section. + while (attackingType != (byte) 0xFE) { + int defendingType = rom[currentOffset + 1]; + int effectivenessInternal = rom[currentOffset + 2]; + Type attacking = Gen3Constants.typeTable[attackingType]; + Type defending = Gen3Constants.typeTable[defendingType]; + Effectiveness effectiveness = null; + switch (effectivenessInternal) { + case 20: + effectiveness = Effectiveness.DOUBLE; + break; + case 10: + effectiveness = Effectiveness.NEUTRAL; + break; + case 5: + effectiveness = Effectiveness.HALF; + break; + case 0: + effectiveness = Effectiveness.ZERO; + break; + } + if (effectiveness != null) { + TypeRelationship relationship = new TypeRelationship(attacking, defending, effectiveness); + typeEffectivenessTable.add(relationship); + } + currentOffset += 3; + attackingType = rom[currentOffset]; + } + return typeEffectivenessTable; + } + + private void writeTypeEffectivenessTable(List<TypeRelationship> typeEffectivenessTable) { + int currentOffset = romEntry.getValue("TypeEffectivenessOffset"); + for (TypeRelationship relationship : typeEffectivenessTable) { + rom[currentOffset] = Gen3Constants.typeToByte(relationship.attacker); + rom[currentOffset + 1] = Gen3Constants.typeToByte(relationship.defender); + byte effectivenessInternal = 0; + switch (relationship.effectiveness) { + case DOUBLE: + effectivenessInternal = 20; + break; + case NEUTRAL: + effectivenessInternal = 10; + break; + case HALF: + effectivenessInternal = 5; + break; + case ZERO: + effectivenessInternal = 0; + break; + } + rom[currentOffset + 2] = effectivenessInternal; + currentOffset += 3; + } + } + + @Override + public void enableGuaranteedPokemonCatching() { + int offset = find(rom, Gen3Constants.perfectOddsBranchLocator); + if (offset > 0) { + // In Cmd_handleballthrow, the middle of the function checks if the odds of catching a Pokemon + // is greater than 254; if it is, then the Pokemon is automatically caught. In ASM, this is + // represented by: + // cmp r6, #0xFE + // bls oddsLessThanOrEqualTo254 + // The below code just nops these two instructions so that we *always* act like our odds are 255, + // and Pokemon are automatically caught no matter what. + rom[offset] = 0x00; + rom[offset + 1] = 0x00; + rom[offset + 2] = 0x00; + rom[offset + 3] = 0x00; + } + } + + @Override + public boolean isRomValid() { + return romEntry.expectedCRC32 == actualCRC32; + } + + @Override + public BufferedImage getMascotImage() { + Pokemon mascotPk = randomPokemon(); + int mascotPokemon = pokedexToInternal[mascotPk.number]; + int frontSprites = romEntry.getValue("FrontSprites"); + int palettes = romEntry.getValue("PokemonPalettes"); + int fsOffset = readPointer(frontSprites + mascotPokemon * 8); + int palOffset = readPointer(palettes + mascotPokemon * 8); + + byte[] trueFrontSprite = DSDecmp.Decompress(rom, fsOffset); + byte[] truePalette = DSDecmp.Decompress(rom, palOffset); + + // Convert palette into RGB + int[] convPalette = new int[16]; + // Leave palette[0] as 00000000 for transparency + for (int i = 0; i < 15; i++) { + int palValue = readWord(truePalette, i * 2 + 2); + convPalette[i + 1] = GFXFunctions.conv16BitColorToARGB(palValue); + } + + // Make image, 4bpp + return GFXFunctions.drawTiledImage(trueFrontSprite, convPalette, 64, 64, 4); + } + + @Override + public List<Integer> getAllHeldItems() { + return Gen3Constants.allHeldItems; + } + + @Override + public boolean hasRivalFinalBattle() { + return romEntry.romType == Gen3Constants.RomType_FRLG; + } + + @Override + public List<Integer> getAllConsumableHeldItems() { + return Gen3Constants.consumableHeldItems; + } + + @Override + public List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, int[] pokeMoves) { + List<Integer> items = new ArrayList<>(); + items.addAll(Gen3Constants.generalPurposeConsumableItems); + if (!consumableOnly) { + items.addAll(Gen3Constants.generalPurposeItems); + } + for (int moveIdx : pokeMoves) { + Move move = moves.get(moveIdx); + if (move == null) { + continue; + } + if (GBConstants.physicalTypes.contains(move.type) && move.power > 0) { + items.add(Gen3Items.liechiBerry); + if (!consumableOnly) { + items.addAll(Gen3Constants.typeBoostingItems.get(move.type)); + items.add(Gen3Items.choiceBand); + } + } + if (!GBConstants.physicalTypes.contains(move.type) && move.power > 0) { + items.add(Gen3Items.petayaBerry); + if (!consumableOnly) { + items.addAll(Gen3Constants.typeBoostingItems.get(move.type)); + } + } + } + if (!consumableOnly) { + List<Integer> speciesItems = Gen3Constants.speciesBoostingItems.get(tp.pokemon.number); + if (speciesItems != null) { + for (int i = 0; i < 6; i++) { // Increase the likelihood of using species specific items. + items.addAll(speciesItems); + } + } + } + return items; + } +} |