package com.dabomstew.pkrandom.romhandlers; /*----------------------------------------------------------------------------*/ /*-- Gen6RomHandler.java - randomizer handler for X/Y/OR/AS. --*/ /*-- --*/ /*-- 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 . --*/ /*----------------------------------------------------------------------------*/ import com.dabomstew.pkrandom.*; import com.dabomstew.pkrandom.constants.*; import com.dabomstew.pkrandom.ctr.AMX; import com.dabomstew.pkrandom.ctr.GARCArchive; import com.dabomstew.pkrandom.ctr.Mini; import com.dabomstew.pkrandom.exceptions.RandomizerIOException; import com.dabomstew.pkrandom.pokemon.*; import pptxt.N3DSTxtHandler; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.*; import java.util.List; import java.util.stream.Collectors; public class Gen6RomHandler extends Abstract3DSRomHandler { public static class Factory extends RomHandler.Factory { @Override public Gen6RomHandler create(Random random, PrintStream logStream) { return new Gen6RomHandler(random, logStream); } public boolean isLoadable(String filename) { return detect3DSRomInner(getProductCodeFromFile(filename), getTitleIdFromFile(filename)); } } public Gen6RomHandler(Random random) { super(random, null); } public Gen6RomHandler(Random random, PrintStream logStream) { super(random, logStream); } private static class OffsetWithinEntry { private int entry; private int offset; } private static class RomFileEntry { public String path; public long[] expectedCRC32s; } private static class RomEntry { private String name; private String romCode; private String titleId; private String acronym; private int romType; private long[] expectedCodeCRC32s = new long[2]; private Map files = new HashMap<>(); private boolean staticPokemonSupport = true, copyStaticPokemon = true; private Map linkedStaticOffsets = new HashMap<>(); private Map strings = new HashMap<>(); private Map numbers = new HashMap<>(); private Map arrayEntries = new HashMap<>(); private Map offsetArrayEntries = new HashMap<>(); private int getInt(String key) { if (!numbers.containsKey(key)) { numbers.put(key, 0); } return numbers.get(key); } private String getString(String key) { if (!strings.containsKey(key)) { strings.put(key, ""); } return strings.get(key); } private String getFile(String key) { if (!files.containsKey(key)) { files.put(key, new RomFileEntry()); } return files.get(key).path; } } private static List roms; static { loadROMInfo(); } private static void loadROMInfo() { roms = new ArrayList<>(); RomEntry current = null; try { Scanner sc = new Scanner(FileFunctions.openConfig("gen6_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(); if (r[0].equals("Game")) { current.romCode = r[1]; } else if (r[0].equals("Type")) { if (r[1].equalsIgnoreCase("ORAS")) { current.romType = Gen6Constants.Type_ORAS; } else { current.romType = Gen6Constants.Type_XY; } } else if (r[0].equals("TitleId")) { current.titleId = r[1]; } else if (r[0].equals("Acronym")) { current.acronym = r[1]; } else if (r[0].equals("CopyFrom")) { for (RomEntry otherEntry : roms) { if (r[1].equalsIgnoreCase(otherEntry.romCode)) { // copy from here current.linkedStaticOffsets.putAll(otherEntry.linkedStaticOffsets); current.arrayEntries.putAll(otherEntry.arrayEntries); current.numbers.putAll(otherEntry.numbers); current.strings.putAll(otherEntry.strings); current.offsetArrayEntries.putAll(otherEntry.offsetArrayEntries); current.files.putAll(otherEntry.files); } } } else if (r[0].startsWith("File<")) { String key = r[0].split("<")[1].split(">")[0]; String[] values = r[1].substring(1, r[1].length() - 1).split(","); String path = values[0]; String crcString = values[1].trim() + ", " + values[2].trim(); String[] crcs = crcString.substring(1, crcString.length() - 1).split(","); RomFileEntry entry = new RomFileEntry(); entry.path = path.trim(); entry.expectedCRC32s = new long[2]; entry.expectedCRC32s[0] = parseRILong("0x" + crcs[0].trim()); entry.expectedCRC32s[1] = parseRILong("0x" + crcs[1].trim()); current.files.put(key, entry); } else if (r[0].equals("CodeCRC32")) { String[] values = r[1].substring(1, r[1].length() - 1).split(","); current.expectedCodeCRC32s[0] = parseRILong("0x" + values[0].trim()); current.expectedCodeCRC32s[1] = parseRILong("0x" + values[1].trim()); } else if (r[0].equals("LinkedStaticEncounterOffsets")) { String[] offsets = r[1].substring(1, r[1].length() - 1).split(","); for (int i = 0; i < offsets.length; i++) { String[] parts = offsets[i].split(":"); current.linkedStaticOffsets.put(Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim())); } } 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 if (r[0].endsWith("Offset") || r[0].endsWith("Count") || r[0].endsWith("Number")) { int offs = parseRIInt(r[1]); current.numbers.put(r[0], offs); } else { current.strings.put(r[0],r[1]); } } } } 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; } } // This ROM private Pokemon[] pokes; private Map formeMappings = new TreeMap<>(); private Map> absolutePokeNumByBaseForme; private Map dummyAbsolutePokeNums; private List pokemonList; private List pokemonListInclFormes; private List megaEvolutions; private Move[] moves; private RomEntry romEntry; private byte[] code; private List abilityNames; private boolean loadedWildMapNames; private Map wildMapNames; private int moveTutorMovesOffset; private List itemNames; private List shopNames; private int shopItemsOffset; private ItemList allowedItems, nonBadItems; private int pickupItemsTableOffset; private long actualCodeCRC32; private Map actualFileCRC32s; private GARCArchive pokeGarc, moveGarc, stringsGarc, storyTextGarc; @Override protected boolean detect3DSRom(String productCode, String titleId) { return detect3DSRomInner(productCode, titleId); } private static boolean detect3DSRomInner(String productCode, String titleId) { return entryFor(productCode, titleId) != null; } private static RomEntry entryFor(String productCode, String titleId) { if (productCode == null || titleId == null) { return null; } for (RomEntry re : roms) { if (productCode.equals(re.romCode) && titleId.equals(re.titleId)) { return re; } } return null; } @Override protected void loadedROM(String productCode, String titleId) { this.romEntry = entryFor(productCode, titleId); try { code = readCode(); } catch (IOException e) { throw new RandomizerIOException(e); } try { stringsGarc = readGARC(romEntry.getFile("TextStrings"),true); storyTextGarc = readGARC(romEntry.getFile("StoryText"), true); } catch (IOException e) { throw new RandomizerIOException(e); } loadPokemonStats(); loadMoves(); pokemonListInclFormes = Arrays.asList(pokes); pokemonList = Arrays.asList(Arrays.copyOfRange(pokes,0,Gen6Constants.pokemonCount + 1)); abilityNames = getStrings(false,romEntry.getInt("AbilityNamesTextOffset")); itemNames = getStrings(false,romEntry.getInt("ItemNamesTextOffset")); shopNames = Gen6Constants.getShopNames(romEntry.romType); loadedWildMapNames = false; if (romEntry.romType == Gen6Constants.Type_ORAS) { isORAS = true; } allowedItems = Gen6Constants.getAllowedItems(romEntry.romType).copy(); nonBadItems = Gen6Constants.getNonBadItems(romEntry.romType).copy(); try { computeCRC32sForRom(); } catch (IOException e) { throw new RandomizerIOException(e); } } private void loadPokemonStats() { try { pokeGarc = this.readGARC(romEntry.getFile("PokemonStats"),true); String[] pokeNames = readPokemonNames(); int formeCount = Gen6Constants.getFormeCount(romEntry.romType); pokes = new Pokemon[Gen6Constants.pokemonCount + formeCount + 1]; for (int i = 1; i <= Gen6Constants.pokemonCount; i++) { pokes[i] = new Pokemon(); pokes[i].number = i; loadBasicPokeStats(pokes[i],pokeGarc.files.get(i).get(0),formeMappings); pokes[i].name = pokeNames[i]; } absolutePokeNumByBaseForme = new HashMap<>(); dummyAbsolutePokeNums = new HashMap<>(); dummyAbsolutePokeNums.put(255,0); int i = Gen6Constants.pokemonCount + 1; int formNum = 1; int prevSpecies = 0; Map currentMap = new HashMap<>(); for (int k: formeMappings.keySet()) { pokes[i] = new Pokemon(); pokes[i].number = i; loadBasicPokeStats(pokes[i], pokeGarc.files.get(k).get(0),formeMappings); FormeInfo fi = formeMappings.get(k); pokes[i].name = pokeNames[fi.baseForme]; pokes[i].baseForme = pokes[fi.baseForme]; pokes[i].formeNumber = fi.formeNumber; pokes[i].formeSuffix = Gen6Constants.formeSuffixes.getOrDefault(k,""); if (fi.baseForme == prevSpecies) { formNum++; currentMap.put(formNum,i); } else { if (prevSpecies != 0) { absolutePokeNumByBaseForme.put(prevSpecies,currentMap); } prevSpecies = fi.baseForme; formNum = 1; currentMap = new HashMap<>(); currentMap.put(formNum,i); } i++; } if (prevSpecies != 0) { absolutePokeNumByBaseForme.put(prevSpecies,currentMap); } } catch (IOException e) { throw new RandomizerIOException(e); } populateEvolutions(); populateMegaEvolutions(); } private void loadBasicPokeStats(Pokemon pkmn, byte[] stats, Map altFormes) { pkmn.hp = stats[Gen6Constants.bsHPOffset] & 0xFF; pkmn.attack = stats[Gen6Constants.bsAttackOffset] & 0xFF; pkmn.defense = stats[Gen6Constants.bsDefenseOffset] & 0xFF; pkmn.speed = stats[Gen6Constants.bsSpeedOffset] & 0xFF; pkmn.spatk = stats[Gen6Constants.bsSpAtkOffset] & 0xFF; pkmn.spdef = stats[Gen6Constants.bsSpDefOffset] & 0xFF; // Type pkmn.primaryType = Gen6Constants.typeTable[stats[Gen6Constants.bsPrimaryTypeOffset] & 0xFF]; pkmn.secondaryType = Gen6Constants.typeTable[stats[Gen6Constants.bsSecondaryTypeOffset] & 0xFF]; // Only one type? if (pkmn.secondaryType == pkmn.primaryType) { pkmn.secondaryType = null; } pkmn.catchRate = stats[Gen6Constants.bsCatchRateOffset] & 0xFF; pkmn.growthCurve = ExpCurve.fromByte(stats[Gen6Constants.bsGrowthCurveOffset]); pkmn.ability1 = stats[Gen6Constants.bsAbility1Offset] & 0xFF; pkmn.ability2 = stats[Gen6Constants.bsAbility2Offset] & 0xFF; pkmn.ability3 = stats[Gen6Constants.bsAbility3Offset] & 0xFF; if (pkmn.ability1 == pkmn.ability2) { pkmn.ability2 = 0; } // Held Items? int item1 = FileFunctions.read2ByteInt(stats, Gen6Constants.bsCommonHeldItemOffset); int item2 = FileFunctions.read2ByteInt(stats, Gen6Constants.bsRareHeldItemOffset); if (item1 == item2) { // guaranteed pkmn.guaranteedHeldItem = item1; pkmn.commonHeldItem = 0; pkmn.rareHeldItem = 0; pkmn.darkGrassHeldItem = -1; } else { pkmn.guaranteedHeldItem = 0; pkmn.commonHeldItem = item1; pkmn.rareHeldItem = item2; pkmn.darkGrassHeldItem = -1; } int formeCount = stats[Gen6Constants.bsFormeCountOffset] & 0xFF; if (formeCount > 1) { if (!altFormes.keySet().contains(pkmn.number)) { int firstFormeOffset = FileFunctions.read2ByteInt(stats, Gen6Constants.bsFormeOffset); if (firstFormeOffset != 0) { for (int i = 1; i < formeCount; i++) { altFormes.put(firstFormeOffset + i - 1,new FormeInfo(pkmn.number,i,FileFunctions.read2ByteInt(stats,Gen6Constants.bsFormeSpriteOffset))); // Assumes that formes are in memory in the same order as their numbers if (Gen6Constants.actuallyCosmeticForms.contains(firstFormeOffset+i-1)) { if (pkmn.number != Species.pikachu && pkmn.number != Species.cherrim) { // No Pikachu/Cherrim pkmn.cosmeticForms += 1; } } } } else { if (pkmn.number != Species.arceus && pkmn.number != Species.genesect && pkmn.number != Species.xerneas) { // Reason for exclusions: // Arceus/Genesect: to avoid confusion // Xerneas: Should be handled automatically? pkmn.cosmeticForms = formeCount; } } } else { if (Gen6Constants.actuallyCosmeticForms.contains(pkmn.number)) { pkmn.actuallyCosmetic = true; } } } } private String[] readPokemonNames() { String[] pokeNames = new String[Gen6Constants.pokemonCount + 1]; List nameList = getStrings(false, romEntry.getInt("PokemonNamesTextOffset")); for (int i = 1; i <= Gen6Constants.pokemonCount; i++) { pokeNames[i] = nameList.get(i); } return pokeNames; } private void populateEvolutions() { for (Pokemon pkmn : pokes) { if (pkmn != null) { pkmn.evolutionsFrom.clear(); pkmn.evolutionsTo.clear(); } } // Read GARC try { GARCArchive evoGARC = readGARC(romEntry.getFile("PokemonEvolutions"),true); for (int i = 1; i <= Gen6Constants.pokemonCount + Gen6Constants.getFormeCount(romEntry.romType); i++) { Pokemon pk = pokes[i]; byte[] evoEntry = evoGARC.files.get(i).get(0); for (int evo = 0; evo < 8; evo++) { int method = readWord(evoEntry, evo * 6); int species = readWord(evoEntry, evo * 6 + 4); if (method >= 1 && method <= Gen6Constants.evolutionMethodCount && species >= 1) { EvolutionType et = EvolutionType.fromIndex(6, method); if (et.equals(EvolutionType.LEVEL_HIGH_BEAUTY)) continue; // Remove Feebas "split" evolution int extraInfo = readWord(evoEntry, evo * 6 + 2); Evolution evol = new Evolution(pk, pokes[species], true, et, extraInfo); if (!pk.evolutionsFrom.contains(evol)) { pk.evolutionsFrom.add(evol); if (!pk.actuallyCosmetic) pokes[species].evolutionsTo.add(evol); } } } // Nincada's Shedinja evo is hardcoded into the game's executable, so // if the Pokemon is Nincada, then let's put it as one of its evolutions if (pk.number == Species.nincada) { Pokemon shedinja = pokes[Species.shedinja]; Evolution evol = new Evolution(pk, shedinja, false, EvolutionType.LEVEL_IS_EXTRA, 20); pk.evolutionsFrom.add(evol); shedinja.evolutionsTo.add(evol); } // 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; } } } } } catch (IOException e) { throw new RandomizerIOException(e); } } private void populateMegaEvolutions() { for (Pokemon pkmn : pokes) { if (pkmn != null) { pkmn.megaEvolutionsFrom.clear(); pkmn.megaEvolutionsTo.clear(); } } // Read GARC try { megaEvolutions = new ArrayList<>(); GARCArchive megaEvoGARC = readGARC(romEntry.getFile("MegaEvolutions"),true); for (int i = 1; i <= Gen6Constants.pokemonCount; i++) { Pokemon pk = pokes[i]; byte[] megaEvoEntry = megaEvoGARC.files.get(i).get(0); for (int evo = 0; evo < 3; evo++) { int formNum = readWord(megaEvoEntry, evo * 8); int method = readWord(megaEvoEntry, evo * 8 + 2); if (method >= 1) { int argument = readWord(megaEvoEntry, evo * 8 + 4); int megaSpecies = absolutePokeNumByBaseForme .getOrDefault(pk.number,dummyAbsolutePokeNums) .getOrDefault(formNum,0); MegaEvolution megaEvo = new MegaEvolution(pk, pokes[megaSpecies], method, argument); if (!pk.megaEvolutionsFrom.contains(megaEvo)) { pk.megaEvolutionsFrom.add(megaEvo); pokes[megaSpecies].megaEvolutionsTo.add(megaEvo); } megaEvolutions.add(megaEvo); } } // split evos don't carry stats if (pk.megaEvolutionsFrom.size() > 1) { for (MegaEvolution e : pk.megaEvolutionsFrom) { e.carryStats = false; } } } } catch (IOException e) { throw new RandomizerIOException(e); } } private List getStrings(boolean isStoryText, int index) { GARCArchive baseGARC = isStoryText ? storyTextGarc : stringsGarc; return getStrings(baseGARC, index); } private List getStrings(GARCArchive textGARC, int index) { byte[] rawFile = textGARC.files.get(index).get(0); return new ArrayList<>(N3DSTxtHandler.readTexts(rawFile,true,romEntry.romType)); } private void setStrings(boolean isStoryText, int index, List strings) { GARCArchive baseGARC = isStoryText ? storyTextGarc : stringsGarc; setStrings(baseGARC, index, strings); } private void setStrings(GARCArchive textGARC, int index, List strings) { byte[] oldRawFile = textGARC.files.get(index).get(0); try { byte[] newRawFile = N3DSTxtHandler.saveEntry(oldRawFile, strings, romEntry.romType); textGARC.setFile(index, newRawFile); } catch (IOException e) { e.printStackTrace(); } } private void loadMoves() { try { moveGarc = this.readGARC(romEntry.getFile("MoveData"),true); int moveCount = Gen6Constants.getMoveCount(romEntry.romType); moves = new Move[moveCount + 1]; List moveNames = getStrings(false, romEntry.getInt("MoveNamesTextOffset")); for (int i = 1; i <= moveCount; i++) { byte[] moveData; if (romEntry.romType == Gen6Constants.Type_ORAS) { moveData = Mini.UnpackMini(moveGarc.files.get(0).get(0), "WD")[i]; } else { moveData = moveGarc.files.get(i).get(0); } moves[i] = new Move(); moves[i].name = moveNames.get(i); moves[i].number = i; moves[i].internalId = i; moves[i].hitratio = (moveData[4] & 0xFF); moves[i].power = moveData[3] & 0xFF; moves[i].pp = moveData[5] & 0xFF; moves[i].type = Gen6Constants.typeTable[moveData[0] & 0xFF]; moves[i].category = Gen6Constants.moveCategoryIndices[moveData[2] & 0xFF]; if (i == Moves.swift) { perfectAccuracy = (int)moves[i].hitratio; } if (GlobalConstants.normalMultihitMoves.contains(i)) { moves[i].hitCount = 19 / 6.0; } 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 } } } catch (IOException e) { throw new RandomizerIOException(e); } } @Override protected void savingROM() throws IOException { savePokemonStats(); saveMoves(); try { writeCode(code); writeGARC(romEntry.getFile("TextStrings"), stringsGarc); writeGARC(romEntry.getFile("StoryText"), storyTextGarc); } catch (IOException e) { throw new RandomizerIOException(e); } } @Override protected String getGameAcronym() { return romEntry.acronym; } @Override protected boolean isGameUpdateSupported(int version) { return version == romEntry.numbers.get("FullyUpdatedVersionNumber"); } @Override protected String getGameVersion() { List titleScreenText = getStrings(false, romEntry.getInt("TitleScreenTextOffset")); if (titleScreenText.size() > romEntry.getInt("UpdateStringOffset")) { return titleScreenText.get(romEntry.getInt("UpdateStringOffset")); } // This shouldn't be seen by users, but is correct assuming we accidentally show it to them. return "Unpatched"; } private void savePokemonStats() { int k = Gen6Constants.getBsSize(romEntry.romType); byte[] duplicateData = pokeGarc.files.get(Gen6Constants.pokemonCount + Gen6Constants.getFormeCount(romEntry.romType) + 1).get(0); for (int i = 1; i <= Gen6Constants.pokemonCount + Gen6Constants.getFormeCount(romEntry.romType); i++) { byte[] pokeData = pokeGarc.files.get(i).get(0); saveBasicPokeStats(pokes[i], pokeData); for (byte pokeDataByte : pokeData) { duplicateData[k] = pokeDataByte; k++; } } try { this.writeGARC(romEntry.getFile("PokemonStats"),pokeGarc); } catch (IOException e) { throw new RandomizerIOException(e); } writeEvolutions(); } private void saveBasicPokeStats(Pokemon pkmn, byte[] stats) { stats[Gen6Constants.bsHPOffset] = (byte) pkmn.hp; stats[Gen6Constants.bsAttackOffset] = (byte) pkmn.attack; stats[Gen6Constants.bsDefenseOffset] = (byte) pkmn.defense; stats[Gen6Constants.bsSpeedOffset] = (byte) pkmn.speed; stats[Gen6Constants.bsSpAtkOffset] = (byte) pkmn.spatk; stats[Gen6Constants.bsSpDefOffset] = (byte) pkmn.spdef; stats[Gen6Constants.bsPrimaryTypeOffset] = Gen6Constants.typeToByte(pkmn.primaryType); if (pkmn.secondaryType == null) { stats[Gen6Constants.bsSecondaryTypeOffset] = stats[Gen6Constants.bsPrimaryTypeOffset]; } else { stats[Gen6Constants.bsSecondaryTypeOffset] = Gen6Constants.typeToByte(pkmn.secondaryType); } stats[Gen6Constants.bsCatchRateOffset] = (byte) pkmn.catchRate; stats[Gen6Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte(); stats[Gen6Constants.bsAbility1Offset] = (byte) pkmn.ability1; stats[Gen6Constants.bsAbility2Offset] = pkmn.ability2 != 0 ? (byte) pkmn.ability2 : (byte) pkmn.ability1; stats[Gen6Constants.bsAbility3Offset] = (byte) pkmn.ability3; // Held items if (pkmn.guaranteedHeldItem > 0) { FileFunctions.write2ByteInt(stats, Gen6Constants.bsCommonHeldItemOffset, pkmn.guaranteedHeldItem); FileFunctions.write2ByteInt(stats, Gen6Constants.bsRareHeldItemOffset, pkmn.guaranteedHeldItem); FileFunctions.write2ByteInt(stats, Gen6Constants.bsDarkGrassHeldItemOffset, 0); } else { FileFunctions.write2ByteInt(stats, Gen6Constants.bsCommonHeldItemOffset, pkmn.commonHeldItem); FileFunctions.write2ByteInt(stats, Gen6Constants.bsRareHeldItemOffset, pkmn.rareHeldItem); FileFunctions.write2ByteInt(stats, Gen6Constants.bsDarkGrassHeldItemOffset, 0); } if (pkmn.fullName().equals("Meowstic")) { stats[Gen6Constants.bsGenderOffset] = 0; } else if (pkmn.fullName().equals("Meowstic-F")) { stats[Gen6Constants.bsGenderOffset] = (byte)0xFE; } } private void writeEvolutions() { try { GARCArchive evoGARC = readGARC(romEntry.getFile("PokemonEvolutions"),true); for (int i = 1; i <= Gen6Constants.pokemonCount + Gen6Constants.getFormeCount(romEntry.romType); i++) { byte[] evoEntry = evoGARC.files.get(i).get(0); Pokemon pk = pokes[i]; if (pk.number == Species.nincada) { writeShedinjaEvolution(); } else if (pk.number == Species.feebas && romEntry.romType == Gen6Constants.Type_ORAS) { recreateFeebasBeautyEvolution(); } int evosWritten = 0; for (Evolution evo : pk.evolutionsFrom) { writeWord(evoEntry, evosWritten * 6, evo.type.toIndex(6)); writeWord(evoEntry, evosWritten * 6 + 2, evo.extraInfo); writeWord(evoEntry, evosWritten * 6 + 4, evo.to.number); evosWritten++; if (evosWritten == 8) { break; } } while (evosWritten < 8) { writeWord(evoEntry, evosWritten * 6, 0); writeWord(evoEntry, evosWritten * 6 + 2, 0); writeWord(evoEntry, evosWritten * 6 + 4, 0); evosWritten++; } } writeGARC(romEntry.getFile("PokemonEvolutions"), evoGARC); } catch (IOException e) { throw new RandomizerIOException(e); } } private void writeShedinjaEvolution() throws IOException { Pokemon nincada = pokes[Species.nincada]; // When the "Limit Pokemon" setting is enabled and Gen 3 is disabled, or when // "Random Every Level" evolutions are selected, we end up clearing out Nincada's // vanilla evolutions. In that case, there's no point in even worrying about // Shedinja, so just return. if (nincada.evolutionsFrom.size() < 2) { return; } Pokemon primaryEvolution = nincada.evolutionsFrom.get(0).to; Pokemon extraEvolution = nincada.evolutionsFrom.get(1).to; // In the CRO that handles the evolution cutscene, there's a hardcoded check to // see if the Pokemon that just evolved is now a Ninjask after evolving. It // performs that check using the following instructions: // sub r0, r1, #0x100 // subs r0, r0, #0x23 // bne skipMakingShedinja // The below code tweaks these instructions to use the species ID of Nincada's // new primary evolution; that way, evolving Nincada will still produce an "extra" // Pokemon like in older generations. byte[] evolutionCRO = readFile(romEntry.getFile("Evolution")); int offset = find(evolutionCRO, Gen6Constants.ninjaskSpeciesPrefix); if (offset > 0) { offset += Gen6Constants.ninjaskSpeciesPrefix.length() / 2; // because it was a prefix int primaryEvoLower = primaryEvolution.number & 0x00FF; int primaryEvoUpper = (primaryEvolution.number & 0xFF00) >> 8; evolutionCRO[offset] = (byte) primaryEvoUpper; evolutionCRO[offset + 4] = (byte) primaryEvoLower; } // In the game's executable, there's a hardcoded value to indicate what "extra" // Pokemon to create. It produces a Shedinja using the following instruction: // mov r1, #0x124, where 0x124 = 292 in decimal, which is Shedinja's species ID. // We can't just blindly replace it, though, because certain constants (for example, // 0x125) cannot be moved without using the movw instruction. This works fine in // Citra, but crashes on real hardware. Instead, we have to annoyingly shift up a // big chunk of code to fill in a nop; we can then do a pc-relative load to a // constant in the new free space. offset = find(code, Gen6Constants.shedinjaSpeciesPrefix); if (offset > 0) { offset += Gen6Constants.shedinjaSpeciesPrefix.length() / 2; // because it was a prefix // Shift up everything below the last nop to make some room at the bottom of the function. for (int i = 80; i < 188; i++) { code[offset + i] = code[offset + i + 4]; } // For every bl that we shifted up, patch them so they're now pointing to the same place they // were before (without this, they will be pointing to 0x4 before where they're supposed to). List blOffsetsToPatch = Arrays.asList(80, 92, 104, 116, 128, 140, 152, 164, 176); for (int blOffsetToPatch : blOffsetsToPatch) { code[offset + blOffsetToPatch] += 1; } // Write Nincada's new extra evolution in the new free space. writeLong(code, offset + 188, extraEvolution.number); // Now write the pc-relative load over the original mov instruction. code[offset] = (byte) 0xB4; code[offset + 1] = 0x10; code[offset + 2] = (byte) 0x9F; code[offset + 3] = (byte) 0xE5; } // Now that we've handled the hardcoded Shedinja evolution, delete it so that // we do *not* handle it in WriteEvolutions nincada.evolutionsFrom.remove(1); extraEvolution.evolutionsTo.remove(0); writeFile(romEntry.getFile("Evolution"), evolutionCRO); } private void recreateFeebasBeautyEvolution() { Pokemon feebas = pokes[Species.feebas]; // When the "Limit Pokemon" setting is enabled, we clear out the evolutions of // everything *not* in the pool, which could include Feebas. In that case, // there's no point in even worrying about its evolutions, so just return. if (feebas.evolutionsFrom.size() == 0) { return; } Evolution prismScaleEvo = feebas.evolutionsFrom.get(0); Pokemon feebasEvolution = prismScaleEvo.to; int beautyNeededToEvolve = 170; Evolution beautyEvolution = new Evolution(feebas, feebasEvolution, true, EvolutionType.LEVEL_HIGH_BEAUTY, beautyNeededToEvolve); feebas.evolutionsFrom.add(beautyEvolution); feebasEvolution.evolutionsTo.add(beautyEvolution); } private void saveMoves() { int moveCount = Gen6Constants.getMoveCount(romEntry.romType); byte[][] miniArchive = new byte[0][0]; if (romEntry.romType == Gen6Constants.Type_ORAS) { miniArchive = Mini.UnpackMini(moveGarc.files.get(0).get(0), "WD"); } for (int i = 1; i <= moveCount; i++) { byte[] data; if (romEntry.romType == Gen6Constants.Type_ORAS) { data = miniArchive[i]; } else { data = moveGarc.files.get(i).get(0); } data[2] = Gen6Constants.moveCategoryToByte(moves[i].category); data[3] = (byte) moves[i].power; data[0] = Gen6Constants.typeToByte(moves[i].type); int hitratio = (int) Math.round(moves[i].hitratio); if (hitratio < 0) { hitratio = 0; } if (hitratio > 101) { hitratio = 100; } data[4] = (byte) hitratio; data[5] = (byte) moves[i].pp; } try { if (romEntry.romType == Gen6Constants.Type_ORAS) { moveGarc.setFile(0, Mini.PackMini(miniArchive, "WD")); } this.writeGARC(romEntry.getFile("MoveData"), moveGarc); } catch (IOException e) { throw new RandomizerIOException(e); } } private void patchFormeReversion() throws IOException { // Upon loading a save, all Mega Pokemon and all Primal Reversions // in the player's party are set back to their base forme. This // patches .code such that this reversion does not happen. String saveLoadFormeReversionPrefix = Gen6Constants.getSaveLoadFormeReversionPrefix(romEntry.romType); int offset = find(code, saveLoadFormeReversionPrefix); if (offset > 0) { offset += saveLoadFormeReversionPrefix.length() / 2; // because it was a prefix // The actual offset of the code we want to patch is 0x10 bytes from the end of // the prefix. We have to do this because these 0x10 bytes differ between the // base game and all game updates, so we cannot use them as part of our prefix. offset += 0x10; // Stubs the call to the function that checks for Primal Reversions and // Mega Pokemon code[offset] = 0x00; code[offset + 1] = 0x00; code[offset + 2] = 0x00; code[offset + 3] = 0x00; } // In ORAS, the game also has hardcoded checks to revert Primal Groudon and Primal Kyogre // immediately after catching them. if (romEntry.romType == Gen6Constants.Type_ORAS) { byte[] battleCRO = readFile(romEntry.getFile("Battle")); offset = find(battleCRO, Gen6Constants.afterBattleFormeReversionPrefix); if (offset > 0) { offset += Gen6Constants.afterBattleFormeReversionPrefix.length() / 2; // because it was a prefix // The game checks for Primal Kyogre and Primal Groudon by pc-relative loading 0x17E, // which is Kyogre's species ID. The call to pml::pokepara::CoreParam::ChangeFormNo // is used by other species which we probably don't want to break, so instead of // stubbing the call to the function, just break the hardcoded species ID check by // making the game pc-relative load a total nonsense ID. battleCRO[offset] = (byte) 0xFF; battleCRO[offset + 1] = (byte) 0xFF; writeFile(romEntry.getFile("Battle"), battleCRO); } } } @Override public List getPokemon() { return pokemonList; } @Override public List getPokemonInclFormes() { return pokemonListInclFormes; } @Override public List getAltFormes() { int formeCount = Gen6Constants.getFormeCount(romEntry.romType); return pokemonListInclFormes.subList(Gen6Constants.pokemonCount + 1, Gen6Constants.pokemonCount + formeCount + 1); } @Override public List getMegaEvolutions() { return megaEvolutions; } @Override public Pokemon getAltFormeOfPokemon(Pokemon pk, int forme) { int pokeNum = absolutePokeNumByBaseForme.getOrDefault(pk.number,dummyAbsolutePokeNums).getOrDefault(forme,0); return pokeNum != 0 ? pokes[pokeNum] : pk; } @Override public List getIrregularFormes() { return Gen6Constants.irregularFormes.stream().map(i -> pokes[i]).collect(Collectors.toList()); } @Override public boolean hasFunctionalFormes() { return true; } @Override public List getStarters() { List starters = new ArrayList<>(); try { byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); List starterIndices = Arrays.stream(romEntry.arrayEntries.get("StarterIndices")).boxed().collect(Collectors.toList()); // Gift Pokemon int count = Gen6Constants.getGiftPokemonCount(romEntry.romType); int size = Gen6Constants.getGiftPokemonSize(romEntry.romType); int offset = romEntry.getInt("GiftPokemonOffset"); for (int i = 0; i < count; i++) { if (!starterIndices.contains(i)) continue; StaticEncounter se = new StaticEncounter(); int species = FileFunctions.read2ByteInt(staticCRO,offset+i*size); Pokemon pokemon = pokes[species]; int forme = staticCRO[offset+i*size + 4]; if (forme > pokemon.cosmeticForms && forme != 30 && forme != 31) { int speciesWithForme = absolutePokeNumByBaseForme .getOrDefault(species, dummyAbsolutePokeNums) .getOrDefault(forme, 0); pokemon = pokes[speciesWithForme]; } se.pkmn = pokemon; se.forme = forme; se.level = staticCRO[offset+i*size + 5]; int heldItem = FileFunctions.readFullInt(staticCRO,offset+i*size + 12); if (heldItem < 0) { heldItem = 0; } se.heldItem = heldItem; starters.add(se); } } catch (IOException e) { throw new RandomizerIOException(e); } return starters.stream().map(pk -> pk.pkmn).collect(Collectors.toList()); } @Override public boolean setStarters(List newStarters) { try { byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); byte[] displayCRO = readFile(romEntry.getFile("StarterDisplay")); List starterIndices = Arrays.stream(romEntry.arrayEntries.get("StarterIndices")).boxed().collect(Collectors.toList()); // Gift Pokemon int count = Gen6Constants.getGiftPokemonCount(romEntry.romType); int size = Gen6Constants.getGiftPokemonSize(romEntry.romType); int offset = romEntry.getInt("GiftPokemonOffset"); int displayOffset = readWord(displayCRO,romEntry.getInt("StarterOffsetOffset")) + romEntry.getInt("StarterExtraOffset"); Iterator starterIter = newStarters.iterator(); int displayIndex = 0; List starterText = getStrings(false,romEntry.getInt("StarterTextOffset")); int[] starterTextIndices = romEntry.arrayEntries.get("SpecificStarterTextOffsets"); for (int i = 0; i < count; i++) { if (!starterIndices.contains(i)) continue; StaticEncounter newStatic = new StaticEncounter(); Pokemon starter = starterIter.next(); if (starter.formeNumber > 0) { newStatic.forme = starter.formeNumber; starter = starter.baseForme; } newStatic.pkmn = starter; if (starter.cosmeticForms > 0) { newStatic.forme = this.random.nextInt(starter.cosmeticForms); } writeWord(staticCRO,offset+i*size,newStatic.pkmn.number); staticCRO[offset+i*size + 4] = (byte)newStatic.forme; // staticCRO[offset+i*size + 5] = (byte)newStatic.level; if (newStatic.heldItem == 0) { writeWord(staticCRO,offset+i*size + 12,-1); } else { writeWord(staticCRO,offset+i*size + 12,newStatic.heldItem); } writeWord(displayCRO,displayOffset+displayIndex*0x54,newStatic.pkmn.number); displayCRO[displayOffset+displayIndex*0x54+2] = (byte)newStatic.forme; if (displayIndex < 3) { starterText.set(starterTextIndices[displayIndex], "[VAR PKNAME(0000)]"); } displayIndex++; } writeFile(romEntry.getFile("StaticPokemon"),staticCRO); writeFile(romEntry.getFile("StarterDisplay"),displayCRO); setStrings(false, romEntry.getInt("StarterTextOffset"), starterText); } catch (IOException e) { throw new RandomizerIOException(e); } return true; } @Override public boolean hasStarterAltFormes() { return true; } @Override public int starterCount() { return romEntry.romType == Gen6Constants.Type_XY ? 6 : 12; } @Override public Map getUpdatedPokemonStats(int generation) { Map map = GlobalConstants.getStatChanges(generation); switch(generation) { case 7: map.put(781,new StatChange(Stat.SPDEF.val,105)); break; case 8: map.put(776,new StatChange(Stat.ATK.val | Stat.SPATK.val,140,140)); break; } return map; } @Override public List getStarterHeldItems() { // do nothing return new ArrayList<>(); } @Override public void setStarterHeldItems(List items) { // do nothing } @Override public List getMoves() { return Arrays.asList(moves); } @Override public List getEncounters(boolean useTimeOfDay) { if (!loadedWildMapNames) { loadWildMapNames(); } try { if (romEntry.romType == Gen6Constants.Type_ORAS) { return getEncountersORAS(); } else { return getEncountersXY(); } } catch (IOException e) { throw new RandomizerIOException(e); } } private List getEncountersXY() throws IOException { GARCArchive encounterGarc = readGARC(romEntry.getFile("WildPokemon"), false); List encounters = new ArrayList<>(); for (int i = 0; i < encounterGarc.files.size() - 1; i++) { byte[] b = encounterGarc.files.get(i).get(0); if (!wildMapNames.containsKey(i)) { wildMapNames.put(i, "? Unknown ?"); } String mapName = wildMapNames.get(i); int offset = FileFunctions.readFullInt(b, 0x10) + 0x10; int length = b.length - offset; if (length < 0x178) { // No encounters in this map continue; } byte[] encounterData = new byte[0x178]; System.arraycopy(b, offset, encounterData, 0, 0x178); // TODO: Is there some rate we can check like in older gens? // First, 12 grass encounters, 12 rough terrain encounters, and 12 encounters each for yellow/purple/red flowers EncounterSet grassEncounters = readEncounter(encounterData, 0, 12); if (grassEncounters.encounters.size() > 0) { grassEncounters.displayName = mapName + " Grass/Cave"; encounters.add(grassEncounters); } EncounterSet yellowFlowerEncounters = readEncounter(encounterData, 48, 12); if (yellowFlowerEncounters.encounters.size() > 0) { yellowFlowerEncounters.displayName = mapName + " Yellow Flowers"; encounters.add(yellowFlowerEncounters); } EncounterSet purpleFlowerEncounters = readEncounter(encounterData, 96, 12); if (purpleFlowerEncounters.encounters.size() > 0) { purpleFlowerEncounters.displayName = mapName + " Purple Flowers"; encounters.add(purpleFlowerEncounters); } EncounterSet redFlowerEncounters = readEncounter(encounterData, 144, 12); if (redFlowerEncounters.encounters.size() > 0) { redFlowerEncounters.displayName = mapName + " Red Flowers"; encounters.add(redFlowerEncounters); } EncounterSet roughTerrainEncounters = readEncounter(encounterData, 192, 12); if (roughTerrainEncounters.encounters.size() > 0) { roughTerrainEncounters.displayName = mapName + " Rough Terrain/Tall Grass"; encounters.add(roughTerrainEncounters); } // 5 surf and 5 rock smash encounters EncounterSet surfEncounters = readEncounter(encounterData, 240, 5); if (surfEncounters.encounters.size() > 0) { surfEncounters.displayName = mapName + " Surf"; encounters.add(surfEncounters); } EncounterSet rockSmashEncounters = readEncounter(encounterData, 260, 5); if (rockSmashEncounters.encounters.size() > 0) { rockSmashEncounters.displayName = mapName + " Rock Smash"; encounters.add(rockSmashEncounters); } // 3 Encounters for each type of rod EncounterSet oldRodEncounters = readEncounter(encounterData, 280, 3); if (oldRodEncounters.encounters.size() > 0) { oldRodEncounters.displayName = mapName + " Old Rod"; encounters.add(oldRodEncounters); } EncounterSet goodRodEncounters = readEncounter(encounterData, 292, 3); if (goodRodEncounters.encounters.size() > 0) { goodRodEncounters.displayName = mapName + " Good Rod"; encounters.add(goodRodEncounters); } EncounterSet superRodEncounters = readEncounter(encounterData, 304, 3); if (superRodEncounters.encounters.size() > 0) { superRodEncounters.displayName = mapName + " Super Rod"; encounters.add(superRodEncounters); } // Lastly, 5 for each kind of Horde EncounterSet hordeCommonEncounters = readEncounter(encounterData, 316, 5); if (hordeCommonEncounters.encounters.size() > 0) { hordeCommonEncounters.displayName = mapName + " Common Horde"; encounters.add(hordeCommonEncounters); } EncounterSet hordeUncommonEncounters = readEncounter(encounterData, 336, 5); if (hordeUncommonEncounters.encounters.size() > 0) { hordeUncommonEncounters.displayName = mapName + " Uncommon Horde"; encounters.add(hordeUncommonEncounters); } EncounterSet hordeRareEncounters = readEncounter(encounterData, 356, 5); if (hordeRareEncounters.encounters.size() > 0) { hordeRareEncounters.displayName = mapName + " Rare Horde"; encounters.add(hordeRareEncounters); } } // The ceiling/flying/rustling bush encounters are hardcoded in the Field CRO byte[] fieldCRO = readFile(romEntry.getFile("Field")); String currentName = Gen6Constants.fallingEncounterNameMap.get(0); int startingOffsetOfCurrentName = 0; for (int i = 0; i < Gen6Constants.fallingEncounterCount; i++) { int offset = Gen6Constants.fallingEncounterOffset + i * Gen6Constants.fieldEncounterSize; EncounterSet fallingEncounter = readFieldEncounter(fieldCRO, offset); if (Gen6Constants.fallingEncounterNameMap.containsKey(i)) { currentName = Gen6Constants.fallingEncounterNameMap.get(i); startingOffsetOfCurrentName = i; } int encounterNumber = (i - startingOffsetOfCurrentName) + 1; fallingEncounter.displayName = currentName + " #" + encounterNumber; encounters.add(fallingEncounter); } currentName = Gen6Constants.rustlingBushEncounterNameMap.get(0); startingOffsetOfCurrentName = 0; for (int i = 0; i < Gen6Constants.rustlingBushEncounterCount; i++) { int offset = Gen6Constants.rustlingBushEncounterOffset + i * Gen6Constants.fieldEncounterSize; EncounterSet rustlingBushEncounter = readFieldEncounter(fieldCRO, offset); if (Gen6Constants.rustlingBushEncounterNameMap.containsKey(i)) { currentName = Gen6Constants.rustlingBushEncounterNameMap.get(i); startingOffsetOfCurrentName = i; } int encounterNumber = (i - startingOffsetOfCurrentName) + 1; rustlingBushEncounter.displayName = currentName + " #" + encounterNumber; encounters.add(rustlingBushEncounter); } return encounters; } private List getEncountersORAS() throws IOException { GARCArchive encounterGarc = readGARC(romEntry.getFile("WildPokemon"), false); List encounters = new ArrayList<>(); for (int i = 0; i < encounterGarc.files.size() - 2; i++) { byte[] b = encounterGarc.files.get(i).get(0); if (!wildMapNames.containsKey(i)) { wildMapNames.put(i, "? Unknown ?"); } String mapName = wildMapNames.get(i); int offset = FileFunctions.readFullInt(b, 0x10) + 0xE; int offset2 = FileFunctions.readFullInt(b, 0x14); int length = offset2 - offset; if (length < 0xF6) { // No encounters in this map continue; } byte[] encounterData = new byte[0xF6]; System.arraycopy(b, offset, encounterData, 0, 0xF6); // First, read 12 grass encounters and 12 long grass encounters EncounterSet grassEncounters = readEncounter(encounterData, 0, 12); if (grassEncounters.encounters.size() > 0) { grassEncounters.displayName = mapName + " Grass/Cave"; grassEncounters.offset = i; encounters.add(grassEncounters); } EncounterSet longGrassEncounters = readEncounter(encounterData, 48, 12); if (longGrassEncounters.encounters.size() > 0) { longGrassEncounters.displayName = mapName + " Long Grass"; longGrassEncounters.offset = i; encounters.add(longGrassEncounters); } // Now, 3 DexNav Foreign encounters EncounterSet dexNavForeignEncounters = readEncounter(encounterData, 96, 3); if (dexNavForeignEncounters.encounters.size() > 0) { dexNavForeignEncounters.displayName = mapName + " DexNav Foreign Encounter"; dexNavForeignEncounters.offset = i; encounters.add(dexNavForeignEncounters); } // 5 surf and 5 rock smash encounters EncounterSet surfEncounters = readEncounter(encounterData, 108, 5); if (surfEncounters.encounters.size() > 0) { surfEncounters.displayName = mapName + " Surf"; surfEncounters.offset = i; encounters.add(surfEncounters); } EncounterSet rockSmashEncounters = readEncounter(encounterData, 128, 5); if (rockSmashEncounters.encounters.size() > 0) { rockSmashEncounters.displayName = mapName + " Rock Smash"; rockSmashEncounters.offset = i; encounters.add(rockSmashEncounters); } // 3 Encounters for each type of rod EncounterSet oldRodEncounters = readEncounter(encounterData, 148, 3); if (oldRodEncounters.encounters.size() > 0) { oldRodEncounters.displayName = mapName + " Old Rod"; oldRodEncounters.offset = i; encounters.add(oldRodEncounters); } EncounterSet goodRodEncounters = readEncounter(encounterData, 160, 3); if (goodRodEncounters.encounters.size() > 0) { goodRodEncounters.displayName = mapName + " Good Rod"; goodRodEncounters.offset = i; encounters.add(goodRodEncounters); } EncounterSet superRodEncounters = readEncounter(encounterData, 172, 3); if (superRodEncounters.encounters.size() > 0) { superRodEncounters.displayName = mapName + " Super Rod"; superRodEncounters.offset = i; encounters.add(superRodEncounters); } // Lastly, 5 for each kind of Horde EncounterSet hordeCommonEncounters = readEncounter(encounterData, 184, 5); if (hordeCommonEncounters.encounters.size() > 0) { hordeCommonEncounters.displayName = mapName + " Common Horde"; hordeCommonEncounters.offset = i; encounters.add(hordeCommonEncounters); } EncounterSet hordeUncommonEncounters = readEncounter(encounterData, 204, 5); if (hordeUncommonEncounters.encounters.size() > 0) { hordeUncommonEncounters.displayName = mapName + " Uncommon Horde"; hordeUncommonEncounters.offset = i; encounters.add(hordeUncommonEncounters); } EncounterSet hordeRareEncounters = readEncounter(encounterData, 224, 5); if (hordeRareEncounters.encounters.size() > 0) { hordeRareEncounters.displayName = mapName + " Rare Horde"; hordeRareEncounters.offset = i; encounters.add(hordeRareEncounters); } } return encounters; } private EncounterSet readEncounter(byte[] data, int offset, int amount) { EncounterSet es = new EncounterSet(); es.rate = 1; for (int i = 0; i < amount; i++) { int species = readWord(data, offset + i * 4) & 0x7FF; int forme = readWord(data, offset + i * 4) >> 11; if (species != 0) { Encounter e = new Encounter(); Pokemon baseForme = pokes[species]; // If the forme is purely cosmetic, just use the base forme as the Pokemon // for this encounter (the cosmetic forme will be stored in the encounter). // Do the same for formes 30 and 31, because they actually aren't formes, but // rather act as indicators for what forme should appear when encountered: // 30 = Spawn the cosmetic forme specific to the user's region (Scatterbug line) // 31 = Spawn *any* cosmetic forme with equal probability (Unown Mirage Cave) if (forme <= baseForme.cosmeticForms || forme == 30 || forme == 31) { e.pokemon = pokes[species]; } else { int speciesWithForme = absolutePokeNumByBaseForme .getOrDefault(species, dummyAbsolutePokeNums) .getOrDefault(forme, 0); e.pokemon = pokes[speciesWithForme]; } e.formeNumber = forme; e.level = data[offset + 2 + i * 4]; e.maxLevel = data[offset + 3 + i * 4]; es.encounters.add(e); } } return es; } private EncounterSet readFieldEncounter(byte[] data, int offset) { EncounterSet es = new EncounterSet(); for (int i = 0; i < 7; i++) { int species = readWord(data, offset + 4 + i * 8); int level = data[offset + 8 + i * 8]; if (species != 0) { Encounter e = new Encounter(); e.pokemon = pokes[species]; e.formeNumber = 0; e.level = level; e.maxLevel = level; es.encounters.add(e); } } return es; } @Override public void setEncounters(boolean useTimeOfDay, List encountersList) { try { if (romEntry.romType == Gen6Constants.Type_ORAS) { setEncountersORAS(encountersList); } else { setEncountersXY(encountersList); } } catch (IOException ex) { throw new RandomizerIOException(ex); } } private void setEncountersXY(List encountersList) throws IOException { String encountersFile = romEntry.getFile("WildPokemon"); GARCArchive encounterGarc = readGARC(encountersFile, false); Iterator encounters = encountersList.iterator(); for (int i = 0; i < encounterGarc.files.size() - 1; i++) { byte[] b = encounterGarc.files.get(i).get(0); int offset = FileFunctions.readFullInt(b, 0x10) + 0x10; int length = b.length - offset; if (length < 0x178) { // No encounters in this map continue; } byte[] encounterData = new byte[0x178]; System.arraycopy(b, offset, encounterData, 0, 0x178); // First, 12 grass encounters, 12 rough terrain encounters, and 12 encounters each for yellow/purple/red flowers if (readEncounter(encounterData, 0, 12).encounters.size() > 0) { EncounterSet grass = encounters.next(); writeEncounter(encounterData, 0, grass.encounters); } if (readEncounter(encounterData, 48, 12).encounters.size() > 0) { EncounterSet yellowFlowers = encounters.next(); writeEncounter(encounterData, 48, yellowFlowers.encounters); } if (readEncounter(encounterData, 96, 12).encounters.size() > 0) { EncounterSet purpleFlowers = encounters.next(); writeEncounter(encounterData, 96, purpleFlowers.encounters); } if (readEncounter(encounterData, 144, 12).encounters.size() > 0) { EncounterSet redFlowers = encounters.next(); writeEncounter(encounterData, 144, redFlowers.encounters); } if (readEncounter(encounterData, 192, 12).encounters.size() > 0) { EncounterSet roughTerrain = encounters.next(); writeEncounter(encounterData, 192, roughTerrain.encounters); } // 5 surf and 5 rock smash encounters if (readEncounter(encounterData, 240, 5).encounters.size() > 0) { EncounterSet surf = encounters.next(); writeEncounter(encounterData, 240, surf.encounters); } if (readEncounter(encounterData, 260, 5).encounters.size() > 0) { EncounterSet rockSmash = encounters.next(); writeEncounter(encounterData, 260, rockSmash.encounters); } // 3 Encounters for each type of rod if (readEncounter(encounterData, 280, 3).encounters.size() > 0) { EncounterSet oldRod = encounters.next(); writeEncounter(encounterData, 280, oldRod.encounters); } if (readEncounter(encounterData, 292, 3).encounters.size() > 0) { EncounterSet goodRod = encounters.next(); writeEncounter(encounterData, 292, goodRod.encounters); } if (readEncounter(encounterData, 304, 3).encounters.size() > 0) { EncounterSet superRod = encounters.next(); writeEncounter(encounterData, 304, superRod.encounters); } // Lastly, 5 for each kind of Horde if (readEncounter(encounterData, 316, 5).encounters.size() > 0) { EncounterSet commonHorde = encounters.next(); writeEncounter(encounterData, 316, commonHorde.encounters); } if (readEncounter(encounterData, 336, 5).encounters.size() > 0) { EncounterSet uncommonHorde = encounters.next(); writeEncounter(encounterData, 336, uncommonHorde.encounters); } if (readEncounter(encounterData, 356, 5).encounters.size() > 0) { EncounterSet rareHorde = encounters.next(); writeEncounter(encounterData, 356, rareHorde.encounters); } // Write the encounter data back to the file System.arraycopy(encounterData, 0, b, offset, 0x178); } // Save writeGARC(encountersFile, encounterGarc); // Now write the encounters hardcoded in the Field CRO byte[] fieldCRO = readFile(romEntry.getFile("Field")); for (int i = 0; i < Gen6Constants.fallingEncounterCount; i++) { int offset = Gen6Constants.fallingEncounterOffset + i * Gen6Constants.fieldEncounterSize; EncounterSet fallingEncounter = encounters.next(); writeFieldEncounter(fieldCRO, offset, fallingEncounter.encounters); } for (int i = 0; i < Gen6Constants.rustlingBushEncounterCount; i++) { int offset = Gen6Constants.rustlingBushEncounterOffset + i * Gen6Constants.fieldEncounterSize; EncounterSet rustlingBushEncounter = encounters.next(); writeFieldEncounter(fieldCRO, offset, rustlingBushEncounter.encounters); } // Save writeFile(romEntry.getFile("Field"), fieldCRO); this.updatePokedexAreaDataXY(encounterGarc, fieldCRO); } private void setEncountersORAS(List encountersList) throws IOException { String encountersFile = romEntry.getFile("WildPokemon"); GARCArchive encounterGarc = readGARC(encountersFile, false); Iterator encounters = encountersList.iterator(); byte[] decStorage = encounterGarc.files.get(encounterGarc.files.size() - 1).get(0); for (int i = 0; i < encounterGarc.files.size() - 2; i++) { byte[] b = encounterGarc.files.get(i).get(0); int offset = FileFunctions.readFullInt(b, 0x10) + 0xE; int offset2 = FileFunctions.readFullInt(b, 0x14); int length = offset2 - offset; if (length < 0xF6) { // No encounters in this map continue; } byte[] encounterData = new byte[0xF6]; System.arraycopy(b, offset, encounterData, 0, 0xF6); // First, 12 grass encounters and 12 long grass encounters if (readEncounter(encounterData, 0, 12).encounters.size() > 0) { EncounterSet grass = encounters.next(); writeEncounter(encounterData, 0, grass.encounters); } if (readEncounter(encounterData, 48, 12).encounters.size() > 0) { EncounterSet longGrass = encounters.next(); writeEncounter(encounterData, 48, longGrass.encounters); } // Now, 3 DexNav Foreign encounters if (readEncounter(encounterData, 96, 3).encounters.size() > 0) { EncounterSet dexNav = encounters.next(); writeEncounter(encounterData, 96, dexNav.encounters); } // 5 surf and 5 rock smash encounters if (readEncounter(encounterData, 108, 5).encounters.size() > 0) { EncounterSet surf = encounters.next(); writeEncounter(encounterData, 108, surf.encounters); } if (readEncounter(encounterData, 128, 5).encounters.size() > 0) { EncounterSet rockSmash = encounters.next(); writeEncounter(encounterData, 128, rockSmash.encounters); } // 3 Encounters for each type of rod if (readEncounter(encounterData, 148, 3).encounters.size() > 0) { EncounterSet oldRod = encounters.next(); writeEncounter(encounterData, 148, oldRod.encounters); } if (readEncounter(encounterData, 160, 3).encounters.size() > 0) { EncounterSet goodRod = encounters.next(); writeEncounter(encounterData, 160, goodRod.encounters); } if (readEncounter(encounterData, 172, 3).encounters.size() > 0) { EncounterSet superRod = encounters.next(); writeEncounter(encounterData, 172, superRod.encounters); } // Lastly, 5 for each kind of Horde if (readEncounter(encounterData, 184, 5).encounters.size() > 0) { EncounterSet commonHorde = encounters.next(); writeEncounter(encounterData, 184, commonHorde.encounters); } if (readEncounter(encounterData, 204, 5).encounters.size() > 0) { EncounterSet uncommonHorde = encounters.next(); writeEncounter(encounterData, 204, uncommonHorde.encounters); } if (readEncounter(encounterData, 224, 5).encounters.size() > 0) { EncounterSet rareHorde = encounters.next(); writeEncounter(encounterData, 224, rareHorde.encounters); } // Write the encounter data back to the file System.arraycopy(encounterData, 0, b, offset, 0xF6); // Also write the encounter data to the decStorage file int decStorageOffset = FileFunctions.readFullInt(decStorage, (i + 1) * 4) + 0xE; System.arraycopy(encounterData, 0, decStorage, decStorageOffset, 0xF4); } // Save writeGARC(encountersFile, encounterGarc); this.updatePokedexAreaDataORAS(encounterGarc); } private void updatePokedexAreaDataXY(GARCArchive encounterGarc, byte[] fieldCRO) throws IOException { byte[] pokedexAreaData = new byte[(Gen6Constants.pokemonCount + 1) * Gen6Constants.perPokemonAreaDataLengthXY]; for (int i = 0; i < pokedexAreaData.length; i += Gen6Constants.perPokemonAreaDataLengthXY) { // This byte is 0x10 for *every* Pokemon. Why? No clue, but let's copy it. pokedexAreaData[i + 133] = 0x10; } int currentMapNum = 0; // Read all the "normal" encounters in the encounters GARC. for (int i = 0; i < encounterGarc.files.size() - 1; i++) { byte[] b = encounterGarc.files.get(i).get(0); int offset = FileFunctions.readFullInt(b, 0x10) + 0x10; int length = b.length - offset; if (length < 0x178) { // No encounters in this map continue; } int areaIndex = Gen6Constants.xyMapNumToPokedexIndex[currentMapNum]; byte[] encounterData = new byte[0x178]; System.arraycopy(b, offset, encounterData, 0, 0x178); EncounterSet grassEncounters = readEncounter(encounterData, 0, 12); updatePokedexAreaDataFromEncounterSet(grassEncounters, pokedexAreaData, areaIndex, 0x1); EncounterSet yellowFlowerEncounters = readEncounter(encounterData, 48, 12); updatePokedexAreaDataFromEncounterSet(yellowFlowerEncounters, pokedexAreaData, areaIndex, 0x2); EncounterSet purpleFlowerEncounters = readEncounter(encounterData, 96, 12); updatePokedexAreaDataFromEncounterSet(purpleFlowerEncounters, pokedexAreaData, areaIndex, 0x4); EncounterSet redFlowerEncounters = readEncounter(encounterData, 144, 12); updatePokedexAreaDataFromEncounterSet(redFlowerEncounters, pokedexAreaData, areaIndex, 0x8); EncounterSet roughTerrainEncounters = readEncounter(encounterData, 192, 12); updatePokedexAreaDataFromEncounterSet(roughTerrainEncounters, pokedexAreaData, areaIndex, 0x10); EncounterSet surfEncounters = readEncounter(encounterData, 240, 5); updatePokedexAreaDataFromEncounterSet(surfEncounters, pokedexAreaData, areaIndex, 0x20); EncounterSet rockSmashEncounters = readEncounter(encounterData, 260, 5); updatePokedexAreaDataFromEncounterSet(rockSmashEncounters, pokedexAreaData, areaIndex, 0x40); EncounterSet oldRodEncounters = readEncounter(encounterData, 280, 3); updatePokedexAreaDataFromEncounterSet(oldRodEncounters, pokedexAreaData, areaIndex, 0x80); EncounterSet goodRodEncounters = readEncounter(encounterData, 292, 3); updatePokedexAreaDataFromEncounterSet(goodRodEncounters, pokedexAreaData, areaIndex, 0x100); EncounterSet superRodEncounters = readEncounter(encounterData, 304, 3); updatePokedexAreaDataFromEncounterSet(superRodEncounters, pokedexAreaData, areaIndex, 0x200); EncounterSet hordeCommonEncounters = readEncounter(encounterData, 316, 5); updatePokedexAreaDataFromEncounterSet(hordeCommonEncounters, pokedexAreaData, areaIndex, 0x400); EncounterSet hordeUncommonEncounters = readEncounter(encounterData, 336, 5); updatePokedexAreaDataFromEncounterSet(hordeUncommonEncounters, pokedexAreaData, areaIndex, 0x400); EncounterSet hordeRareEncounters = readEncounter(encounterData, 356, 5); updatePokedexAreaDataFromEncounterSet(hordeRareEncounters, pokedexAreaData, areaIndex, 0x400); currentMapNum++; } // Now read all the stuff that's hardcoded in the Field CRO for (int i = 0; i < Gen6Constants.fallingEncounterCount; i++) { int areaIndex = Gen6Constants.xyMapNumToPokedexIndex[currentMapNum]; int offset = Gen6Constants.fallingEncounterOffset + i * Gen6Constants.fieldEncounterSize; EncounterSet fallingEncounter = readFieldEncounter(fieldCRO, offset); updatePokedexAreaDataFromEncounterSet(fallingEncounter, pokedexAreaData, areaIndex, 0x800); currentMapNum++; } for (int i = 0; i < Gen6Constants.rustlingBushEncounterCount; i++) { int areaIndex = Gen6Constants.xyMapNumToPokedexIndex[currentMapNum]; int offset = Gen6Constants.rustlingBushEncounterOffset + i * Gen6Constants.fieldEncounterSize; EncounterSet rustlingBushEncounter = readFieldEncounter(fieldCRO, offset); updatePokedexAreaDataFromEncounterSet(rustlingBushEncounter, pokedexAreaData, areaIndex, 0x800); currentMapNum++; } // Write out the newly-created area data to the GARC GARCArchive pokedexAreaGarc = readGARC(romEntry.getFile("PokedexAreaData"), true); pokedexAreaGarc.setFile(0, pokedexAreaData); writeGARC(romEntry.getFile("PokedexAreaData"), pokedexAreaGarc); } private void updatePokedexAreaDataORAS(GARCArchive encounterGarc) throws IOException { byte[] pokedexAreaData = new byte[(Gen6Constants.pokemonCount + 1) * Gen6Constants.perPokemonAreaDataLengthORAS]; int currentMapNum = 0; for (int i = 0; i < encounterGarc.files.size() - 2; i++) { byte[] b = encounterGarc.files.get(i).get(0); int offset = FileFunctions.readFullInt(b, 0x10) + 0xE; int offset2 = FileFunctions.readFullInt(b, 0x14); int length = offset2 - offset; if (length < 0xF6) { // No encounters in this map continue; } int areaIndex = Gen6Constants.orasMapNumToPokedexIndex[currentMapNum]; if (areaIndex == -1) { // Current encounters are not taken into account for the Pokedex currentMapNum++; continue; } byte[] encounterData = new byte[0xF6]; System.arraycopy(b, offset, encounterData, 0, 0xF6); EncounterSet grassEncounters = readEncounter(encounterData, 0, 12); updatePokedexAreaDataFromEncounterSet(grassEncounters, pokedexAreaData, areaIndex, 0x1); EncounterSet longGrassEncounters = readEncounter(encounterData, 48, 12); updatePokedexAreaDataFromEncounterSet(longGrassEncounters, pokedexAreaData, areaIndex, 0x2); int foreignEncounterType = grassEncounters.encounters.size() > 0 ? 0x04 : 0x08; EncounterSet dexNavForeignEncounters = readEncounter(encounterData, 96, 3); updatePokedexAreaDataFromEncounterSet(dexNavForeignEncounters, pokedexAreaData, areaIndex, foreignEncounterType); EncounterSet surfEncounters = readEncounter(encounterData, 108, 5); updatePokedexAreaDataFromEncounterSet(surfEncounters, pokedexAreaData, areaIndex, 0x10); EncounterSet rockSmashEncounters = readEncounter(encounterData, 128, 5); updatePokedexAreaDataFromEncounterSet(rockSmashEncounters, pokedexAreaData, areaIndex, 0x20); EncounterSet oldRodEncounters = readEncounter(encounterData, 148, 3); updatePokedexAreaDataFromEncounterSet(oldRodEncounters, pokedexAreaData, areaIndex, 0x40); EncounterSet goodRodEncounters = readEncounter(encounterData, 160, 3); updatePokedexAreaDataFromEncounterSet(goodRodEncounters, pokedexAreaData, areaIndex, 0x80); EncounterSet superRodEncounters = readEncounter(encounterData, 172, 3); updatePokedexAreaDataFromEncounterSet(superRodEncounters, pokedexAreaData, areaIndex, 0x100); EncounterSet hordeCommonEncounters = readEncounter(encounterData, 184, 5); updatePokedexAreaDataFromEncounterSet(hordeCommonEncounters, pokedexAreaData, areaIndex, 0x200); EncounterSet hordeUncommonEncounters = readEncounter(encounterData, 204, 5); updatePokedexAreaDataFromEncounterSet(hordeUncommonEncounters, pokedexAreaData, areaIndex, 0x200); EncounterSet hordeRareEncounters = readEncounter(encounterData, 224, 5); updatePokedexAreaDataFromEncounterSet(hordeRareEncounters, pokedexAreaData, areaIndex, 0x200); currentMapNum++; } GARCArchive pokedexAreaGarc = readGARC(romEntry.getFile("PokedexAreaData"), true); pokedexAreaGarc.setFile(0, pokedexAreaData); writeGARC(romEntry.getFile("PokedexAreaData"), pokedexAreaGarc); } private void updatePokedexAreaDataFromEncounterSet(EncounterSet es, byte[] pokedexAreaData, int areaIndex, int encounterType) { for (Encounter enc : es.encounters) { Pokemon pkmn = enc.pokemon; int perPokemonAreaDataLength = romEntry.romType == Gen6Constants.Type_XY ? Gen6Constants.perPokemonAreaDataLengthXY : Gen6Constants.perPokemonAreaDataLengthORAS; int offset = pkmn.getBaseNumber() * perPokemonAreaDataLength + areaIndex * 4; int value = FileFunctions.readFullInt(pokedexAreaData, offset); value |= encounterType; FileFunctions.writeFullInt(pokedexAreaData, offset, value); } } private void writeEncounter(byte[] data, int offset, List encounters) { for (int i = 0; i < encounters.size(); i++) { Encounter encounter = encounters.get(i); int speciesAndFormeData = (encounter.formeNumber << 11) + encounter.pokemon.getBaseNumber(); writeWord(data, offset + i * 4, speciesAndFormeData); data[offset + 2 + i * 4] = (byte) encounter.level; data[offset + 3 + i * 4] = (byte) encounter.maxLevel; } } private void writeFieldEncounter(byte[] data, int offset, List encounters) { for (int i = 0; i < encounters.size(); i++) { Encounter encounter = encounters.get(i); writeWord(data, offset + 4 + i * 8, encounter.pokemon.getBaseNumber()); data[offset + 8 + i * 8] = (byte) encounter.level; } } private void loadWildMapNames() { try { wildMapNames = new HashMap<>(); GARCArchive encounterGarc = this.readGARC(romEntry.getFile("WildPokemon"), false); int zoneDataOffset = romEntry.getInt("MapTableFileOffset"); byte[] zoneData = encounterGarc.files.get(zoneDataOffset).get(0); List allMapNames = getStrings(false, romEntry.getInt("MapNamesTextOffset")); for (int map = 0; map < zoneDataOffset; map++) { int indexNum = (map * 56) + 0x1C; int nameIndex1 = zoneData[indexNum] & 0xFF; int nameIndex2 = 0x100 * ((int) (zoneData[indexNum + 1]) & 1); String mapName = allMapNames.get(nameIndex1 + nameIndex2); wildMapNames.put(map, mapName); } loadedWildMapNames = true; } catch (IOException e) { throw new RandomizerIOException(e); } } @Override public List getTrainers() { List allTrainers = new ArrayList<>(); boolean isORAS = romEntry.romType == Gen6Constants.Type_ORAS; try { GARCArchive trainers = this.readGARC(romEntry.getFile("TrainerData"),true); GARCArchive trpokes = this.readGARC(romEntry.getFile("TrainerPokemon"),true); int trainernum = trainers.files.size(); List tclasses = this.getTrainerClassNames(); List tnames = this.getTrainerNames(); Map tnamesMap = new TreeMap<>(); for (int i = 0; i < tnames.size(); i++) { tnamesMap.put(i,tnames.get(i)); } for (int i = 1; i < trainernum; i++) { // Trainer entries are 20 bytes in X/Y, 24 bytes in ORAS // Team flags; 1 byte; 0x01 = custom moves, 0x02 = held item // [ORAS only] 1 byte unused // Class; 1 byte // [ORAS only] 1 byte unknown // [ORAS only] 2 bytes unused // Battle Mode; 1 byte; 0=single, 1=double, 2=triple, 3=rotation, 4=??? // Number of pokemon in team; 1 byte // Items; 2 bytes each, 4 item slots // AI Flags; 2 byte // 3 bytes not used // Victory Money; 1 byte; The money given out after defeat = // 4 * this value * highest level poke in party // Victory Item; 2 bytes; The item given out after defeat. // In X/Y, these are berries, nuggets, pearls (e.g. Battle Chateau) // In ORAS, none of these are set. byte[] trainer = trainers.files.get(i).get(0); byte[] trpoke = trpokes.files.get(i).get(0); Trainer tr = new Trainer(); tr.poketype = isORAS ? readWord(trainer,0) : trainer[0] & 0xFF; tr.offset = i; tr.trainerclass = isORAS ? readWord(trainer,2) : trainer[1] & 0xFF; int offset = isORAS ? 6 : 2; int battleType = trainer[offset] & 0xFF; int numPokes = trainer[offset+1] & 0xFF; boolean healer = trainer[offset+13] != 0; int pokeOffs = 0; String trainerClass = tclasses.get(tr.trainerclass); String trainerName = tnamesMap.getOrDefault(i - 1, "UNKNOWN"); tr.fullDisplayName = trainerClass + " " + trainerName; for (int poke = 0; poke < numPokes; poke++) { // Structure is // ST SB LV LV SP SP FRM FRM // (HI HI) // (M1 M1 M2 M2 M3 M3 M4 M4) // ST (strength) corresponds to the IVs of a trainer's pokemon. // In ORAS, this value is like previous gens, a number 0-255 // to represent 0 to 31 IVs. In the vanilla games, the top // leaders/champions have 29. // In X/Y, the bottom 5 bits are the IVs. It is unknown what // the top 3 bits correspond to, perhaps EV spread? // The second byte, SB = 0 0 Ab Ab 0 0 Fm Ml // Ab Ab = ability number, 0 for random // Fm = 1 for forced female // Ml = 1 for forced male // There's also a trainer flag to force gender, but // this allows fixed teams with mixed genders. int level = readWord(trpoke, pokeOffs + 2); int species = readWord(trpoke, pokeOffs + 4); int formnum = readWord(trpoke, pokeOffs + 6); TrainerPokemon tpk = new TrainerPokemon(); tpk.level = level; tpk.pokemon = pokes[species]; tpk.strength = trpoke[pokeOffs]; if (isORAS) { tpk.IVs = (tpk.strength * 31 / 255); } else { tpk.IVs = tpk.strength & 0x1F; } int abilityAndFlag = trpoke[pokeOffs + 1]; tpk.abilitySlot = (abilityAndFlag >>> 4) & 0xF; tpk.forcedGenderFlag = (abilityAndFlag & 0xF); tpk.forme = formnum; tpk.formeSuffix = Gen6Constants.getFormeSuffixByBaseForme(species,formnum); tpk.absolutePokeNumber = absolutePokeNumByBaseForme .getOrDefault(species,dummyAbsolutePokeNums) .getOrDefault(formnum,species); pokeOffs += 8; if (tr.pokemonHaveItems()) { tpk.heldItem = readWord(trpoke, pokeOffs); pokeOffs += 2; tpk.hasMegaStone = Gen6Constants.isMegaStone(tpk.heldItem); } if (tr.pokemonHaveCustomMoves()) { int attack1 = readWord(trpoke, pokeOffs); int attack2 = readWord(trpoke, pokeOffs + 2); int attack3 = readWord(trpoke, pokeOffs + 4); int attack4 = readWord(trpoke, pokeOffs + 6); tpk.move1 = attack1; tpk.move2 = attack2; tpk.move3 = attack3; tpk.move4 = attack4; pokeOffs += 8; } tr.pokemon.add(tpk); } allTrainers.add(tr); } if (romEntry.romType == Gen6Constants.Type_XY) { Gen6Constants.tagTrainersXY(allTrainers); Gen6Constants.setMultiBattleStatusXY(allTrainers); } else { Gen6Constants.tagTrainersORAS(allTrainers); Gen6Constants.setMultiBattleStatusORAS(allTrainers); } } catch (IOException ex) { throw new RandomizerIOException(ex); } return allTrainers; } @Override public List getMainPlaythroughTrainers() { return new ArrayList<>(); } @Override public List getEvolutionItems() { return Gen6Constants.evolutionItems; } @Override public void setTrainers(List trainerData, boolean doubleBattleMode) { Iterator allTrainers = trainerData.iterator(); boolean isORAS = romEntry.romType == Gen6Constants.Type_ORAS; try { GARCArchive trainers = this.readGARC(romEntry.getFile("TrainerData"),true); GARCArchive trpokes = this.readGARC(romEntry.getFile("TrainerPokemon"),true); // Get current movesets in case we need to reset them for certain // trainer mons. Map> movesets = this.getMovesLearnt(); int trainernum = trainers.files.size(); for (int i = 1; i < trainernum; i++) { byte[] trainer = trainers.files.get(i).get(0); Trainer tr = allTrainers.next(); // preserve original poketype for held item & moves int offset = 0; if (isORAS) { writeWord(trainer,0,tr.poketype); offset = 4; } else { trainer[0] = (byte) tr.poketype; } int numPokes = tr.pokemon.size(); trainer[offset+3] = (byte) numPokes; if (doubleBattleMode) { if (!tr.skipImportant()) { if (trainer[offset+2] == 0) { trainer[offset+2] = 1; trainer[offset+12] |= 0x80; // Flag that needs to be set for trainers not to attack their own pokes } } } int bytesNeeded = 8 * numPokes; if (tr.pokemonHaveCustomMoves()) { bytesNeeded += 8 * numPokes; } if (tr.pokemonHaveItems()) { bytesNeeded += 2 * numPokes; } byte[] trpoke = new byte[bytesNeeded]; int pokeOffs = 0; Iterator tpokes = tr.pokemon.iterator(); for (int poke = 0; poke < numPokes; poke++) { TrainerPokemon tp = tpokes.next(); byte abilityAndFlag = (byte)((tp.abilitySlot << 4) | tp.forcedGenderFlag); trpoke[pokeOffs] = (byte) tp.strength; trpoke[pokeOffs + 1] = abilityAndFlag; writeWord(trpoke, pokeOffs + 2, tp.level); writeWord(trpoke, pokeOffs + 4, tp.pokemon.number); writeWord(trpoke, pokeOffs + 6, tp.forme); pokeOffs += 8; if (tr.pokemonHaveItems()) { writeWord(trpoke, pokeOffs, tp.heldItem); pokeOffs += 2; } if (tr.pokemonHaveCustomMoves()) { if (tp.resetMoves) { int[] pokeMoves = RomFunctions.getMovesAtLevel(tp.absolutePokeNumber, movesets, tp.level); for (int m = 0; m < 4; m++) { writeWord(trpoke, pokeOffs + m * 2, pokeMoves[m]); } } else { writeWord(trpoke, pokeOffs, tp.move1); writeWord(trpoke, pokeOffs + 2, tp.move2); writeWord(trpoke, pokeOffs + 4, tp.move3); writeWord(trpoke, pokeOffs + 6, tp.move4); } pokeOffs += 8; } } trpokes.setFile(i,trpoke); } this.writeGARC(romEntry.getFile("TrainerData"), trainers); this.writeGARC(romEntry.getFile("TrainerPokemon"), trpokes); } catch (IOException ex) { throw new RandomizerIOException(ex); } } @Override public Map> getMovesLearnt() { Map> movesets = new TreeMap<>(); try { GARCArchive movesLearnt = this.readGARC(romEntry.getFile("PokemonMovesets"),true); int formeCount = Gen6Constants.getFormeCount(romEntry.romType); // int formeOffset = Gen5Constants.getFormeMovesetOffset(romEntry.romType); for (int i = 1; i <= Gen6Constants.pokemonCount + formeCount; i++) { Pokemon pkmn = pokes[i]; byte[] movedata; // if (i > Gen6Constants.pokemonCount) { // movedata = movesLearnt.files.get(i + formeOffset); // } else { // movedata = movesLearnt.files.get(i); // } movedata = movesLearnt.files.get(i).get(0); int moveDataLoc = 0; List learnt = new ArrayList<>(); while (readWord(movedata, moveDataLoc) != 0xFFFF || readWord(movedata, moveDataLoc + 2) != 0xFFFF) { int move = readWord(movedata, moveDataLoc); int level = readWord(movedata, moveDataLoc + 2); MoveLearnt ml = new MoveLearnt(); ml.level = level; ml.move = move; learnt.add(ml); moveDataLoc += 4; } movesets.put(pkmn.number, learnt); } } catch (IOException e) { throw new RandomizerIOException(e); } return movesets; } @Override public void setMovesLearnt(Map> movesets) { try { GARCArchive movesLearnt = readGARC(romEntry.getFile("PokemonMovesets"),true); int formeCount = Gen6Constants.getFormeCount(romEntry.romType); // int formeOffset = Gen6Constants.getFormeMovesetOffset(romEntry.romType); for (int i = 1; i <= Gen6Constants.pokemonCount + formeCount; i++) { Pokemon pkmn = pokes[i]; List learnt = movesets.get(pkmn.number); int sizeNeeded = learnt.size() * 4 + 4; byte[] moveset = new byte[sizeNeeded]; int j = 0; for (; j < learnt.size(); j++) { MoveLearnt ml = learnt.get(j); writeWord(moveset, j * 4, ml.move); writeWord(moveset, j * 4 + 2, ml.level); } writeWord(moveset, j * 4, 0xFFFF); writeWord(moveset, j * 4 + 2, 0xFFFF); // if (i > Gen5Constants.pokemonCount) { // movesLearnt.files.set(i + formeOffset, moveset); // } else { // movesLearnt.files.set(i, moveset); // } movesLearnt.setFile(i, moveset); } // Save this.writeGARC(romEntry.getFile("PokemonMovesets"), movesLearnt); } catch (IOException e) { throw new RandomizerIOException(e); } } @Override public boolean canChangeStaticPokemon() { return romEntry.staticPokemonSupport; } @Override public boolean hasStaticAltFormes() { return true; } @Override public List getStaticPokemon() { List statics = new ArrayList<>(); try { byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); // Static Pokemon int count = Gen6Constants.getStaticPokemonCount(romEntry.romType); int size = Gen6Constants.staticPokemonSize; int offset = romEntry.getInt("StaticPokemonOffset"); for (int i = 0; i < count; i++) { StaticEncounter se = new StaticEncounter(); int species = FileFunctions.read2ByteInt(staticCRO,offset+i*size); Pokemon pokemon = pokes[species]; int forme = staticCRO[offset+i*size + 2]; if (forme > pokemon.cosmeticForms && forme != 30 && forme != 31) { int speciesWithForme = absolutePokeNumByBaseForme .getOrDefault(species, dummyAbsolutePokeNums) .getOrDefault(forme, 0); pokemon = pokes[speciesWithForme]; } se.pkmn = pokemon; se.forme = forme; se.level = staticCRO[offset+i*size + 3]; short heldItem = (short)FileFunctions.read2ByteInt(staticCRO,offset+i*size + 4); if (heldItem < 0) { heldItem = 0; } se.heldItem = heldItem; statics.add(se); } List skipStarters = Arrays.stream(romEntry.arrayEntries.get("StarterIndices")).boxed().collect(Collectors.toList()); // Gift Pokemon count = Gen6Constants.getGiftPokemonCount(romEntry.romType); size = Gen6Constants.getGiftPokemonSize(romEntry.romType); offset = romEntry.getInt("GiftPokemonOffset"); for (int i = 0; i < count; i++) { if (skipStarters.contains(i)) continue; StaticEncounter se = new StaticEncounter(); int species = FileFunctions.read2ByteInt(staticCRO,offset+i*size); Pokemon pokemon = pokes[species]; int forme = staticCRO[offset+i*size + 4]; if (forme > pokemon.cosmeticForms && forme != 30 && forme != 31) { int speciesWithForme = absolutePokeNumByBaseForme .getOrDefault(species, dummyAbsolutePokeNums) .getOrDefault(forme, 0); pokemon = pokes[speciesWithForme]; } se.pkmn = pokemon; se.forme = forme; se.level = staticCRO[offset+i*size + 5]; int heldItem = FileFunctions.readFullInt(staticCRO,offset+i*size + 12); if (heldItem < 0) { heldItem = 0; } se.heldItem = heldItem; if (romEntry.romType == Gen6Constants.Type_ORAS) { int metLocation = FileFunctions.read2ByteInt(staticCRO, offset + i * size + 18); if (metLocation == 0xEA64) { se.isEgg = true; } } statics.add(se); } } catch (IOException e) { throw new RandomizerIOException(e); } consolidateLinkedEncounters(statics); return statics; } private void consolidateLinkedEncounters(List statics) { List encountersToRemove = new ArrayList<>(); for (Map.Entry entry : romEntry.linkedStaticOffsets.entrySet()) { StaticEncounter baseEncounter = statics.get(entry.getKey()); StaticEncounter linkedEncounter = statics.get(entry.getValue()); baseEncounter.linkedEncounters.add(linkedEncounter); encountersToRemove.add(linkedEncounter); } for (StaticEncounter encounter : encountersToRemove) { statics.remove(encounter); } } @Override public boolean setStaticPokemon(List staticPokemon) { // Static Pokemon try { byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); unlinkStaticEncounters(staticPokemon); Iterator staticIter = staticPokemon.iterator(); int staticCount = Gen6Constants.getStaticPokemonCount(romEntry.romType); int size = Gen6Constants.staticPokemonSize; int offset = romEntry.getInt("StaticPokemonOffset"); for (int i = 0; i < staticCount; i++) { StaticEncounter se = staticIter.next(); writeWord(staticCRO,offset+i*size,se.pkmn.number); staticCRO[offset+i*size + 2] = (byte)se.forme; staticCRO[offset+i*size + 3] = (byte)se.level; if (se.heldItem == 0) { writeWord(staticCRO,offset+i*size + 4,-1); } else { writeWord(staticCRO,offset+i*size + 4,se.heldItem); } } List skipStarters = Arrays.stream(romEntry.arrayEntries.get("StarterIndices")).boxed().collect(Collectors.toList()); // Gift Pokemon int giftCount = Gen6Constants.getGiftPokemonCount(romEntry.romType); size = Gen6Constants.getGiftPokemonSize(romEntry.romType); offset = romEntry.getInt("GiftPokemonOffset"); for (int i = 0; i < giftCount; i++) { if (skipStarters.contains(i)) continue; StaticEncounter se = staticIter.next(); writeWord(staticCRO,offset+i*size,se.pkmn.number); staticCRO[offset+i*size + 4] = (byte)se.forme; staticCRO[offset+i*size + 5] = (byte)se.level; if (se.heldItem == 0) { writeWord(staticCRO,offset+i*size + 12,-1); } else { writeWord(staticCRO,offset+i*size + 12,se.heldItem); } } writeFile(romEntry.getFile("StaticPokemon"),staticCRO); if (romEntry.romType == Gen6Constants.Type_XY) { int[] boxLegendaryOffsets = romEntry.arrayEntries.get("BoxLegendaryOffsets"); StaticEncounter boxLegendaryEncounter = staticPokemon.get(boxLegendaryOffsets[0]); fixBoxLegendariesXY(boxLegendaryEncounter.pkmn.number); setRoamersXY(staticPokemon); } else { StaticEncounter rayquazaEncounter = staticPokemon.get(romEntry.getInt("RayquazaEncounterNumber")); fixRayquazaORAS(rayquazaEncounter.pkmn.number); } return true; } catch (IOException e) { throw new RandomizerIOException(e); } } private void unlinkStaticEncounters(List statics) { List offsetsToInsert = new ArrayList<>(); for (Map.Entry entry : romEntry.linkedStaticOffsets.entrySet()) { offsetsToInsert.add(entry.getValue()); } Collections.sort(offsetsToInsert); for (Integer offsetToInsert : offsetsToInsert) { statics.add(offsetToInsert, new StaticEncounter()); } for (Map.Entry entry : romEntry.linkedStaticOffsets.entrySet()) { StaticEncounter baseEncounter = statics.get(entry.getKey()); statics.set(entry.getValue(), baseEncounter.linkedEncounters.get(0)); } } private void fixBoxLegendariesXY(int boxLegendarySpecies) throws IOException { // We need to edit the script file or otherwise the text will still say "Xerneas" or "Yveltal" GARCArchive encounterGarc = readGARC(romEntry.getFile("WildPokemon"), false); byte[] boxLegendaryRoomData = encounterGarc.getFile(Gen6Constants.boxLegendaryEncounterFileXY); AMX localScript = new AMX(boxLegendaryRoomData, 1); byte[] data = localScript.decData; int[] boxLegendaryScriptOffsets = romEntry.arrayEntries.get("BoxLegendaryScriptOffsets"); for (int i = 0; i < boxLegendaryScriptOffsets.length; i++) { FileFunctions.write2ByteInt(data, boxLegendaryScriptOffsets[i], boxLegendarySpecies); } byte[] modifiedScript = localScript.getBytes(); System.arraycopy(modifiedScript, 0, boxLegendaryRoomData, Gen6Constants.boxLegendaryLocalScriptOffsetXY, modifiedScript.length); encounterGarc.setFile(Gen6Constants.boxLegendaryEncounterFileXY, boxLegendaryRoomData); writeGARC(romEntry.getFile("WildPokemon"), encounterGarc); // We also need to edit DllField.cro so that the hardcoded checks for // Xerneas's/Yveltal's ID will instead be checks for our randomized species ID. byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); int functionOffset = find(staticCRO, Gen6Constants.boxLegendaryFunctionPrefixXY); if (functionOffset > 0) { functionOffset += Gen6Constants.boxLegendaryFunctionPrefixXY.length() / 2; // because it was a prefix // At multiple points in the function, the game calls pml::pokepara::CoreParam::GetMonNo // and compares the result to r8; every single one of these comparisons is followed by a // nop. However, the way in which the species ID is loaded into r8 differs depending on // the game. We'd prefer to write the same assembly for both games, and there's a trick // we can abuse to do so. Since the species ID is never used outside of this comparison, // we can feel free to mutate it however we please. The below code allows us to write any // arbitrary species ID and make the proper comparison like this: // sub r0, r0, (speciesLower x 0x100) // subs r0, r0, speciesUpper int speciesUpper = boxLegendarySpecies & 0x00FF; int speciesLower = (boxLegendarySpecies & 0xFF00) >> 8; for (int i = 0; i < Gen6Constants.boxLegendaryCodeOffsetsXY.length; i++) { int codeOffset = functionOffset + Gen6Constants.boxLegendaryCodeOffsetsXY[i]; staticCRO[codeOffset] = (byte) speciesLower; staticCRO[codeOffset + 1] = 0x0C; staticCRO[codeOffset + 2] = 0x40; staticCRO[codeOffset + 3] = (byte) 0xE2; staticCRO[codeOffset + 4] = (byte) speciesUpper; staticCRO[codeOffset + 5] = 0x00; staticCRO[codeOffset + 6] = 0x50; staticCRO[codeOffset + 7] = (byte) 0xE2; } } writeFile(romEntry.getFile("StaticPokemon"), staticCRO); } private void setRoamersXY(List staticPokemon) throws IOException { int[] roamingLegendaryOffsets = romEntry.arrayEntries.get("RoamingLegendaryOffsets"); StaticEncounter[] roamers = new StaticEncounter[roamingLegendaryOffsets.length]; for (int i = 0; i < roamers.length; i++) { roamers[i] = staticPokemon.get(roamingLegendaryOffsets[i]); } int roamerSpeciesOffset = find(code, Gen6Constants.xyRoamerSpeciesLocator); int freeSpaceOffset = find(code, Gen6Constants.xyRoamerFreeSpacePostfix); if (roamerSpeciesOffset > 0 && freeSpaceOffset > 0) { // In order to make this code work with all versions of XY, we had to find the *end* of our free space. // The beginning is five instructions back. freeSpaceOffset -= 20; // The unmodified code looks like this: // nop // bl FUN_0041b710 // nop // nop // b LAB_003b7d1c // We want to move both branches to the top so that we have 12 bytes of space to work with. // Start by moving "bl FUN_0041b710" up one instruction, making sure to adjust the branch accordingly. code[freeSpaceOffset] = (byte)(code[freeSpaceOffset + 4] + 1); code[freeSpaceOffset + 1] = code[freeSpaceOffset + 5]; code[freeSpaceOffset + 2] = code[freeSpaceOffset + 6]; code[freeSpaceOffset + 3] = code[freeSpaceOffset + 7]; // Now move "b LAB_003b7d1c" up three instructions, again adjusting the branch accordingly. code[freeSpaceOffset + 4] = (byte)(code[freeSpaceOffset + 16] + 3); code[freeSpaceOffset + 5] = code[freeSpaceOffset + 17]; code[freeSpaceOffset + 6] = code[freeSpaceOffset + 18]; code[freeSpaceOffset + 7] = code[freeSpaceOffset + 19]; // In the free space now opened up, write the three roamer species. for (int i = 0; i < roamers.length; i++) { int offset = freeSpaceOffset + 8 + (i * 4); int species = roamers[i].pkmn.getBaseNumber(); FileFunctions.writeFullInt(code, offset, species); } // To load the species ID, the game currently does "moveq r4, #0x90" for Articuno and similar // things for Zapdos and Moltres. Instead, just pc-relative load what we wrote before. The fact // that we change the conditional moveq to the unconditional pc-relative load only matters for // the case where the player's starter index is *not* 0, 1, or 2, but that can't happen outside // of save editing. for (int i = 0; i < roamers.length; i++) { int offset = roamerSpeciesOffset + (i * 12); code[offset] = (byte)(0xAC - (8 * i)); code[offset + 1] = 0x41; code[offset + 2] = (byte) 0x9F; code[offset + 3] = (byte) 0xE5; } } // The level of the roamer is set by a separate function in DllField. byte[] fieldCRO = readFile(romEntry.getFile("Field")); int levelOffset = find(fieldCRO, Gen6Constants.xyRoamerLevelPrefix); if (levelOffset > 0) { levelOffset += Gen6Constants.xyRoamerLevelPrefix.length() / 2; // because it was a prefix fieldCRO[levelOffset] = (byte) roamers[0].level; } writeFile(romEntry.getFile("Field"), fieldCRO); } private void fixRayquazaORAS(int rayquazaEncounterSpecies) throws IOException { // We need to edit the script file or otherwise the text will still say "Rayquaza" int rayquazaScriptFile = romEntry.getInt("RayquazaEncounterScriptNumber"); GARCArchive scriptGarc = readGARC(romEntry.getFile("Scripts"), true); AMX rayquazaAMX = new AMX(scriptGarc.files.get(rayquazaScriptFile).get(0)); byte[] data = rayquazaAMX.decData; for (int i = 0; i < Gen6Constants.rayquazaScriptOffsetsORAS.length; i++) { FileFunctions.write2ByteInt(data, Gen6Constants.rayquazaScriptOffsetsORAS[i], rayquazaEncounterSpecies); } scriptGarc.setFile(rayquazaScriptFile, rayquazaAMX.getBytes()); writeGARC(romEntry.getFile("Scripts"), scriptGarc); // We also need to edit DllField.cro so that the hardcoded checks for Rayquaza's species // ID will instead be checks for our randomized species ID. byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); int functionOffset = find(staticCRO, Gen6Constants.rayquazaFunctionPrefixORAS); if (functionOffset > 0) { functionOffset += Gen6Constants.rayquazaFunctionPrefixORAS.length() / 2; // because it was a prefix // Every Rayquaza check consists of "cmp r0, #0x180" followed by a nop. Replace // all three checks with a sub and subs instructions so that we can write any // random species ID. int speciesUpper = rayquazaEncounterSpecies & 0x00FF; int speciesLower = (rayquazaEncounterSpecies & 0xFF00) >> 8; for (int i = 0; i < Gen6Constants.rayquazaCodeOffsetsORAS.length; i++) { int codeOffset = functionOffset + Gen6Constants.rayquazaCodeOffsetsORAS[i]; staticCRO[codeOffset] = (byte) speciesLower; staticCRO[codeOffset + 1] = 0x0C; staticCRO[codeOffset + 2] = 0x40; staticCRO[codeOffset + 3] = (byte) 0xE2; staticCRO[codeOffset + 4] = (byte) speciesUpper; staticCRO[codeOffset + 5] = 0x00; staticCRO[codeOffset + 6] = 0x50; staticCRO[codeOffset + 7] = (byte) 0xE2; } } writeFile(romEntry.getFile("StaticPokemon"), staticCRO); } @Override public int miscTweaksAvailable() { int available = 0; available |= MiscTweak.FASTEST_TEXT.getValue(); available |= MiscTweak.BAN_LUCKY_EGG.getValue(); available |= MiscTweak.RETAIN_ALT_FORMES.getValue(); available |= MiscTweak.NATIONAL_DEX_AT_START.getValue(); return available; } @Override public void applyMiscTweak(MiscTweak tweak) { if (tweak == MiscTweak.FASTEST_TEXT) { applyFastestText(); } else if (tweak == MiscTweak.BAN_LUCKY_EGG) { allowedItems.banSingles(Items.luckyEgg); nonBadItems.banSingles(Items.luckyEgg); } else if (tweak == MiscTweak.RETAIN_ALT_FORMES) { try { patchFormeReversion(); } catch (IOException e) { e.printStackTrace(); } } else if (tweak == MiscTweak.NATIONAL_DEX_AT_START) { patchForNationalDex(); } } private void applyFastestText() { int offset = find(code, Gen6Constants.fastestTextPrefixes[0]); if (offset > 0) { offset += Gen6Constants.fastestTextPrefixes[0].length() / 2; // because it was a prefix code[offset] = 0x03; code[offset + 1] = 0x40; code[offset + 2] = (byte) 0xA0; code[offset + 3] = (byte) 0xE3; } offset = find(code, Gen6Constants.fastestTextPrefixes[1]); if (offset > 0) { offset += Gen6Constants.fastestTextPrefixes[1].length() / 2; // because it was a prefix code[offset] = 0x03; code[offset + 1] = 0x50; code[offset + 2] = (byte) 0xA0; code[offset + 3] = (byte) 0xE3; } } private void patchForNationalDex() { int offset = find(code, Gen6Constants.nationalDexFunctionLocator); if (offset > 0) { // In Savedata::ZukanData::GetZenkokuZukanFlag, we load a flag into r0 and // then AND it with 0x1 to get a boolean that determines if the player has // the National Dex. The below code patches this piece of code so that // instead of loading the flag, we simply "mov r0, #0x1". code[offset] = 0x01; code[offset + 1] = 0x00; code[offset + 2] = (byte) 0xA0; code[offset + 3] = (byte) 0xE3; } if (romEntry.romType == Gen6Constants.Type_XY) { offset = find(code, Gen6Constants.xyGetDexFlagFunctionLocator); if (offset > 0) { // In addition to the code listed above, XY also use a function that I'm // calling Savedata::ZukanData::GetDexFlag(int) to determine what Pokedexes // the player owns. It can be called with 0 (Central), 1 (Coastal), 2 (Mountain), // or 3 (National). Since the player *always* has the Central Dex, the code has // a short-circuit for it that looks like this: // cmp r5, #0x0 // moveq r0, #0x1 // beq returnFromFunction // The below code nops out that comparison and makes the move and branch instructions // non-conditional; no matter what's on the save file, the player will have all dexes. FileFunctions.writeFullInt(code, offset, 0); code[offset + 7] = (byte) 0xE3; code[offset + 11] = (byte) 0xEA; } } else { // DllSangoZukan.cro will refuse to let you open either the Hoenn or National Pokedex if you have // caught 0 Pokemon in the Hoenn Pokedex; it is unknown *how* it does this, though. Instead, let's // just hack up the function that determines how many Pokemon in the Hoenn Pokedex you've caught so // it returns 1 if you haven't caught anything. offset = find(code, Gen6Constants.orasGetHoennDexCaughtFunctionPrefix); if (offset > 0) { offset += Gen6Constants.orasGetHoennDexCaughtFunctionPrefix.length() / 2; // because it was a prefix // At the start of the function, there's a check that the Zukan block on the save data is valid; // this is obviously generated by either a macro or inlined function, since literally every function // relating to the Pokedex has this too. First, it checks if the checksum is correct then does a beq // to branch to the main body of the function; let's replace this with an unconditional branch. code[offset + 31] = (byte) 0xEA; // Now, in the space where the function would normally handle the call to the assert function // to crash the game if the checksum is invalid, we can write the following code: // mov r0, r7 // cmp r0, #0x0 // moveq r0, #0x1 // ldmia sp!,{r4 r5 r6 r7 r8 r9 r10 r11 r12 pc} FileFunctions.writeFullIntBigEndian(code, offset + 32, 0x0700A0E1); FileFunctions.writeFullIntBigEndian(code, offset + 36, 0x000050E3); FileFunctions.writeFullIntBigEndian(code, offset + 40, 0x0100A003); FileFunctions.writeFullIntBigEndian(code, offset + 44, 0xF09FBDE8); // At the end of the function, the game normally does "mov r0, r7" and then returns, where r7 // contains the number of Pokemon caught in the Hoenn Pokedex. Instead, branch to the code we // wrote above. FileFunctions.writeFullIntBigEndian(code, offset + 208, 0xD2FFFFEA); } } } @Override public List getTMMoves() { String tmDataPrefix = Gen6Constants.tmDataPrefix; int offset = find(code, tmDataPrefix); if (offset != 0) { offset += Gen6Constants.tmDataPrefix.length() / 2; // because it was a prefix List tms = new ArrayList<>(); for (int i = 0; i < Gen6Constants.tmBlockOneCount; i++) { tms.add(readWord(code, offset + i * 2)); } offset += (Gen6Constants.getTMBlockTwoStartingOffset(romEntry.romType) * 2); for (int i = 0; i < (Gen6Constants.tmCount - Gen6Constants.tmBlockOneCount); i++) { tms.add(readWord(code, offset + i * 2)); } return tms; } else { return null; } } @Override public List getHMMoves() { String tmDataPrefix = Gen6Constants.tmDataPrefix; int offset = find(code, tmDataPrefix); if (offset != 0) { offset += Gen6Constants.tmDataPrefix.length() / 2; // because it was a prefix offset += Gen6Constants.tmBlockOneCount * 2; // TM data List hms = new ArrayList<>(); for (int i = 0; i < Gen6Constants.hmBlockOneCount; i++) { hms.add(readWord(code, offset + i * 2)); } if (romEntry.romType == Gen6Constants.Type_ORAS) { hms.add(readWord(code, offset + Gen6Constants.rockSmashOffsetORAS)); hms.add(readWord(code, offset + Gen6Constants.diveOffsetORAS)); } return hms; } else { return null; } } @Override public void setTMMoves(List moveIndexes) { String tmDataPrefix = Gen6Constants.tmDataPrefix; int offset = find(code, tmDataPrefix); if (offset > 0) { offset += Gen6Constants.tmDataPrefix.length() / 2; // because it was a prefix for (int i = 0; i < Gen6Constants.tmBlockOneCount; i++) { writeWord(code, offset + i * 2, moveIndexes.get(i)); } offset += (Gen6Constants.getTMBlockTwoStartingOffset(romEntry.romType) * 2); for (int i = 0; i < (Gen6Constants.tmCount - Gen6Constants.tmBlockOneCount); i++) { writeWord(code, offset + i * 2, moveIndexes.get(i + Gen6Constants.tmBlockOneCount)); } // Update TM item descriptions List itemDescriptions = getStrings(false, romEntry.getInt("ItemDescriptionsTextOffset")); List moveDescriptions = getStrings(false, romEntry.getInt("MoveDescriptionsTextOffset")); // TM01 is item 328 and so on for (int i = 0; i < Gen6Constants.tmBlockOneCount; i++) { itemDescriptions.set(i + Gen6Constants.tmBlockOneOffset, moveDescriptions.get(moveIndexes.get(i))); } // TM93-95 are 618-620 for (int i = 0; i < Gen6Constants.tmBlockTwoCount; i++) { itemDescriptions.set(i + Gen6Constants.tmBlockTwoOffset, moveDescriptions.get(moveIndexes.get(i + Gen6Constants.tmBlockOneCount))); } // TM96-100 are 690 and so on for (int i = 0; i < Gen6Constants.tmBlockThreeCount; i++) { itemDescriptions.set(i + Gen6Constants.tmBlockThreeOffset, moveDescriptions.get(moveIndexes.get(i + Gen6Constants.tmBlockOneCount + Gen6Constants.tmBlockTwoCount))); } // Save the new item descriptions setStrings(false, romEntry.getInt("ItemDescriptionsTextOffset"), itemDescriptions); // Palettes String palettePrefix = Gen6Constants.itemPalettesPrefix; int offsPals = find(code, palettePrefix); if (offsPals > 0) { offsPals += Gen6Constants.itemPalettesPrefix.length() / 2; // because it was a prefix // Write pals for (int i = 0; i < Gen6Constants.tmBlockOneCount; i++) { int itmNum = Gen6Constants.tmBlockOneOffset + i; Move m = this.moves[moveIndexes.get(i)]; int pal = this.typeTMPaletteNumber(m.type, false); writeWord(code, offsPals + itmNum * 4, pal); } for (int i = 0; i < (Gen6Constants.tmBlockTwoCount); i++) { int itmNum = Gen6Constants.tmBlockTwoOffset + i; Move m = this.moves[moveIndexes.get(i + Gen6Constants.tmBlockOneCount)]; int pal = this.typeTMPaletteNumber(m.type, false); writeWord(code, offsPals + itmNum * 4, pal); } for (int i = 0; i < (Gen6Constants.tmBlockThreeCount); i++) { int itmNum = Gen6Constants.tmBlockThreeOffset + i; Move m = this.moves[moveIndexes.get(i + Gen6Constants.tmBlockOneCount + Gen6Constants.tmBlockTwoCount)]; int pal = this.typeTMPaletteNumber(m.type, false); writeWord(code, offsPals + itmNum * 4, pal); } } } } private int find(byte[] data, 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 found = RomFunctions.search(data, searchFor); if (found.size() == 0) { return -1; // not found } else if (found.size() > 1) { return -2; // not unique } else { return found.get(0); } } @Override public int getTMCount() { return Gen6Constants.tmCount; } @Override public int getHMCount() { return Gen6Constants.getHMCount(romEntry.romType); } @Override public Map getTMHMCompatibility() { Map compat = new TreeMap<>(); int formeCount = Gen6Constants.getFormeCount(romEntry.romType); for (int i = 1; i <= Gen6Constants.pokemonCount + formeCount; i++) { byte[] data; data = pokeGarc.files.get(i).get(0); Pokemon pkmn = pokes[i]; boolean[] flags = new boolean[Gen6Constants.tmCount + Gen6Constants.getHMCount(romEntry.romType) + 1]; for (int j = 0; j < 14; j++) { readByteIntoFlags(data, flags, j * 8 + 1, Gen6Constants.bsTMHMCompatOffset + j); } compat.put(pkmn, flags); } return compat; } @Override public void setTMHMCompatibility(Map compatData) { for (Map.Entry compatEntry : compatData.entrySet()) { Pokemon pkmn = compatEntry.getKey(); boolean[] flags = compatEntry.getValue(); byte[] data = pokeGarc.files.get(pkmn.number).get(0); for (int j = 0; j < 14; j++) { data[Gen6Constants.bsTMHMCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1); } } } @Override public boolean hasMoveTutors() { return romEntry.romType == Gen6Constants.Type_ORAS; } @Override public List getMoveTutorMoves() { List mtMoves = new ArrayList<>(); int mtOffset = getMoveTutorMovesOffset(); if (mtOffset > 0) { int val = 0; while (val != 0xFFFF) { val = FileFunctions.read2ByteInt(code,mtOffset); mtOffset += 2; if (val == 0x26E || val == 0xFFFF) continue; mtMoves.add(val); } } return mtMoves; } @Override public void setMoveTutorMoves(List moves) { int mtOffset = find(code, Gen6Constants.tutorsShopPrefix); if (mtOffset > 0) { mtOffset += Gen6Constants.tutorsShopPrefix.length() / 2; // because it was a prefix for (int i = 0; i < Gen6Constants.tutorMoveCount; i++) { FileFunctions.write2ByteInt(code,mtOffset + i*8, moves.get(i)); } } mtOffset = getMoveTutorMovesOffset(); if (mtOffset > 0) { for (int move: moves) { int val = FileFunctions.read2ByteInt(code,mtOffset); if (val == 0x26E) mtOffset += 2; FileFunctions.write2ByteInt(code,mtOffset,move); mtOffset += 2; } } } private int getMoveTutorMovesOffset() { int offset = moveTutorMovesOffset; if (offset == 0) { offset = find(code, Gen6Constants.tutorsLocator); moveTutorMovesOffset = offset; } return offset; } @Override public Map getMoveTutorCompatibility() { Map compat = new TreeMap<>(); int[] sizes = Gen6Constants.tutorSize; int formeCount = Gen6Constants.getFormeCount(romEntry.romType); for (int i = 1; i <= Gen6Constants.pokemonCount + formeCount; i++) { byte[] data; data = pokeGarc.files.get(i).get(0); Pokemon pkmn = pokes[i]; boolean[] flags = new boolean[Arrays.stream(sizes).sum() + 1]; int offset = 0; for (int mt = 0; mt < 4; mt++) { for (int j = 0; j < 4; j++) { readByteIntoFlags(data, flags, offset + j * 8 + 1, Gen6Constants.bsMTCompatOffset + mt * 4 + j); } offset += sizes[mt]; } compat.put(pkmn, flags); } return compat; } @Override public void setMoveTutorCompatibility(Map compatData) { if (!hasMoveTutors()) return; int[] sizes = Gen6Constants.tutorSize; int formeCount = Gen6Constants.getFormeCount(romEntry.romType); for (int i = 1; i <= Gen6Constants.pokemonCount + formeCount; i++) { byte[] data; data = pokeGarc.files.get(i).get(0); Pokemon pkmn = pokes[i]; boolean[] flags = compatData.get(pkmn); int offset = 0; for (int mt = 0; mt < 4; mt++) { boolean[] mtflags = new boolean[sizes[mt] + 1]; System.arraycopy(flags, offset + 1, mtflags, 1, sizes[mt]); for (int j = 0; j < 4; j++) { data[Gen6Constants.bsMTCompatOffset + mt * 4 + j] = getByteFromFlags(mtflags, j * 8 + 1); } offset += sizes[mt]; } } } @Override public String getROMName() { return "Pokemon " + romEntry.name; } @Override public String getROMCode() { return romEntry.romCode; } @Override public String getSupportLevel() { return "Complete"; } @Override public boolean hasTimeBasedEncounters() { return false; } @Override public List getMovesBannedFromLevelup() { return Gen6Constants.bannedMoves; } @Override public boolean hasWildAltFormes() { return true; } @Override public List bannedForStaticPokemon() { return Gen6Constants.actuallyCosmeticForms .stream() .filter(index -> index < Gen6Constants.pokemonCount + Gen6Constants.getFormeCount(romEntry.romType)) .map(index -> pokes[index]) .collect(Collectors.toList()); } @Override public boolean forceSwapStaticMegaEvos() { return romEntry.romType == Gen6Constants.Type_XY; } @Override public boolean hasMainGameLegendaries() { return true; } @Override public List getMainGameLegendaries() { return Arrays.stream(romEntry.arrayEntries.get("MainGameLegendaries")).boxed().collect(Collectors.toList()); } @Override public List getSpecialMusicStatics() { return new ArrayList<>(); } @Override public void applyCorrectStaticMusic(Map specialMusicStaticChanges) { } @Override public boolean hasStaticMusicFix() { return false; } @Override public List getTotemPokemon() { return new ArrayList<>(); } @Override public void setTotemPokemon(List totemPokemon) { } @Override public void removeImpossibleEvolutions(Settings settings) { boolean changeMoveEvos = !(settings.getMovesetsMod() == Settings.MovesetsMod.UNCHANGED); Map> movesets = this.getMovesLearnt(); Set extraEvolutions = new HashSet<>(); for (Pokemon pkmn : pokes) { if (pkmn != null) { extraEvolutions.clear(); for (Evolution evo : pkmn.evolutionsFrom) { if (changeMoveEvos && evo.type == EvolutionType.LEVEL_WITH_MOVE) { // read move int move = evo.extraInfo; int levelLearntAt = 1; for (MoveLearnt ml : movesets.get(evo.from.number)) { if (ml.move == move) { levelLearntAt = ml.level; break; } } if (levelLearntAt == 1) { // override for piloswine levelLearntAt = 45; } // change to pure level evo evo.type = EvolutionType.LEVEL; evo.extraInfo = levelLearntAt; addEvoUpdateLevel(impossibleEvolutionUpdates, evo); } // Pure Trade if (evo.type == EvolutionType.TRADE) { // Replace w/ level 37 evo.type = EvolutionType.LEVEL; evo.extraInfo = 37; addEvoUpdateLevel(impossibleEvolutionUpdates, evo); } // Trade w/ Item if (evo.type == EvolutionType.TRADE_ITEM) { // Get the current item & evolution int item = evo.extraInfo; if (evo.from.number == Species.slowpoke) { // Slowpoke is awkward - he already has a level evo // So we can't do Level up w/ Held Item for him // Put Water Stone instead evo.type = EvolutionType.STONE; evo.extraInfo = Items.waterStone; addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames.get(evo.extraInfo)); } else { addEvoUpdateHeldItem(impossibleEvolutionUpdates, evo, itemNames.get(item)); // Replace, for this entry, w/ // Level up w/ Held Item at Day evo.type = EvolutionType.LEVEL_ITEM_DAY; // now add an extra evo for // Level up w/ Held Item at Night Evolution extraEntry = new Evolution(evo.from, evo.to, true, EvolutionType.LEVEL_ITEM_NIGHT, item); extraEvolutions.add(extraEntry); } } if (evo.type == EvolutionType.TRADE_SPECIAL) { // This is the karrablast <-> shelmet trade // Replace it with Level up w/ Other Species in Party // (22) // Based on what species we're currently dealing with evo.type = EvolutionType.LEVEL_WITH_OTHER; evo.extraInfo = (evo.from.number == Species.karrablast ? Species.shelmet : Species.karrablast); addEvoUpdateParty(impossibleEvolutionUpdates, evo, pokes[evo.extraInfo].fullName()); } // TBD: Pancham, Sliggoo? Sylveon? } pkmn.evolutionsFrom.addAll(extraEvolutions); for (Evolution ev : extraEvolutions) { ev.to.evolutionsTo.add(ev); } } } } @Override public void makeEvolutionsEasier(Settings settings) { boolean wildsRandomized = !settings.getWildPokemonMod().equals(Settings.WildPokemonMod.UNCHANGED); if (wildsRandomized) { for (Pokemon pkmn : pokes) { if (pkmn != null) { for (Evolution evo : pkmn.evolutionsFrom) { if (evo.type == EvolutionType.LEVEL_WITH_OTHER) { // Replace w/ level 35 evo.type = EvolutionType.LEVEL; evo.extraInfo = 35; addEvoUpdateCondensed(easierEvolutionUpdates, evo, false); } } } } } } @Override public void removeTimeBasedEvolutions() { Set extraEvolutions = new HashSet<>(); for (Pokemon pkmn : pokes) { if (pkmn != null) { extraEvolutions.clear(); for (Evolution evo : pkmn.evolutionsFrom) { if (evo.type == EvolutionType.HAPPINESS_DAY) { if (evo.from.number == Species.eevee) { // We can't set Eevee to evolve into Espeon with happiness at night because that's how // Umbreon works in the original game. Instead, make Eevee: == sun stone => Espeon evo.type = EvolutionType.STONE; evo.extraInfo = Items.sunStone; addEvoUpdateStone(timeBasedEvolutionUpdates, evo, itemNames.get(evo.extraInfo)); } else { // Add an extra evo for Happiness at Night addEvoUpdateHappiness(timeBasedEvolutionUpdates, evo); Evolution extraEntry = new Evolution(evo.from, evo.to, true, EvolutionType.HAPPINESS_NIGHT, 0); extraEvolutions.add(extraEntry); } } else if (evo.type == EvolutionType.HAPPINESS_NIGHT) { if (evo.from.number == Species.eevee) { // We can't set Eevee to evolve into Umbreon with happiness at day because that's how // Espeon works in the original game. Instead, make Eevee: == moon stone => Umbreon evo.type = EvolutionType.STONE; evo.extraInfo = Items.moonStone; addEvoUpdateStone(timeBasedEvolutionUpdates, evo, itemNames.get(evo.extraInfo)); } else { // Add an extra evo for Happiness at Day addEvoUpdateHappiness(timeBasedEvolutionUpdates, evo); Evolution extraEntry = new Evolution(evo.from, evo.to, true, EvolutionType.HAPPINESS_DAY, 0); extraEvolutions.add(extraEntry); } } else if (evo.type == EvolutionType.LEVEL_ITEM_DAY) { int item = evo.extraInfo; // Make sure we don't already have an evo for the same item at night (e.g., when using Change Impossible Evos) if (evo.from.evolutionsFrom.stream().noneMatch(e -> e.type == EvolutionType.LEVEL_ITEM_NIGHT && e.extraInfo == item)) { // Add an extra evo for Level w/ Item During Night addEvoUpdateHeldItem(timeBasedEvolutionUpdates, evo, itemNames.get(item)); Evolution extraEntry = new Evolution(evo.from, evo.to, true, EvolutionType.LEVEL_ITEM_NIGHT, item); extraEvolutions.add(extraEntry); } } else if (evo.type == EvolutionType.LEVEL_ITEM_NIGHT) { int item = evo.extraInfo; // Make sure we don't already have an evo for the same item at day (e.g., when using Change Impossible Evos) if (evo.from.evolutionsFrom.stream().noneMatch(e -> e.type == EvolutionType.LEVEL_ITEM_DAY && e.extraInfo == item)) { // Add an extra evo for Level w/ Item During Day addEvoUpdateHeldItem(timeBasedEvolutionUpdates, evo, itemNames.get(item)); Evolution extraEntry = new Evolution(evo.from, evo.to, true, EvolutionType.LEVEL_ITEM_DAY, item); extraEvolutions.add(extraEntry); } } else if (evo.type == EvolutionType.LEVEL_DAY || evo.type == EvolutionType.LEVEL_NIGHT) { addEvoUpdateLevel(timeBasedEvolutionUpdates, evo); evo.type = EvolutionType.LEVEL; } } pkmn.evolutionsFrom.addAll(extraEvolutions); for (Evolution ev : extraEvolutions) { ev.to.evolutionsTo.add(ev); } } } } @Override public boolean hasShopRandomization() { return true; } @Override public boolean canChangeTrainerText() { return true; } @Override public List getTrainerNames() { List tnames = getStrings(false, romEntry.getInt("TrainerNamesTextOffset")); tnames.remove(0); // blank one return tnames; } @Override public int maxTrainerNameLength() { return 10; } @Override public void setTrainerNames(List trainerNames) { List tnames = getStrings(false, romEntry.getInt("TrainerNamesTextOffset")); List newTNames = new ArrayList<>(trainerNames); newTNames.add(0, tnames.get(0)); // the 0-entry, preserve it setStrings(false, romEntry.getInt("TrainerNamesTextOffset"), newTNames); try { writeStringsForAllLanguages(newTNames, romEntry.getInt("TrainerNamesTextOffset")); } catch (IOException e) { throw new RandomizerIOException(e); } } private void writeStringsForAllLanguages(List strings, int index) throws IOException { List nonEnglishLanguages = Arrays.asList("JaKana", "JaKanji", "Fr", "It", "De", "Es", "Ko"); for (String nonEnglishLanguage : nonEnglishLanguages) { String key = "TextStrings" + nonEnglishLanguage; GARCArchive stringsGarcForLanguage = readGARC(romEntry.getFile(key),true); setStrings(stringsGarcForLanguage, index, strings); writeGARC(romEntry.getFile(key), stringsGarcForLanguage); } } @Override public TrainerNameMode trainerNameMode() { return TrainerNameMode.MAX_LENGTH; } @Override public List getTCNameLengthsByTrainer() { return new ArrayList<>(); } @Override public List getTrainerClassNames() { return getStrings(false, romEntry.getInt("TrainerClassesTextOffset")); } @Override public void setTrainerClassNames(List trainerClassNames) { setStrings(false, romEntry.getInt("TrainerClassesTextOffset"), trainerClassNames); try { writeStringsForAllLanguages(trainerClassNames, romEntry.getInt("TrainerClassesTextOffset")); } catch (IOException e) { throw new RandomizerIOException(e); } } @Override public int maxTrainerClassNameLength() { return 15; // "Pokémon Breeder" is possible, so, } @Override public boolean fixedTrainerClassNamesLength() { return false; } @Override public List getDoublesTrainerClasses() { int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses"); List doubles = new ArrayList<>(); for (int tClass : doublesClasses) { doubles.add(tClass); } return doubles; } @Override public String getDefaultExtension() { return "cxi"; } @Override public int abilitiesPerPokemon() { return 3; } @Override public int highestAbilityIndex() { return Gen6Constants.getHighestAbilityIndex(romEntry.romType); } @Override public int internalStringLength(String string) { return string.length(); } @Override public void randomizeIntroPokemon() { if (romEntry.romType == Gen6Constants.Type_XY) { // Pick a random Pokemon, including formes Pokemon introPokemon = randomPokemonInclFormes(); while (introPokemon.actuallyCosmetic) { introPokemon = randomPokemonInclFormes(); } int introPokemonNum = introPokemon.number; int introPokemonForme = 0; boolean checkCosmetics = true; if (introPokemon.formeNumber > 0) { introPokemonForme = introPokemon.formeNumber; introPokemonNum = introPokemon.baseForme.number; checkCosmetics = false; } if (checkCosmetics && introPokemon.cosmeticForms > 0) { introPokemonForme = introPokemon.getCosmeticFormNumber(this.random.nextInt(introPokemon.cosmeticForms)); } else if (!checkCosmetics && introPokemon.cosmeticForms > 0) { introPokemonForme += introPokemon.getCosmeticFormNumber(this.random.nextInt(introPokemon.cosmeticForms)); } // Find the value for the Pokemon's cry int baseAddr = find(code, Gen6Constants.criesTablePrefixXY); baseAddr += Gen6Constants.criesTablePrefixXY.length() / 2; int pkNumKey = introPokemonNum; if (introPokemonForme != 0) { int extraOffset = readLong(code, baseAddr + (pkNumKey * 0x14)); pkNumKey = extraOffset + (introPokemonForme - 1); } int initialCry = readLong(code, baseAddr + (pkNumKey * 0x14) + 0x4); int repeatedCry = readLong(code, baseAddr + (pkNumKey * 0x14) + 0x10); // Write to DLLIntro.cro try { byte[] introCRO = readFile(romEntry.getFile("Intro")); // Replace the Pokemon model that's loaded, and set its forme int croModelOffset = find(introCRO, Gen6Constants.introPokemonModelOffsetXY); croModelOffset += Gen6Constants.introPokemonModelOffsetXY.length() / 2; writeWord(introCRO, croModelOffset, introPokemonNum); introCRO[croModelOffset + 2] = (byte)introPokemonForme; // Shiny chance if (this.random.nextInt(256) == 0) { introCRO[croModelOffset + 4] = 1; } // Replace the initial cry when the Pokemon exits the ball // First, re-point two branches int croInitialCryOffset1 = find(introCRO, Gen6Constants.introInitialCryOffset1XY); croInitialCryOffset1 += Gen6Constants.introInitialCryOffset1XY.length() / 2; introCRO[croInitialCryOffset1] = 0x5E; int croInitialCryOffset2 = find(introCRO, Gen6Constants.introInitialCryOffset2XY); croInitialCryOffset2 += Gen6Constants.introInitialCryOffset2XY.length() / 2; introCRO[croInitialCryOffset2] = 0x2F; // Then change the parameters that are loaded for a function call, and also change the function call // itself to a function that uses the "cry value" instead of Pokemon ID + forme + emotion (same function // that is used for the repeated cries) int croInitialCryOffset3 = find(introCRO, Gen6Constants.introInitialCryOffset3XY); croInitialCryOffset3 += Gen6Constants.introInitialCryOffset3XY.length() / 2; writeLong(introCRO, croInitialCryOffset3, 0xE1A02000); // cpy r2,r0 writeLong(introCRO, croInitialCryOffset3 + 0x4, 0xE59F100C); // ldr r1,=#CRY_VALUE writeLong(introCRO, croInitialCryOffset3 + 0x8, 0xE58D0000); // str r0,[sp] writeLong(introCRO, croInitialCryOffset3 + 0xC, 0xEBFFFDE9); // bl FUN_006a51d4 writeLong(introCRO, croInitialCryOffset3 + 0x10, readLong(introCRO, croInitialCryOffset3 + 0x14)); // Move these two instructions up four bytes writeLong(introCRO, croInitialCryOffset3 + 0x14, readLong(introCRO, croInitialCryOffset3 + 0x18)); writeLong(introCRO, croInitialCryOffset3 + 0x18, initialCry); // CRY_VALUE pool // Replace the repeated cry that the Pokemon does while standing around // Just replace a pool value int croRepeatedCryOffset = find(introCRO, Gen6Constants.introRepeatedCryOffsetXY); croRepeatedCryOffset += Gen6Constants.introRepeatedCryOffsetXY.length() / 2; writeLong(introCRO, croRepeatedCryOffset, repeatedCry); writeFile(romEntry.getFile("Intro"), introCRO); } catch (IOException e) { e.printStackTrace(); } } } @Override public ItemList getAllowedItems() { return allowedItems; } @Override public ItemList getNonBadItems() { return nonBadItems; } @Override public List getUniqueNoSellItems() { return Gen6Constants.uniqueNoSellItems; } @Override public List getRegularShopItems() { return Gen6Constants.regularShopItems; } @Override public List getOPShopItems() { return Gen6Constants.opShopItems; } @Override public String[] getItemNames() { return itemNames.toArray(new String[0]); } @Override public String abilityName(int number) { return abilityNames.get(number); } @Override public Map> getAbilityVariations() { return Gen5Constants.abilityVariations; } @Override public List getUselessAbilities() { return new ArrayList<>(Gen6Constants.uselessAbilities); } @Override public int getAbilityForTrainerPokemon(TrainerPokemon tp) { // Before randomizing Trainer Pokemon, one possible value for abilitySlot is 0, // which represents "Either Ability 1 or 2". During randomization, we make sure to // to set abilitySlot to some non-zero value, but if you call this method without // randomization, then you'll hit this case. if (tp.abilitySlot < 1 || tp.abilitySlot > 3) { return 0; } List abilityList = Arrays.asList(tp.pokemon.ability1, tp.pokemon.ability2, tp.pokemon.ability3); return abilityList.get(tp.abilitySlot - 1); } @Override public boolean hasMegaEvolutions() { return true; } private int tmFromIndex(int index) { if (index >= Gen6Constants.tmBlockOneOffset && index < Gen6Constants.tmBlockOneOffset + Gen6Constants.tmBlockOneCount) { return index - (Gen6Constants.tmBlockOneOffset - 1); } else if (index >= Gen6Constants.tmBlockTwoOffset && index < Gen6Constants.tmBlockTwoOffset + Gen6Constants.tmBlockTwoCount) { return (index + Gen6Constants.tmBlockOneCount) - (Gen6Constants.tmBlockTwoOffset - 1); } else { return (index + Gen6Constants.tmBlockOneCount + Gen6Constants.tmBlockTwoCount) - (Gen6Constants.tmBlockThreeOffset - 1); } } private int indexFromTM(int tm) { if (tm >= 1 && tm <= Gen6Constants.tmBlockOneCount) { return tm + (Gen6Constants.tmBlockOneOffset - 1); } else if (tm > Gen6Constants.tmBlockOneCount && tm <= Gen6Constants.tmBlockOneCount + Gen6Constants.tmBlockTwoCount) { return tm + (Gen6Constants.tmBlockTwoOffset - 1 - Gen6Constants.tmBlockOneCount); } else { return tm + (Gen6Constants.tmBlockThreeOffset - 1 - (Gen6Constants.tmBlockOneCount + Gen6Constants.tmBlockTwoCount)); } } @Override public List getCurrentFieldTMs() { List fieldItems = this.getFieldItems(); List fieldTMs = new ArrayList<>(); ItemList allowedItems = Gen6Constants.getAllowedItems(romEntry.romType); for (int item : fieldItems) { if (allowedItems.isTM(item)) { fieldTMs.add(tmFromIndex(item)); } } return fieldTMs; } @Override public void setFieldTMs(List fieldTMs) { List fieldItems = this.getFieldItems(); int fiLength = fieldItems.size(); Iterator iterTMs = fieldTMs.iterator(); ItemList allowedItems = Gen6Constants.getAllowedItems(romEntry.romType); for (int i = 0; i < fiLength; i++) { int oldItem = fieldItems.get(i); if (allowedItems.isTM(oldItem)) { int newItem = indexFromTM(iterTMs.next()); fieldItems.set(i, newItem); } } this.setFieldItems(fieldItems); } @Override public List getRegularFieldItems() { List fieldItems = this.getFieldItems(); List fieldRegItems = new ArrayList<>(); ItemList allowedItems = Gen6Constants.getAllowedItems(romEntry.romType); for (int item : fieldItems) { if (allowedItems.isAllowed(item) && !(allowedItems.isTM(item))) { fieldRegItems.add(item); } } return fieldRegItems; } @Override public void setRegularFieldItems(List items) { List fieldItems = this.getFieldItems(); int fiLength = fieldItems.size(); Iterator iterNewItems = items.iterator(); ItemList allowedItems = Gen6Constants.getAllowedItems(romEntry.romType); for (int i = 0; i < fiLength; i++) { int oldItem = fieldItems.get(i); if (!(allowedItems.isTM(oldItem)) && allowedItems.isAllowed(oldItem) && oldItem != Items.masterBall) { int newItem = iterNewItems.next(); fieldItems.set(i, newItem); } } this.setFieldItems(fieldItems); } @Override public List getRequiredFieldTMs() { return Gen6Constants.getRequiredFieldTMs(romEntry.romType); } public List getFieldItems() { List fieldItems = new ArrayList<>(); try { // normal items int normalItemsFile = romEntry.getInt("FieldItemsScriptNumber"); int normalItemsOffset = romEntry.getInt("FieldItemsOffset"); GARCArchive scriptGarc = readGARC(romEntry.getFile("Scripts"),true); AMX normalItemAMX = new AMX(scriptGarc.files.get(normalItemsFile).get(0)); byte[] data = normalItemAMX.decData; for (int i = normalItemsOffset; i < data.length; i += 12) { int item = FileFunctions.read2ByteInt(data,i); fieldItems.add(item); } // hidden items - separate handling for XY and ORAS if (romEntry.romType == Gen6Constants.Type_XY) { int hiddenItemsFile = romEntry.getInt("HiddenItemsScriptNumber"); int hiddenItemsOffset = romEntry.getInt("HiddenItemsOffset"); AMX hiddenItemAMX = new AMX(scriptGarc.files.get(hiddenItemsFile).get(0)); data = hiddenItemAMX.decData; for (int i = hiddenItemsOffset; i < data.length; i += 12) { int item = FileFunctions.read2ByteInt(data,i); fieldItems.add(item); } } else { String hiddenItemsPrefix = Gen6Constants.hiddenItemsPrefixORAS; int offsHidden = find(code,hiddenItemsPrefix); if (offsHidden > 0) { offsHidden += hiddenItemsPrefix.length() / 2; for (int i = 0; i < Gen6Constants.hiddenItemCountORAS; i++) { int item = FileFunctions.read2ByteInt(code, offsHidden + (i * 0xE) + 2); fieldItems.add(item); } } } // In ORAS, it's possible to encounter the sparkling Mega Stone items on the field // before you finish the game. Thus, we want to randomize them as well. if (romEntry.romType == Gen6Constants.Type_ORAS) { List fieldMegaStones = this.getFieldMegaStonesORAS(scriptGarc); fieldItems.addAll(fieldMegaStones); } } catch (IOException e) { throw new RandomizerIOException(e); } return fieldItems; } private List getFieldMegaStonesORAS(GARCArchive scriptGarc) throws IOException { List fieldMegaStones = new ArrayList<>(); int megaStoneItemScriptFile = romEntry.getInt("MegaStoneItemScriptNumber"); byte[] megaStoneItemEventBytes = scriptGarc.getFile(megaStoneItemScriptFile); AMX megaStoneItemEvent = new AMX(megaStoneItemEventBytes); for (int i = 0; i < Gen6Constants.megastoneTableLengthORAS; i++) { int offset = Gen6Constants.megastoneTableStartingOffsetORAS + (i * Gen6Constants.megastoneTableEntrySizeORAS); int item = FileFunctions.read2ByteInt(megaStoneItemEvent.decData, offset); fieldMegaStones.add(item); } return fieldMegaStones; } public void setFieldItems(List items) { try { Iterator iterItems = items.iterator(); // normal items int normalItemsFile = romEntry.getInt("FieldItemsScriptNumber"); int normalItemsOffset = romEntry.getInt("FieldItemsOffset"); GARCArchive scriptGarc = readGARC(romEntry.getFile("Scripts"),true); AMX normalItemAMX = new AMX(scriptGarc.files.get(normalItemsFile).get(0)); byte[] data = normalItemAMX.decData; for (int i = normalItemsOffset; i < data.length; i += 12) { int item = iterItems.next(); FileFunctions.write2ByteInt(data,i,item); } scriptGarc.setFile(normalItemsFile,normalItemAMX.getBytes()); // hidden items - separate handling for XY and ORAS if (romEntry.romType == Gen6Constants.Type_XY) { int hiddenItemsFile = romEntry.getInt("HiddenItemsScriptNumber"); int hiddenItemsOffset = romEntry.getInt("HiddenItemsOffset"); AMX hiddenItemAMX = new AMX(scriptGarc.files.get(hiddenItemsFile).get(0)); data = hiddenItemAMX.decData; for (int i = hiddenItemsOffset; i < data.length; i += 12) { int item = iterItems.next(); FileFunctions.write2ByteInt(data,i,item); } scriptGarc.setFile(hiddenItemsFile,hiddenItemAMX.getBytes()); } else { String hiddenItemsPrefix = Gen6Constants.hiddenItemsPrefixORAS; int offsHidden = find(code,hiddenItemsPrefix); if (offsHidden > 0) { offsHidden += hiddenItemsPrefix.length() / 2; for (int i = 0; i < Gen6Constants.hiddenItemCountORAS; i++) { int item = iterItems.next(); FileFunctions.write2ByteInt(code,offsHidden + (i * 0xE) + 2, item); } } } // Sparkling Mega Stone items for ORAS only if (romEntry.romType == Gen6Constants.Type_ORAS) { List fieldMegaStones = this.getFieldMegaStonesORAS(scriptGarc); Map megaStoneMap = new HashMap<>(); int megaStoneItemScriptFile = romEntry.getInt("MegaStoneItemScriptNumber"); byte[] megaStoneItemEventBytes = scriptGarc.getFile(megaStoneItemScriptFile); AMX megaStoneItemEvent = new AMX(megaStoneItemEventBytes); for (int i = 0; i < Gen6Constants.megastoneTableLengthORAS; i++) { int offset = Gen6Constants.megastoneTableStartingOffsetORAS + (i * Gen6Constants.megastoneTableEntrySizeORAS); int oldItem = fieldMegaStones.get(i); int newItem = iterItems.next(); if (megaStoneMap.containsKey(oldItem)) { // There are some duplicate entries for certain Mega Stones, and we're not quite sure why. // Set them to the same item for sanity's sake. int replacementItem = megaStoneMap.get(oldItem); FileFunctions.write2ByteInt(megaStoneItemEvent.decData, offset, replacementItem); } else { FileFunctions.write2ByteInt(megaStoneItemEvent.decData, offset, newItem); megaStoneMap.put(oldItem, newItem); } } scriptGarc.setFile(megaStoneItemScriptFile, megaStoneItemEvent.getBytes()); } writeGARC(romEntry.getFile("Scripts"),scriptGarc); } catch (IOException e) { throw new RandomizerIOException(e); } } @Override public List getIngameTrades() { List trades = new ArrayList<>(); int count = romEntry.getInt("IngameTradeCount"); String prefix = Gen6Constants.getIngameTradesPrefix(romEntry.romType); List tradeStrings = getStrings(false, romEntry.getInt("IngameTradesTextOffset")); int textOffset = romEntry.getInt("IngameTradesTextExtraOffset"); int offset = find(code,prefix); if (offset > 0) { offset += prefix.length() / 2; for (int i = 0; i < count; i++) { IngameTrade trade = new IngameTrade(); trade.nickname = tradeStrings.get(textOffset + i); trade.givenPokemon = pokes[FileFunctions.read2ByteInt(code,offset)]; trade.ivs = new int[6]; for (int iv = 0; iv < 6; iv++) { trade.ivs[iv] = code[offset + 5 + iv]; } trade.otId = FileFunctions.read2ByteInt(code,offset + 0xE); trade.item = FileFunctions.read2ByteInt(code,offset + 0x10); trade.otName = tradeStrings.get(textOffset + count + i); trade.requestedPokemon = pokes[FileFunctions.read2ByteInt(code,offset + 0x20)]; trades.add(trade); offset += Gen6Constants.ingameTradeSize; } } return trades; } @Override public void setIngameTrades(List trades) { List oldTrades = this.getIngameTrades(); int[] hardcodedTradeOffsets = romEntry.arrayEntries.get("HardcodedTradeOffsets"); int[] hardcodedTradeTexts = romEntry.arrayEntries.get("HardcodedTradeTexts"); int count = romEntry.getInt("IngameTradeCount"); String prefix = Gen6Constants.getIngameTradesPrefix(romEntry.romType); List tradeStrings = getStrings(false, romEntry.getInt("IngameTradesTextOffset")); int textOffset = romEntry.getInt("IngameTradesTextExtraOffset"); int offset = find(code,prefix); if (offset > 0) { offset += prefix.length() / 2; for (int i = 0; i < count; i++) { IngameTrade trade = trades.get(i); tradeStrings.set(textOffset + i, trade.nickname); FileFunctions.write2ByteInt(code,offset,trade.givenPokemon.number); for (int iv = 0; iv < 6; iv++) { code[offset + 5 + iv] = (byte)trade.ivs[iv]; } FileFunctions.write2ByteInt(code,offset + 0xE,trade.otId); FileFunctions.write2ByteInt(code,offset + 0x10,trade.item); tradeStrings.set(textOffset + count + i, trade.otName); FileFunctions.write2ByteInt(code,offset + 0x20, trade.requestedPokemon == null ? 0 : trade.requestedPokemon.number); offset += Gen6Constants.ingameTradeSize; // In XY, there are some trades that use hardcoded strings. Go and forcibly update // the story text so that the trainer says what they want to trade. if (romEntry.romType == Gen6Constants.Type_XY && Gen6Constants.xyHardcodedTradeOffsets.contains(i)) { int hardcodedTradeIndex = Gen6Constants.xyHardcodedTradeOffsets.indexOf(i); updateHardcodedTradeText(oldTrades.get(i), trade, Gen6Constants.xyHardcodedTradeTexts.get(hardcodedTradeIndex)); } } this.setStrings(false, romEntry.getInt("IngameTradesTextOffset"), tradeStrings); } } // NOTE: This method is kind of stupid, in that it doesn't try to reflow the text to better fit; it just // blindly replaces the Pokemon's name. However, it seems to work well enough for what we need. private void updateHardcodedTradeText(IngameTrade oldTrade, IngameTrade newTrade, int hardcodedTradeTextFile) { List hardcodedTradeStrings = getStrings(true, hardcodedTradeTextFile); Pokemon oldRequested = oldTrade.requestedPokemon; String oldRequestedName = oldRequested != null ? oldRequested.name : null; String oldGivenName = oldTrade.givenPokemon.name; Pokemon newRequested = newTrade.requestedPokemon; String newRequestedName = newRequested != null ? newRequested.name : null; String newGivenName = newTrade.givenPokemon.name; for (int i = 0; i < hardcodedTradeStrings.size(); i++) { String hardcodedTradeString = hardcodedTradeStrings.get(i); if (oldRequestedName != null && newRequestedName != null && hardcodedTradeString.contains(oldRequestedName)) { hardcodedTradeString = hardcodedTradeString.replace(oldRequestedName, newRequestedName); } if (hardcodedTradeString.contains(oldGivenName)) { hardcodedTradeString = hardcodedTradeString.replace(oldGivenName, newGivenName); } hardcodedTradeStrings.set(i, hardcodedTradeString); } this.setStrings(true, hardcodedTradeTextFile, hardcodedTradeStrings); } @Override public boolean hasDVs() { return false; } @Override public int generationOfPokemon() { return 6; } @Override public void removeEvosForPokemonPool() { // slightly more complicated than gen2/3 // we have to update a "baby table" too List pokemonIncluded = this.mainPokemonListInclFormes; Set 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); } } try { // baby pokemon GARCArchive babyGarc = readGARC(romEntry.getFile("BabyPokemon"), true); byte[] masterFile = babyGarc.getFile(Gen6Constants.pokemonCount + 1); for (int i = 1; i <= Gen6Constants.pokemonCount; i++) { byte[] babyFile = babyGarc.getFile(i); Pokemon baby = pokes[i]; while (baby.evolutionsTo.size() > 0) { // Grab the first "to evolution" even if there are multiple baby = baby.evolutionsTo.get(0).from; } writeWord(babyFile, 0, baby.number); writeWord(masterFile, i * 2, baby.number); babyGarc.setFile(i, babyFile); } babyGarc.setFile(Gen6Constants.pokemonCount + 1, masterFile); writeGARC(romEntry.getFile("BabyPokemon"), babyGarc); } catch (IOException e) { throw new RandomizerIOException(e); } } @Override public boolean supportsFourStartingMoves() { return true; } @Override public List getFieldMoves() { if (romEntry.romType == Gen6Constants.Type_XY) { return Gen6Constants.fieldMovesXY; } else { return Gen6Constants.fieldMovesORAS; } } @Override public List getEarlyRequiredHMMoves() { return new ArrayList<>(); } @Override public Map getShopItems() { int[] tmShops = romEntry.arrayEntries.get("TMShops"); int[] regularShops = romEntry.arrayEntries.get("RegularShops"); int[] shopItemSizes = romEntry.arrayEntries.get("ShopItemSizes"); int shopCount = romEntry.getInt("ShopCount"); Map shopItemsMap = new TreeMap<>(); int offset = getShopItemsOffset(); if (offset <= 0) { return shopItemsMap; } for (int i = 0; i < shopCount; i++) { boolean badShop = false; for (int tmShop: tmShops) { if (i == tmShop) { badShop = true; offset += (shopItemSizes[i] * 2); break; } } for (int regularShop: regularShops) { if (badShop) break; if (i == regularShop) { badShop = true; offset += (shopItemSizes[i] * 2); break; } } if (!badShop) { List items = new ArrayList<>(); for (int j = 0; j < shopItemSizes[i]; j++) { items.add(FileFunctions.read2ByteInt(code,offset)); offset += 2; } Shop shop = new Shop(); shop.items = items; shop.name = shopNames.get(i); shop.isMainGame = Gen6Constants.getMainGameShops(romEntry.romType).contains(i); shopItemsMap.put(i, shop); } } return shopItemsMap; } @Override public void setShopItems(Map shopItems) { int[] shopItemSizes = romEntry.arrayEntries.get("ShopItemSizes"); int[] tmShops = romEntry.arrayEntries.get("TMShops"); int[] regularShops = romEntry.arrayEntries.get("RegularShops"); int shopCount = romEntry.getInt("ShopCount"); int offset = getShopItemsOffset(); if (offset <= 0) { return; } for (int i = 0; i < shopCount; i++) { boolean badShop = false; for (int tmShop: tmShops) { if (badShop) break; if (i == tmShop) { badShop = true; offset += (shopItemSizes[i] * 2); break; } } for (int regularShop: regularShops) { if (badShop) break; if (i == regularShop) { badShop = true; offset += (shopItemSizes[i] * 2); break; } } if (!badShop) { List shopContents = shopItems.get(i).items; Iterator iterItems = shopContents.iterator(); for (int j = 0; j < shopItemSizes[i]; j++) { Integer item = iterItems.next(); FileFunctions.write2ByteInt(code,offset,item); offset += 2; } } } } private int getShopItemsOffset() { int offset = shopItemsOffset; if (offset == 0) { String locator = Gen6Constants.getShopItemsLocator(romEntry.romType); offset = find(code, locator); shopItemsOffset = offset; } return offset; } @Override public void setShopPrices() { try { GARCArchive itemPriceGarc = this.readGARC(romEntry.getFile("ItemData"),true); for (int i = 1; i < itemPriceGarc.files.size(); i++) { writeWord(itemPriceGarc.files.get(i).get(0),0,Gen6Constants.balancedItemPrices.get(i)); } writeGARC(romEntry.getFile("ItemData"),itemPriceGarc); } catch (IOException e) { throw new RandomizerIOException(e); } } @Override public List getPickupItems() { List pickupItems = new ArrayList<>(); // If we haven't found the pickup table for this ROM already, find it. if (pickupItemsTableOffset == 0) { int offset = find(code, Gen6Constants.pickupTableLocator); 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 < Gen6Constants.numberOfPickupItems; i++) { int itemOffset = pickupItemsTableOffset + (2 * i); int item = FileFunctions.read2ByteInt(code, itemOffset); PickupItem pickupItem = new PickupItem(item); pickupItems.add(pickupItem); } } // Assuming we got the items from the last step, fill out the probabilities. if (pickupItems.size() > 0) { 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 pickupItems) { if (pickupItemsTableOffset > 0) { for (int i = 0; i < Gen6Constants.numberOfPickupItems; i++) { int itemOffset = pickupItemsTableOffset + (2 * i); int item = pickupItems.get(i).item; FileFunctions.write2ByteInt(code, itemOffset, item); } } } private void computeCRC32sForRom() throws IOException { this.actualFileCRC32s = new HashMap<>(); this.actualCodeCRC32 = FileFunctions.getCRC32(code); for (String fileKey : romEntry.files.keySet()) { byte[] file = readFile(romEntry.getFile(fileKey)); long crc32 = FileFunctions.getCRC32(file); this.actualFileCRC32s.put(fileKey, crc32); } } @Override public boolean isRomValid() { int index = this.hasGameUpdateLoaded() ? 1 : 0; if (romEntry.expectedCodeCRC32s[index] != actualCodeCRC32) { return false; } for (String fileKey : romEntry.files.keySet()) { long expectedCRC32 = romEntry.files.get(fileKey).expectedCRC32s[index]; long actualCRC32 = actualFileCRC32s.get(fileKey); if (expectedCRC32 != actualCRC32) { return false; } } return true; } @Override public BufferedImage getMascotImage() { try { GARCArchive pokespritesGARC = this.readGARC(romEntry.getFile("PokemonGraphics"),false); int pkIndex = this.random.nextInt(pokespritesGARC.files.size()-2)+1; byte[] icon = pokespritesGARC.files.get(pkIndex).get(0); int paletteCount = readWord(icon,2); byte[] rawPalette = Arrays.copyOfRange(icon,4,4+paletteCount*2); int[] palette = new int[paletteCount]; for (int i = 0; i < paletteCount; i++) { palette[i] = GFXFunctions.conv3DS16BitColorToARGB(readWord(rawPalette, i * 2)); } int width = 64; int height = 32; // Get the picture and uncompress it. byte[] uncompressedPic = Arrays.copyOfRange(icon,4+paletteCount*2,4+paletteCount*2+width*height); int bpp = paletteCount <= 0x10 ? 4 : 8; // Output to 64x144 tiled image to prepare for unscrambling BufferedImage bim = GFXFunctions.drawTiledZOrderImage(uncompressedPic, palette, 0, width, height, bpp); // Unscramble the above onto a 96x96 canvas BufferedImage finalImage = new BufferedImage(40, 30, BufferedImage.TYPE_INT_ARGB); Graphics g = finalImage.getGraphics(); g.drawImage(bim, 0, 0, 64, 64, 0, 0, 64, 64, null); g.drawImage(bim, 64, 0, 96, 8, 0, 64, 32, 72, null); g.drawImage(bim, 64, 8, 96, 16, 32, 64, 64, 72, null); g.drawImage(bim, 64, 16, 96, 24, 0, 72, 32, 80, null); g.drawImage(bim, 64, 24, 96, 32, 32, 72, 64, 80, null); g.drawImage(bim, 64, 32, 96, 40, 0, 80, 32, 88, null); g.drawImage(bim, 64, 40, 96, 48, 32, 80, 64, 88, null); g.drawImage(bim, 64, 48, 96, 56, 0, 88, 32, 96, null); g.drawImage(bim, 64, 56, 96, 64, 32, 88, 64, 96, null); g.drawImage(bim, 0, 64, 64, 96, 0, 96, 64, 128, null); g.drawImage(bim, 64, 64, 96, 72, 0, 128, 32, 136, null); g.drawImage(bim, 64, 72, 96, 80, 32, 128, 64, 136, null); g.drawImage(bim, 64, 80, 96, 88, 0, 136, 32, 144, null); g.drawImage(bim, 64, 88, 96, 96, 32, 136, 64, 144, null); // Phew, all done. return finalImage; } catch (IOException e) { throw new RandomizerIOException(e); } } @Override public List getAllHeldItems() { return Gen6Constants.allHeldItems; } @Override public List getAllConsumableHeldItems() { return Gen6Constants.consumableHeldItems; } @Override public List getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List moves, Map> movesets) { List items = new ArrayList<>(); items.addAll(Gen6Constants.generalPurposeConsumableItems); int frequencyBoostCount = 6; // Make some very good items more common, but not too common if (!consumableOnly) { frequencyBoostCount = 8; // bigger to account for larger item pool. items.addAll(Gen6Constants.generalPurposeItems); } int[] pokeMoves = RomFunctions.getMovesAtLevel(tp.pokemon.number, movesets, tp.level); int numDamagingMoves = 0; for (int moveIdx : pokeMoves) { Move move = moves.get(moveIdx); if (move == null) { continue; } if (move.category == MoveCategory.PHYSICAL) { numDamagingMoves++; items.add(Items.liechiBerry); items.add(Gen6Constants.consumableTypeBoostingItems.get(move.type)); if (!consumableOnly) { items.addAll(Gen6Constants.typeBoostingItems.get(move.type)); items.add(Items.choiceBand); items.add(Items.muscleBand); } } if (move.category == MoveCategory.SPECIAL) { numDamagingMoves++; items.add(Items.petayaBerry); items.add(Gen6Constants.consumableTypeBoostingItems.get(move.type)); if (!consumableOnly) { items.addAll(Gen6Constants.typeBoostingItems.get(move.type)); items.add(Items.wiseGlasses); items.add(Items.choiceSpecs); } } if (!consumableOnly && Gen6Constants.moveBoostingItems.containsKey(moveIdx)) { items.addAll(Gen6Constants.moveBoostingItems.get(moveIdx)); } } if (numDamagingMoves >= 2) { items.add(Items.assaultVest); } Map byType = Effectiveness.against(tp.pokemon.primaryType, tp.pokemon.secondaryType, 6); for(Map.Entry entry : byType.entrySet()) { Integer berry = Gen6Constants.weaknessReducingBerries.get(entry.getKey()); if (entry.getValue() == Effectiveness.DOUBLE) { items.add(berry); } else if (entry.getValue() == Effectiveness.QUADRUPLE) { for (int i = 0; i < frequencyBoostCount; i++) { items.add(berry); } } } if (byType.get(Type.NORMAL) == Effectiveness.NEUTRAL) { items.add(Items.chilanBerry); } int ability = this.getAbilityForTrainerPokemon(tp); if (ability == Abilities.levitate) { items.removeAll(Arrays.asList(Items.shucaBerry)); } else if (byType.get(Type.GROUND) == Effectiveness.DOUBLE || byType.get(Type.GROUND) == Effectiveness.QUADRUPLE) { items.add(Items.airBalloon); } if (!consumableOnly) { if (Gen6Constants.abilityBoostingItems.containsKey(ability)) { items.addAll(Gen6Constants.abilityBoostingItems.get(ability)); } if (tp.pokemon.primaryType == Type.POISON || tp.pokemon.secondaryType == Type.POISON) { items.add(Items.blackSludge); } List speciesItems = Gen6Constants.speciesBoostingItems.get(tp.pokemon.number); if (speciesItems != null) { for (int i = 0; i < frequencyBoostCount; i++) { items.addAll(speciesItems); } } if (!tp.pokemon.evolutionsFrom.isEmpty() && tp.level >= 20) { // eviolite can be too good for early game, so we gate it behind a minimum level. // We go with the same level as the option for "No early wonder guard". items.add(Items.eviolite); } } return items; } }