diff options
author | Rafael Marçalo <public@rafaelmarcalo.xyz> | 2024-09-05 16:31:33 +0100 |
---|---|---|
committer | Rafael Marçalo <public@rafaelmarcalo.xyz> | 2024-09-05 16:31:33 +0100 |
commit | 8b67572ad7e1508341345dc46a2597e9fa170cbb (patch) | |
tree | 8f37c4d60ce0f07b9eaf30be34f39298da97b242 /src/com/pkrandom/romhandlers | |
parent | b65f4a80da28e7ec4de16c8b1abf906e8d7be2c5 (diff) |
Diffstat (limited to 'src/com/pkrandom/romhandlers')
-rw-r--r-- | src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java | 350 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/AbstractDSRomHandler.java | 390 | ||||
-rw-r--r-- | src/com/pkrandom/romhandlers/AbstractGBCRomHandler.java | 224 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/AbstractGBRomHandler.java | 210 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/AbstractRomHandler.java | 7558 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/Gen1RomHandler.java | 2918 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/Gen2RomHandler.java | 2999 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/Gen3RomHandler.java | 4473 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/Gen4RomHandler.java | 5841 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/Gen5RomHandler.java | 4343 | ||||
-rw-r--r-- | src/com/pkrandom/romhandlers/Gen6RomHandler.java | 4270 | ||||
-rw-r--r-- | src/com/pkrandom/romhandlers/Gen7RomHandler.java | 3821 | ||||
-rwxr-xr-x | src/com/pkrandom/romhandlers/RomHandler.java | 660 |
13 files changed, 38057 insertions, 0 deletions
diff --git a/src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java b/src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java new file mode 100644 index 0000000..94b7111 --- /dev/null +++ b/src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java @@ -0,0 +1,350 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- Abstract3DSRomHandler.java - a base class for 3DS rom handlers --*/ +/*-- which standardises common 3DS functions. --*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import com.pkrandom.FileFunctions; +import com.pkrandom.ctr.GARCArchive; +import com.pkrandom.ctr.NCCH; +import com.pkrandom.exceptions.CannotWriteToLocationException; +import com.pkrandom.exceptions.EncryptedROMException; +import com.pkrandom.exceptions.RandomizerIOException; +import com.pkrandom.pokemon.Type; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Random; + +public abstract class Abstract3DSRomHandler extends AbstractRomHandler { + + private NCCH baseRom; + private NCCH gameUpdate; + private String loadedFN; + + public Abstract3DSRomHandler(Random random, PrintStream logStream) { + super(random, logStream); + } + + @Override + public boolean loadRom(String filename) { + String productCode = getProductCodeFromFile(filename); + String titleId = getTitleIdFromFile(filename); + if (!this.detect3DSRom(productCode, titleId)) { + return false; + } + // Load inner rom + try { + baseRom = new NCCH(filename, productCode, titleId); + if (!baseRom.isDecrypted()) { + throw new EncryptedROMException(filename); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + loadedFN = filename; + this.loadedROM(productCode, titleId); + return true; + } + + protected abstract boolean detect3DSRom(String productCode, String titleId); + + @Override + public String loadedFilename() { + return loadedFN; + } + + protected abstract void loadedROM(String productCode, String titleId); + + protected abstract void savingROM() throws IOException; + + protected abstract String getGameAcronym(); + + @Override + public boolean saveRomFile(String filename, long seed) { + try { + savingROM(); + baseRom.saveAsNCCH(filename, getGameAcronym(), seed); + } catch (IOException | NoSuchAlgorithmException e) { + if (e.getMessage().contains("Access is denied")) { + throw new CannotWriteToLocationException("The randomizer cannot write to this location: " + filename); + } else { + throw new RandomizerIOException(e); + } + } + return true; + } + + @Override + public boolean saveRomDirectory(String filename) { + try { + savingROM(); + baseRom.saveAsLayeredFS(filename); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + return true; + } + + protected abstract boolean isGameUpdateSupported(int version); + + @Override + public boolean hasGameUpdateLoaded() { + return gameUpdate != null; + } + + @Override + public boolean loadGameUpdate(String filename) { + String productCode = getProductCodeFromFile(filename); + String titleId = getTitleIdFromFile(filename); + try { + gameUpdate = new NCCH(filename, productCode, titleId); + if (!gameUpdate.isDecrypted()) { + throw new EncryptedROMException(filename); + } + int version = gameUpdate.getVersion(); + if (!this.isGameUpdateSupported(version)) { + System.out.println("Game Update: Supplied unexpected version " + version); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + this.loadedROM(baseRom.getProductCode(), baseRom.getTitleId()); + return true; + } + + @Override + public void removeGameUpdate() { + gameUpdate = null; + this.loadedROM(baseRom.getProductCode(), baseRom.getTitleId()); + } + + protected abstract String getGameVersion(); + + @Override + public String getGameUpdateVersion() { + return getGameVersion(); + } + + @Override + public void printRomDiagnostics(PrintStream logStream) { + baseRom.printRomDiagnostics(logStream, gameUpdate); + } + + public void closeInnerRom() throws IOException { + baseRom.closeROM(); + } + + @Override + public boolean hasPhysicalSpecialSplit() { + // Default value for Gen4+. + // Handlers can override again in case of ROM hacks etc. + return true; + } + + protected byte[] readCode() throws IOException { + if (gameUpdate != null) { + return gameUpdate.getCode(); + } + return baseRom.getCode(); + } + + protected void writeCode(byte[] data) throws IOException { + baseRom.writeCode(data); + } + + protected GARCArchive readGARC(String subpath, boolean skipDecompression) throws IOException { + return new GARCArchive(readFile(subpath),skipDecompression); + } + + protected GARCArchive readGARC(String subpath, List<Boolean> compressThese) throws IOException { + return new GARCArchive(readFile(subpath),compressThese); + } + + protected void writeGARC(String subpath, GARCArchive garc) throws IOException { + this.writeFile(subpath,garc.getBytes()); + } + + protected byte[] readFile(String location) throws IOException { + if (gameUpdate != null && gameUpdate.hasFile(location)) { + return gameUpdate.getFile(location); + } + return baseRom.getFile(location); + } + + protected void writeFile(String location, byte[] data) throws IOException { + writeFile(location, data, 0, data.length); + } + + protected void readByteIntoFlags(byte[] data, boolean[] flags, int offsetIntoFlags, int offsetIntoData) { + int thisByte = data[offsetIntoData] & 0xFF; + for (int i = 0; i < 8 && (i + offsetIntoFlags) < flags.length; i++) { + flags[offsetIntoFlags + i] = ((thisByte >> i) & 0x01) == 0x01; + } + } + + protected byte getByteFromFlags(boolean[] flags, int offsetIntoFlags) { + int thisByte = 0; + for (int i = 0; i < 8 && (i + offsetIntoFlags) < flags.length; i++) { + thisByte |= (flags[offsetIntoFlags + i] ? 1 : 0) << i; + } + return (byte) thisByte; + } + + protected int readWord(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8); + } + + protected void writeWord(byte[] data, int offset, int value) { + data[offset] = (byte) (value & 0xFF); + data[offset + 1] = (byte) ((value >> 8) & 0xFF); + } + + protected int readLong(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8) | ((data[offset + 2] & 0xFF) << 16) + | ((data[offset + 3] & 0xFF) << 24); + } + + protected void writeLong(byte[] data, int offset, int value) { + data[offset] = (byte) (value & 0xFF); + data[offset + 1] = (byte) ((value >> 8) & 0xFF); + data[offset + 2] = (byte) ((value >> 16) & 0xFF); + data[offset + 3] = (byte) ((value >> 24) & 0xFF); + } + + protected void writeFile(String location, byte[] data, int offset, int length) throws IOException { + if (offset != 0 || length != data.length) { + byte[] newData = new byte[length]; + System.arraycopy(data, offset, newData, 0, length); + data = newData; + } + baseRom.writeFile(location, data); + if (gameUpdate != null && gameUpdate.hasFile(location)) { + gameUpdate.writeFile(location, data); + } + } + + public String getTitleIdFromLoadedROM() { + return baseRom.getTitleId(); + } + + protected static String getProductCodeFromFile(String filename) { + try { + long ncchStartingOffset = NCCH.getCXIOffsetInFile(filename); + if (ncchStartingOffset == -1) { + return null; + } + FileInputStream fis = new FileInputStream(filename); + fis.skip(ncchStartingOffset + 0x150); + byte[] productCode = FileFunctions.readFullyIntoBuffer(fis, 0x10); + fis.close(); + return new String(productCode, "UTF-8").trim(); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + public static String getTitleIdFromFile(String filename) { + try { + long ncchStartingOffset = NCCH.getCXIOffsetInFile(filename); + if (ncchStartingOffset == -1) { + return null; + } + FileInputStream fis = new FileInputStream(filename); + fis.skip(ncchStartingOffset + 0x118); + byte[] programId = FileFunctions.readFullyIntoBuffer(fis, 0x8); + fis.close(); + reverseArray(programId); + return bytesToHex(programId); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + private static void reverseArray(byte[] bytes) { + for (int i = 0; i < bytes.length / 2; i++) { + byte temp = bytes[i]; + bytes[i] = bytes[bytes.length - i - 1]; + bytes[bytes.length - i - 1] = temp; + } + } + + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; i++) { + int unsignedByte = bytes[i] & 0xFF; + hexChars[i * 2] = HEX_ARRAY[unsignedByte >>> 4]; + hexChars[i * 2 + 1] = HEX_ARRAY[unsignedByte & 0x0F]; + } + return new String(hexChars); + } + + protected int typeTMPaletteNumber(Type t, boolean isGen7) { + if (t == null) { + return 322; // CURSE + } + switch (t) { + case DARK: + return 309; + case DRAGON: + return 310; + case PSYCHIC: + return 311; + case NORMAL: + return 312; + case POISON: + return 313; + case ICE: + return 314; + case FIGHTING: + return 315; + case FIRE: + return 316; + case WATER: + return 317; + case FLYING: + return 323; + case GRASS: + return 318; + case ROCK: + return 319; + case ELECTRIC: + return 320; + case GROUND: + return 321; + case GHOST: + default: + return 322; // for CURSE + case STEEL: + return 324; + case BUG: + return 325; + case FAIRY: + return isGen7 ? 555 : 546; + } + } +} diff --git a/src/com/pkrandom/romhandlers/AbstractDSRomHandler.java b/src/com/pkrandom/romhandlers/AbstractDSRomHandler.java new file mode 100755 index 0000000..3736758 --- /dev/null +++ b/src/com/pkrandom/romhandlers/AbstractDSRomHandler.java @@ -0,0 +1,390 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- AbstractDSRomHandler.java - a base class for DS rom handlers --*/ +/*-- which standardises common DS functions. --*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import com.pkrandom.FileFunctions; +import com.pkrandom.RomFunctions; +import com.pkrandom.exceptions.CannotWriteToLocationException; +import com.pkrandom.exceptions.RandomizerIOException; +import com.pkrandom.newnds.NARCArchive; +import com.pkrandom.newnds.NDSRom; +import com.pkrandom.pokemon.Type; + +public abstract class AbstractDSRomHandler extends AbstractRomHandler { + + protected String dataFolder; + private NDSRom baseRom; + private String loadedFN; + private boolean arm9Extended = false; + + public AbstractDSRomHandler(Random random, PrintStream logStream) { + super(random, logStream); + } + + protected abstract boolean detectNDSRom(String ndsCode, byte version); + + @Override + public boolean loadRom(String filename) { + if (!this.detectNDSRom(getROMCodeFromFile(filename), getVersionFromFile(filename))) { + return false; + } + // Load inner rom + try { + baseRom = new NDSRom(filename); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + loadedFN = filename; + loadedROM(baseRom.getCode(), baseRom.getVersion()); + return true; + } + + @Override + public String loadedFilename() { + return loadedFN; + } + + protected byte[] get3byte(int amount) { + byte[] ret = new byte[3]; + ret[0] = (byte) (amount & 0xFF); + ret[1] = (byte) ((amount >> 8) & 0xFF); + ret[2] = (byte) ((amount >> 16) & 0xFF); + return ret; + } + + protected abstract void loadedROM(String romCode, byte version); + + protected abstract void savingROM(); + + @Override + public boolean saveRomFile(String filename, long seed) { + savingROM(); + try { + baseRom.saveTo(filename); + } catch (IOException e) { + if (e.getMessage().contains("Access is denied")) { + throw new CannotWriteToLocationException("The randomizer cannot write to this location: " + filename); + } else { + throw new RandomizerIOException(e); + } + } + return true; + } + + @Override + public boolean saveRomDirectory(String filename) { + // do nothing. DS games do have the concept of a filesystem, but it's way more + // convenient for users to use ROM files instead. + return true; + } + + @Override + public boolean hasGameUpdateLoaded() { + return false; + } + + @Override + public boolean loadGameUpdate(String filename) { + // do nothing, as DS games don't have external game updates + return true; + } + + @Override + public void removeGameUpdate() { + // do nothing, as DS games don't have external game updates + } + + @Override + public String getGameUpdateVersion() { + // do nothing, as DS games don't have external game updates + return null; + } + + @Override + public void printRomDiagnostics(PrintStream logStream) { + baseRom.printRomDiagnostics(logStream); + } + + public void closeInnerRom() throws IOException { + baseRom.closeROM(); + } + + @Override + public boolean canChangeStaticPokemon() { + return false; + } + + @Override + public boolean hasPhysicalSpecialSplit() { + // Default value for Gen4+. + // Handlers can override again in case of ROM hacks etc. + return true; + } + + public NARCArchive readNARC(String subpath) throws IOException { + return new NARCArchive(readFile(subpath)); + } + + public void writeNARC(String subpath, NARCArchive narc) throws IOException { + this.writeFile(subpath, narc.getBytes()); + } + + protected static String getROMCodeFromFile(String filename) { + try { + FileInputStream fis = new FileInputStream(filename); + fis.skip(0x0C); + byte[] sig = FileFunctions.readFullyIntoBuffer(fis, 4); + fis.close(); + return new String(sig, "US-ASCII"); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + protected static byte getVersionFromFile(String filename) { + try { + FileInputStream fis = new FileInputStream(filename); + fis.skip(0x1E); + byte[] version = FileFunctions.readFullyIntoBuffer(fis, 1); + fis.close(); + return version[0]; + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + protected int readByte(byte[] data, int offset) { return data[offset] & 0xFF; } + + protected int readWord(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8); + } + + protected int readLong(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8) | ((data[offset + 2] & 0xFF) << 16) + | ((data[offset + 3] & 0xFF) << 24); + } + + protected int readRelativePointer(byte[] data, int offset) { + return readLong(data, offset) + offset + 4; + } + + protected void writeWord(byte[] data, int offset, int value) { + data[offset] = (byte) (value & 0xFF); + data[offset + 1] = (byte) ((value >> 8) & 0xFF); + } + + protected void writeLong(byte[] data, int offset, int value) { + data[offset] = (byte) (value & 0xFF); + data[offset + 1] = (byte) ((value >> 8) & 0xFF); + data[offset + 2] = (byte) ((value >> 16) & 0xFF); + data[offset + 3] = (byte) ((value >> 24) & 0xFF); + } + + protected void writeRelativePointer(byte[] data, int offset, int pointer) { + int relPointer = pointer - (offset + 4); + writeLong(data, offset, relPointer); + } + + protected byte[] readFile(String location) throws IOException { + return baseRom.getFile(location); + } + + protected void writeFile(String location, byte[] data) throws IOException { + writeFile(location, data, 0, data.length); + } + + protected void writeFile(String location, byte[] data, int offset, int length) throws IOException { + if (offset != 0 || length != data.length) { + byte[] newData = new byte[length]; + System.arraycopy(data, offset, newData, 0, length); + data = newData; + } + baseRom.writeFile(location, data); + } + + protected byte[] readARM9() throws IOException { + return baseRom.getARM9(); + } + + protected void writeARM9(byte[] data) throws IOException { + baseRom.writeARM9(data); + } + + protected byte[] readOverlay(int number) throws IOException { + return baseRom.getOverlay(number); + } + + protected void writeOverlay(int number, byte[] data) throws IOException { + baseRom.writeOverlay(number, data); + } + + protected void readByteIntoFlags(byte[] data, boolean[] flags, int offsetIntoFlags, int offsetIntoData) { + int thisByte = data[offsetIntoData] & 0xFF; + for (int i = 0; i < 8 && (i + offsetIntoFlags) < flags.length; i++) { + flags[offsetIntoFlags + i] = ((thisByte >> i) & 0x01) == 0x01; + } + } + + protected byte getByteFromFlags(boolean[] flags, int offsetIntoFlags) { + int thisByte = 0; + for (int i = 0; i < 8 && (i + offsetIntoFlags) < flags.length; i++) { + thisByte |= (flags[offsetIntoFlags + i] ? 1 : 0) << i; + } + return (byte) thisByte; + } + + protected int typeTMPaletteNumber(Type t) { + if (t == null) { + return 411; // CURSE + } + switch (t) { + case FIGHTING: + return 398; + case DRAGON: + return 399; + case WATER: + return 400; + case PSYCHIC: + return 401; + case NORMAL: + return 402; + case POISON: + return 403; + case ICE: + return 404; + case GRASS: + return 405; + case FIRE: + return 406; + case DARK: + return 407; + case STEEL: + return 408; + case ELECTRIC: + return 409; + case GROUND: + return 410; + case GHOST: + default: + return 411; // for CURSE + case ROCK: + return 412; + case FLYING: + return 413; + case BUG: + return 610; + } + } + + 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<Integer> 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); + } + } + + protected byte[] extendARM9(byte[] arm9, int extendBy, String prefix, int arm9Offset) { + /* + Simply extending the ARM9 at the end doesn't work. Towards the end of the ARM9, the following sections exist: + 1. A section that is copied to ITCM (Instruction Tightly Coupled Memory) + 2. A section that is copied to DTCM (Data Tightly Coupled Memory) + 3. Pointers specifying to where these sections should be copied as well as their sizes + + All of these sections are later overwritten(!) and the area is used more or less like a regular RAM area. + This means that if any new code is put after these sections, it will also be overwritten. + Changing which area is overwritten is not viable. There are very many pointers to this area that would need to + be re-indexed. + + Our solution is to extend the section that is to be copied to ITCM, so that any new code gets copied to + ITCM and can be executed from there. This means we have to shift all the data that is after this in order to + make space. Additionally, elsewhere in the ARM9, pointers are stored specifying from where the ITCM + section should be copied, as well as some other data. They are supposedly part of some sort of NDS library + functions and should work the same across games; look for "[SDK+NINTENDO:" in the ARM9 and these pointers should + be slightly before that. They are as follows (each pointer = 4 bytes): + 1. Pointer specifying from where the destination pointers/sizes should be read (see point 3 above) + 2. Pointer specifying the end address of the ARM9. + 3. Pointer specifying from where data copying should start (since ITCM is first, this corresponds to the start + of the section that should be copied to ITCM). + 4. Pointer specifying where data should start being overwritten. (should be identical to #3) + 5. Pointer specifying where data should stop being overwritten (should correspond to start of ovl table). + 6. ??? + + Out of these, we want to change #1 (it will be moved because we have to shift the end of the ARM9 to make space + for enlarging the "copy to ITCM" area) and #2 (since the ARM9 will be made larger). We also want to change the + specified size for the ITCM area since we're enlarging it. + */ + + if (arm9Extended) return arm9; // Don't try to extend the ARM9 more than once + + int tcmCopyingPointersOffset = find(arm9, prefix); + tcmCopyingPointersOffset += prefix.length() / 2; // because it was a prefix + + int oldDestPointersOffset = FileFunctions.readFullInt(arm9, tcmCopyingPointersOffset) - arm9Offset; + int itcmSrcOffset = + FileFunctions.readFullInt(arm9, tcmCopyingPointersOffset + 8) - arm9Offset; + int itcmSizeOffset = oldDestPointersOffset + 4; + int oldITCMSize = FileFunctions.readFullInt(arm9, itcmSizeOffset); + + int oldDTCMOffset = itcmSrcOffset + oldITCMSize; + + byte[] newARM9 = Arrays.copyOf(arm9, arm9.length + extendBy); + + // Change: + // 1. Pointer to destination pointers/sizes + // 2. ARM9 size + // 3. Size of the area copied to ITCM + FileFunctions.writeFullInt(newARM9, tcmCopyingPointersOffset, + oldDestPointersOffset + extendBy + arm9Offset); + FileFunctions.writeFullInt(newARM9, tcmCopyingPointersOffset + 4, + newARM9.length + arm9Offset); + FileFunctions.writeFullInt(newARM9, itcmSizeOffset, oldITCMSize + extendBy); + + // Finally, shift everything + System.arraycopy(newARM9, oldDTCMOffset, newARM9, oldDTCMOffset + extendBy, + arm9.length - oldDTCMOffset); + + arm9Extended = true; + + return newARM9; + } + +} diff --git a/src/com/pkrandom/romhandlers/AbstractGBCRomHandler.java b/src/com/pkrandom/romhandlers/AbstractGBCRomHandler.java new file mode 100644 index 0000000..897b6cd --- /dev/null +++ b/src/com/pkrandom/romhandlers/AbstractGBCRomHandler.java @@ -0,0 +1,224 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- AbstractGBCRomHandler.java - an extension of AbstractGBRomHandler --*/ +/*-- used for Gen 1 and Gen 2. --*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.Scanner; + +import com.pkrandom.FileFunctions; +import com.pkrandom.constants.GBConstants; + +public abstract class AbstractGBCRomHandler extends AbstractGBRomHandler { + + private String[] tb; + private Map<String, Byte> d; + private int longestTableToken; + + public AbstractGBCRomHandler(Random random, PrintStream logStream) { + super(random, logStream); + } + + protected void clearTextTables() { + tb = new String[256]; + if (d != null) { + d.clear(); + } else { + d = new HashMap<String, Byte>(); + } + longestTableToken = 0; + } + + protected void readTextTable(String name) { + try { + Scanner sc = new Scanner(FileFunctions.openConfig(name + ".tbl"), "UTF-8"); + while (sc.hasNextLine()) { + String q = sc.nextLine(); + if (!q.trim().isEmpty()) { + String[] r = q.split("=", 2); + if (r[1].endsWith("\r\n")) { + r[1] = r[1].substring(0, r[1].length() - 2); + } + int hexcode = Integer.parseInt(r[0], 16); + if (tb[hexcode] != null) { + String oldMatch = tb[hexcode]; + tb[hexcode] = null; + if (d.get(oldMatch) == hexcode) { + d.remove(oldMatch); + } + } + tb[hexcode] = r[1]; + longestTableToken = Math.max(longestTableToken, r[1].length()); + d.put(r[1], (byte) hexcode); + } + } + sc.close(); + } catch (FileNotFoundException e) { + } + + } + + protected String readString(int offset, int maxLength, boolean textEngineMode) { + StringBuilder string = new StringBuilder(); + for (int c = 0; c < maxLength; c++) { + int currChar = rom[offset + c] & 0xFF; + if (tb[currChar] != null) { + string.append(tb[currChar]); + if (textEngineMode && (tb[currChar].equals("\\r") || tb[currChar].equals("\\e"))) { + break; + } + } else { + if (currChar == GBConstants.stringTerminator) { + break; + } else { + string.append("\\x" + String.format("%02X", currChar)); + } + } + } + return string.toString(); + } + + protected int lengthOfStringAt(int offset, boolean textEngineMode) { + int len = 0; + while (rom[offset + len] != GBConstants.stringTerminator + && (!textEngineMode || (rom[offset + len] != GBConstants.stringPrintedTextEnd && rom[offset + len] != GBConstants.stringPrintedTextPromptEnd))) { + len++; + } + + if (textEngineMode + && (rom[offset + len] == GBConstants.stringPrintedTextEnd || rom[offset + len] == GBConstants.stringPrintedTextPromptEnd)) { + len++; + } + return len; + } + + protected byte[] translateString(String text) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while (text.length() != 0) { + int i = Math.max(0, longestTableToken - text.length()); + if (text.charAt(0) == '\\' && text.charAt(1) == 'x') { + baos.write(Integer.parseInt(text.substring(2, 4), 16)); + text = text.substring(4); + } else { + while (!(d.containsKey(text.substring(0, longestTableToken - i)) || (i == longestTableToken))) { + i++; + } + if (i == longestTableToken) { + text = text.substring(1); + } else { + baos.write(d.get(text.substring(0, longestTableToken - i)) & 0xFF); + text = text.substring(longestTableToken - i); + } + } + } + return baos.toByteArray(); + } + + protected String readFixedLengthString(int offset, int length) { + return readString(offset, length, false); + } + + // pads the length with terminators, so length should be at least str's len + // + 1 + protected void writeFixedLengthString(String str, int offset, int length) { + byte[] translated = translateString(str); + int len = Math.min(translated.length, length); + System.arraycopy(translated, 0, rom, offset, len); + while (len < length) { + rom[offset + len] = GBConstants.stringTerminator; + len++; + } + } + + protected void writeVariableLengthString(String str, int offset, boolean alreadyTerminated) { + byte[] translated = translateString(str); + System.arraycopy(translated, 0, rom, offset, translated.length); + if (!alreadyTerminated) { + rom[offset + translated.length] = GBConstants.stringTerminator; + } + } + + protected int makeGBPointer(int offset) { + if (offset < GBConstants.bankSize) { + return offset; + } else { + return (offset % GBConstants.bankSize) + GBConstants.bankSize; + } + } + + protected int bankOf(int offset) { + return (offset / GBConstants.bankSize); + } + + protected int calculateOffset(int bank, int pointer) { + if (pointer < GBConstants.bankSize) { + return pointer; + } else { + return (pointer % GBConstants.bankSize) + bank * GBConstants.bankSize; + } + } + + protected String readVariableLengthString(int offset, boolean textEngineMode) { + return readString(offset, Integer.MAX_VALUE, textEngineMode); + } + + protected static boolean romSig(byte[] rom, String sig) { + try { + int sigOffset = GBConstants.romSigOffset; + byte[] sigBytes = sig.getBytes("US-ASCII"); + for (int i = 0; i < sigBytes.length; i++) { + if (rom[sigOffset + i] != sigBytes[i]) { + return false; + } + } + return true; + } catch (UnsupportedEncodingException ex) { + return false; + } + + } + + protected static boolean romCode(byte[] rom, String code) { + try { + int sigOffset = GBConstants.romCodeOffset; + byte[] sigBytes = code.getBytes("US-ASCII"); + for (int i = 0; i < sigBytes.length; i++) { + if (rom[sigOffset + i] != sigBytes[i]) { + return false; + } + } + return true; + } catch (UnsupportedEncodingException ex) { + return false; + } + + } + +} diff --git a/src/com/pkrandom/romhandlers/AbstractGBRomHandler.java b/src/com/pkrandom/romhandlers/AbstractGBRomHandler.java new file mode 100755 index 0000000..a581f3d --- /dev/null +++ b/src/com/pkrandom/romhandlers/AbstractGBRomHandler.java @@ -0,0 +1,210 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- AbstractGBRomHandler.java - a base class for GB/GBA rom handlers --*/ +/*-- which standardises common GB(A) functions.--*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Random; + +import com.pkrandom.FileFunctions; +import com.pkrandom.exceptions.CannotWriteToLocationException; +import com.pkrandom.exceptions.RandomizerIOException; + +public abstract class AbstractGBRomHandler extends AbstractRomHandler { + + protected byte[] rom; + protected byte[] originalRom; + private String loadedFN; + + public AbstractGBRomHandler(Random random, PrintStream logStream) { + super(random, logStream); + } + + @Override + public boolean loadRom(String filename) { + byte[] loaded = loadFile(filename); + if (!detectRom(loaded)) { + return false; + } + this.rom = loaded; + this.originalRom = new byte[rom.length]; + System.arraycopy(rom, 0, originalRom, 0, rom.length); + loadedFN = filename; + loadedRom(); + return true; + } + + @Override + public String loadedFilename() { + return loadedFN; + } + + @Override + public boolean saveRomFile(String filename, long seed) { + savingRom(); + try { + FileOutputStream fos = new FileOutputStream(filename); + fos.write(rom); + fos.close(); + return true; + } catch (IOException ex) { + if (ex.getMessage().contains("Access is denied")) { + throw new CannotWriteToLocationException("The randomizer cannot write to this location: " + filename); + } + return false; + } + } + + @Override + public boolean saveRomDirectory(String filename) { + // do nothing, because GB games don't really have a concept of a filesystem + return true; + } + + @Override + public boolean hasGameUpdateLoaded() { + return false; + } + + @Override + public boolean loadGameUpdate(String filename) { + // do nothing, as GB games don't have external game updates + return true; + } + + @Override + public void removeGameUpdate() { + // do nothing, as GB games don't have external game updates + } + + @Override + public String getGameUpdateVersion() { + // do nothing, as DS games don't have external game updates + return null; + } + + @Override + public void printRomDiagnostics(PrintStream logStream) { + Path p = Paths.get(loadedFN); + logStream.println("File name: " + p.getFileName().toString()); + long crc = FileFunctions.getCRC32(originalRom); + logStream.println("Original ROM CRC32: " + String.format("%08X", crc)); + } + + @Override + public boolean canChangeStaticPokemon() { + return true; + } + + @Override + public boolean hasPhysicalSpecialSplit() { + // Default value for Gen1-Gen3. + // Handlers can override again in case of ROM hacks etc. + return false; + } + + public abstract boolean detectRom(byte[] rom); + + public abstract void loadedRom(); + + public abstract void savingRom(); + + protected static byte[] loadFile(String filename) { + try { + return FileFunctions.readFileFullyIntoBuffer(filename); + } catch (IOException ex) { + throw new RandomizerIOException(ex); + } + } + + protected static byte[] loadFilePartial(String filename, int maxBytes) { + try { + File fh = new File(filename); + if (!fh.exists() || !fh.isFile() || !fh.canRead()) { + return new byte[0]; + } + long fileSize = fh.length(); + if (fileSize > Integer.MAX_VALUE) { + return new byte[0]; + } + FileInputStream fis = new FileInputStream(filename); + byte[] file = FileFunctions.readFullyIntoBuffer(fis, Math.min((int) fileSize, maxBytes)); + fis.close(); + return file; + } catch (IOException ex) { + return new byte[0]; + } + } + + protected void readByteIntoFlags(boolean[] flags, int offsetIntoFlags, int offsetIntoROM) { + int thisByte = rom[offsetIntoROM] & 0xFF; + for (int i = 0; i < 8 && (i + offsetIntoFlags) < flags.length; i++) { + flags[offsetIntoFlags + i] = ((thisByte >> i) & 0x01) == 0x01; + } + } + + protected byte getByteFromFlags(boolean[] flags, int offsetIntoFlags) { + int thisByte = 0; + for (int i = 0; i < 8 && (i + offsetIntoFlags) < flags.length; i++) { + thisByte |= (flags[offsetIntoFlags + i] ? 1 : 0) << i; + } + return (byte) thisByte; + } + + protected int readWord(int offset) { + return readWord(rom, offset); + } + + protected int readWord(byte[] data, int offset) { + return (data[offset] & 0xFF) + ((data[offset + 1] & 0xFF) << 8); + } + + protected void writeWord(int offset, int value) { + writeWord(rom, offset, value); + } + + protected void writeWord(byte[] data, int offset, int value) { + data[offset] = (byte) (value % 0x100); + data[offset + 1] = (byte) ((value / 0x100) % 0x100); + } + + protected boolean matches(byte[] data, int offset, byte[] needle) { + for (int i = 0; i < needle.length; i++) { + if (offset + i >= data.length) { + return false; + } + if (data[offset + i] != needle[i]) { + return false; + } + } + return true; + } + +} diff --git a/src/com/pkrandom/romhandlers/AbstractRomHandler.java b/src/com/pkrandom/romhandlers/AbstractRomHandler.java new file mode 100755 index 0000000..d0c185c --- /dev/null +++ b/src/com/pkrandom/romhandlers/AbstractRomHandler.java @@ -0,0 +1,7558 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- AbstractRomHandler.java - a base class for all rom handlers which --*/ +/*-- implements the majority of the actual --*/ +/*-- randomizer logic by building on the base --*/ +/*-- getters & setters provided by each concrete --*/ +/*-- handler. --*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import java.io.PrintStream; +import java.util.*; +import java.util.stream.Collectors; + +import com.pkrandom.*; +import com.pkrandom.constants.*; +import com.pkrandom.exceptions.RandomizationException; +import com.pkrandom.pokemon.*; + +public abstract class AbstractRomHandler implements RomHandler { + + private boolean restrictionsSet; + protected List<Pokemon> mainPokemonList; + protected List<Pokemon> mainPokemonListInclFormes; + private List<Pokemon> altFormesList; + private List<MegaEvolution> megaEvolutionsList; + private List<Pokemon> noLegendaryList, onlyLegendaryList, ultraBeastList; + private List<Pokemon> noLegendaryListInclFormes, onlyLegendaryListInclFormes; + private List<Pokemon> noLegendaryAltsList, onlyLegendaryAltsList; + private List<Pokemon> pickedStarters; + protected final Random random; + private final Random cosmeticRandom; + protected PrintStream logStream; + private List<Pokemon> alreadyPicked = new ArrayList<>(); + private Map<Pokemon, Integer> placementHistory = new HashMap<>(); + private Map<Integer, Integer> itemPlacementHistory = new HashMap<>(); + private int fullyEvolvedRandomSeed; + boolean isORAS = false; + boolean isSM = false; + int perfectAccuracy = 100; + + /* Constructor */ + + public AbstractRomHandler(Random random, PrintStream logStream) { + this.random = random; + this.cosmeticRandom = RandomSource.cosmeticInstance(); + this.fullyEvolvedRandomSeed = -1; + this.logStream = logStream; + } + + /* + * Public Methods, implemented here for all gens. Unlikely to be overridden. + */ + + public void setLog(PrintStream logStream) { + this.logStream = logStream; + } + + public void setPokemonPool(Settings settings) { + GenRestrictions restrictions = null; + if (settings != null) { + restrictions = settings.getCurrentRestrictions(); + + // restrictions should already be null if "Limit Pokemon" is disabled, but this is a safeguard + if (!settings.isLimitPokemon()) { + restrictions = null; + } + } + + restrictionsSet = true; + mainPokemonList = this.allPokemonWithoutNull(); + mainPokemonListInclFormes = this.allPokemonInclFormesWithoutNull(); + altFormesList = this.getAltFormes(); + megaEvolutionsList = this.getMegaEvolutions(); + if (restrictions != null) { + mainPokemonList = new ArrayList<>(); + mainPokemonListInclFormes = new ArrayList<>(); + megaEvolutionsList = new ArrayList<>(); + List<Pokemon> allPokemon = this.getPokemon(); + + if (restrictions.allow_gen1) { + addPokesFromRange(mainPokemonList, allPokemon, Species.bulbasaur, Species.mew); + } + + if (restrictions.allow_gen2 && allPokemon.size() > Gen2Constants.pokemonCount) { + addPokesFromRange(mainPokemonList, allPokemon, Species.chikorita, Species.celebi); + } + + if (restrictions.allow_gen3 && allPokemon.size() > Gen3Constants.pokemonCount) { + addPokesFromRange(mainPokemonList, allPokemon, Species.treecko, Species.deoxys); + } + + if (restrictions.allow_gen4 && allPokemon.size() > Gen4Constants.pokemonCount) { + addPokesFromRange(mainPokemonList, allPokemon, Species.turtwig, Species.arceus); + } + + if (restrictions.allow_gen5 && allPokemon.size() > Gen5Constants.pokemonCount) { + addPokesFromRange(mainPokemonList, allPokemon, Species.victini, Species.genesect); + } + + if (restrictions.allow_gen6 && allPokemon.size() > Gen6Constants.pokemonCount) { + addPokesFromRange(mainPokemonList, allPokemon, Species.chespin, Species.volcanion); + } + + int maxGen7SpeciesID = isSM ? Species.marshadow : Species.zeraora; + if (restrictions.allow_gen7 && allPokemon.size() > maxGen7SpeciesID) { + addPokesFromRange(mainPokemonList, allPokemon, Species.rowlet, maxGen7SpeciesID); + } + + // If the user specified it, add all the evolutionary relatives for everything in the mainPokemonList + if (restrictions.allow_evolutionary_relatives) { + addEvolutionaryRelatives(mainPokemonList); + } + + // Now that mainPokemonList has all the selected Pokemon, update mainPokemonListInclFormes too + addAllPokesInclFormes(mainPokemonList, mainPokemonListInclFormes); + + // Populate megaEvolutionsList with all of the mega evolutions that exist in the pool + List<MegaEvolution> allMegaEvolutions = this.getMegaEvolutions(); + for (MegaEvolution megaEvo : allMegaEvolutions) { + if (mainPokemonListInclFormes.contains(megaEvo.to)) { + megaEvolutionsList.add(megaEvo); + } + } + } + + noLegendaryList = new ArrayList<>(); + noLegendaryListInclFormes = new ArrayList<>(); + onlyLegendaryList = new ArrayList<>(); + onlyLegendaryListInclFormes = new ArrayList<>(); + noLegendaryAltsList = new ArrayList<>(); + onlyLegendaryAltsList = new ArrayList<>(); + ultraBeastList = new ArrayList<>(); + + for (Pokemon p : mainPokemonList) { + if (p.isLegendary()) { + onlyLegendaryList.add(p); + } else if (p.isUltraBeast()) { + ultraBeastList.add(p); + } else { + noLegendaryList.add(p); + } + } + for (Pokemon p : mainPokemonListInclFormes) { + if (p.isLegendary()) { + onlyLegendaryListInclFormes.add(p); + } else if (!ultraBeastList.contains(p)) { + noLegendaryListInclFormes.add(p); + } + } + for (Pokemon f : altFormesList) { + if (f.isLegendary()) { + onlyLegendaryAltsList.add(f); + } else { + noLegendaryAltsList.add(f); + } + } + } + + private void addPokesFromRange(List<Pokemon> pokemonPool, List<Pokemon> allPokemon, int range_min, int range_max) { + for (int i = range_min; i <= range_max; i++) { + if (!pokemonPool.contains(allPokemon.get(i))) { + pokemonPool.add(allPokemon.get(i)); + } + } + } + + private void addEvolutionaryRelatives(List<Pokemon> pokemonPool) { + Set<Pokemon> newPokemon = new TreeSet<>(); + for (Pokemon pk : pokemonPool) { + List<Pokemon> evolutionaryRelatives = getEvolutionaryRelatives(pk); + for (Pokemon relative : evolutionaryRelatives) { + if (!pokemonPool.contains(relative) && !newPokemon.contains(relative)) { + newPokemon.add(relative); + } + } + } + + pokemonPool.addAll(newPokemon); + } + + private void addAllPokesInclFormes(List<Pokemon> pokemonPool, List<Pokemon> pokemonPoolInclFormes) { + List<Pokemon> altFormes = this.getAltFormes(); + for (int i = 0; i < pokemonPool.size(); i++) { + Pokemon currentPokemon = pokemonPool.get(i); + if (!pokemonPoolInclFormes.contains(currentPokemon)) { + pokemonPoolInclFormes.add(currentPokemon); + } + for (int j = 0; j < altFormes.size(); j++) { + Pokemon potentialAltForme = altFormes.get(j); + if (potentialAltForme.baseForme != null && potentialAltForme.baseForme.number == currentPokemon.number) { + pokemonPoolInclFormes.add(potentialAltForme); + } + } + } + } + + @Override + public void shufflePokemonStats(Settings settings) { + boolean evolutionSanity = settings.isBaseStatsFollowEvolutions(); + boolean megaEvolutionSanity = settings.isBaseStatsFollowMegaEvolutions(); + + if (evolutionSanity) { + copyUpEvolutionsHelper(pk -> pk.shuffleStats(AbstractRomHandler.this.random), + (evFrom, evTo, toMonIsFinalEvo) -> evTo.copyShuffledStatsUpEvolution(evFrom) + ); + } else { + List<Pokemon> allPokes = this.getPokemonInclFormes(); + for (Pokemon pk : allPokes) { + if (pk != null) { + pk.shuffleStats(this.random); + } + } + } + + List<Pokemon> allPokes = this.getPokemonInclFormes(); + for (Pokemon pk : allPokes) { + if (pk != null && pk.actuallyCosmetic) { + pk.copyBaseFormeBaseStats(pk.baseForme); + } + } + + if (megaEvolutionSanity) { + List<MegaEvolution> allMegaEvos = getMegaEvolutions(); + for (MegaEvolution megaEvo: allMegaEvos) { + if (megaEvo.from.megaEvolutionsFrom.size() > 1) continue; + megaEvo.to.copyShuffledStatsUpEvolution(megaEvo.from); + } + } + } + + @Override + public void randomizePokemonStats(Settings settings) { + boolean evolutionSanity = settings.isBaseStatsFollowEvolutions(); + boolean megaEvolutionSanity = settings.isBaseStatsFollowMegaEvolutions(); + boolean assignEvoStatsRandomly = settings.isAssignEvoStatsRandomly(); + + if (evolutionSanity) { + if (assignEvoStatsRandomly) { + copyUpEvolutionsHelper(pk -> pk.randomizeStatsWithinBST(AbstractRomHandler.this.random), + (evFrom, evTo, toMonIsFinalEvo) -> evTo.assignNewStatsForEvolution(evFrom, this.random), + (evFrom, evTo, toMonIsFinalEvo) -> evTo.assignNewStatsForEvolution(evFrom, this.random), + true + ); + } else { + copyUpEvolutionsHelper(pk -> pk.randomizeStatsWithinBST(AbstractRomHandler.this.random), + (evFrom, evTo, toMonIsFinalEvo) -> evTo.copyRandomizedStatsUpEvolution(evFrom), + (evFrom, evTo, toMonIsFinalEvo) -> evTo.assignNewStatsForEvolution(evFrom, this.random), + true + ); + } + } else { + List<Pokemon> allPokes = this.getPokemonInclFormes(); + for (Pokemon pk : allPokes) { + if (pk != null) { + pk.randomizeStatsWithinBST(this.random); + } + } + } + + List<Pokemon> allPokes = this.getPokemonInclFormes(); + for (Pokemon pk : allPokes) { + if (pk != null && pk.actuallyCosmetic) { + pk.copyBaseFormeBaseStats(pk.baseForme); + } + } + + if (megaEvolutionSanity) { + List<MegaEvolution> allMegaEvos = getMegaEvolutions(); + for (MegaEvolution megaEvo: allMegaEvos) { + if (megaEvo.from.megaEvolutionsFrom.size() > 1 || assignEvoStatsRandomly) { + megaEvo.to.assignNewStatsForEvolution(megaEvo.from, this.random); + } else { + megaEvo.to.copyRandomizedStatsUpEvolution(megaEvo.from); + } + } + } + } + + @Override + public void updatePokemonStats(Settings settings) { + int generation = settings.getUpdateBaseStatsToGeneration(); + + List<Pokemon> pokes = getPokemonInclFormes(); + + for (int gen = 6; gen <= generation; gen++) { + Map<Integer,StatChange> statChanges = getUpdatedPokemonStats(gen); + + for (int i = 1; i < pokes.size(); i++) { + StatChange changedStats = statChanges.get(i); + if (changedStats != null) { + int statNum = 0; + if ((changedStats.stat & Stat.HP.val) != 0) { + pokes.get(i).hp = changedStats.values[statNum]; + statNum++; + } + if ((changedStats.stat & Stat.ATK.val) != 0) { + pokes.get(i).attack = changedStats.values[statNum]; + statNum++; + } + if ((changedStats.stat & Stat.DEF.val) != 0) { + pokes.get(i).defense = changedStats.values[statNum]; + statNum++; + } + if ((changedStats.stat & Stat.SPATK.val) != 0) { + if (generationOfPokemon() != 1) { + pokes.get(i).spatk = changedStats.values[statNum]; + } + statNum++; + } + if ((changedStats.stat & Stat.SPDEF.val) != 0) { + if (generationOfPokemon() != 1) { + pokes.get(i).spdef = changedStats.values[statNum]; + } + statNum++; + } + if ((changedStats.stat & Stat.SPEED.val) != 0) { + pokes.get(i).speed = changedStats.values[statNum]; + statNum++; + } + if ((changedStats.stat & Stat.SPECIAL.val) != 0) { + pokes.get(i).special = changedStats.values[statNum]; + } + } + } + } + } + + public Pokemon randomPokemon() { + checkPokemonRestrictions(); + return mainPokemonList.get(this.random.nextInt(mainPokemonList.size())); + } + + @Override + public Pokemon randomPokemonInclFormes() { + checkPokemonRestrictions(); + return mainPokemonListInclFormes.get(this.random.nextInt(mainPokemonListInclFormes.size())); + } + + @Override + public Pokemon randomNonLegendaryPokemon() { + checkPokemonRestrictions(); + return noLegendaryList.get(this.random.nextInt(noLegendaryList.size())); + } + + private Pokemon randomNonLegendaryPokemonInclFormes() { + checkPokemonRestrictions(); + return noLegendaryListInclFormes.get(this.random.nextInt(noLegendaryListInclFormes.size())); + } + + @Override + public Pokemon randomLegendaryPokemon() { + checkPokemonRestrictions(); + return onlyLegendaryList.get(this.random.nextInt(onlyLegendaryList.size())); + } + + private List<Pokemon> twoEvoPokes; + + @Override + public Pokemon random2EvosPokemon(boolean allowAltFormes) { + if (twoEvoPokes == null) { + // Prepare the list + twoEvoPokes = new ArrayList<>(); + List<Pokemon> allPokes = + allowAltFormes ? + this.getPokemonInclFormes() + .stream() + .filter(pk -> pk == null || !pk.actuallyCosmetic) + .collect(Collectors.toList()) : + this.getPokemon(); + for (Pokemon pk : allPokes) { + if (pk != null && pk.evolutionsTo.size() == 0 && pk.evolutionsFrom.size() > 0) { + // Potential candidate + for (Evolution ev : pk.evolutionsFrom) { + // If any of the targets here evolve, the original + // Pokemon has 2+ stages. + if (ev.to.evolutionsFrom.size() > 0) { + twoEvoPokes.add(pk); + break; + } + } + } + } + } + return twoEvoPokes.get(this.random.nextInt(twoEvoPokes.size())); + } + + @Override + public Type randomType() { + Type t = Type.randomType(this.random); + while (!typeInGame(t)) { + t = Type.randomType(this.random); + } + return t; + } + + @Override + public void randomizePokemonTypes(Settings settings) { + boolean evolutionSanity = settings.getTypesMod() == Settings.TypesMod.RANDOM_FOLLOW_EVOLUTIONS; + boolean megaEvolutionSanity = settings.isTypesFollowMegaEvolutions(); + boolean dualTypeOnly = settings.isDualTypeOnly(); + + List<Pokemon> allPokes = this.getPokemonInclFormes(); + if (evolutionSanity) { + // Type randomization with evolution sanity + copyUpEvolutionsHelper(pk -> { + // Step 1: Basic or Excluded From Copying Pokemon + // A Basic/EFC pokemon has a 35% chance of a second type if + // it has an evolution that copies type/stats, a 50% chance + // otherwise + pk.primaryType = randomType(); + pk.secondaryType = null; + if (pk.evolutionsFrom.size() == 1 && pk.evolutionsFrom.get(0).carryStats) { + if (AbstractRomHandler.this.random.nextDouble() < 0.35 || dualTypeOnly) { + pk.secondaryType = randomType(); + while (pk.secondaryType == pk.primaryType) { + pk.secondaryType = randomType(); + } + } + } else { + if (AbstractRomHandler.this.random.nextDouble() < 0.5 || dualTypeOnly) { + pk.secondaryType = randomType(); + while (pk.secondaryType == pk.primaryType) { + pk.secondaryType = randomType(); + } + } + } + }, (evFrom, evTo, toMonIsFinalEvo) -> { + evTo.primaryType = evFrom.primaryType; + evTo.secondaryType = evFrom.secondaryType; + + if (evTo.secondaryType == null) { + double chance = toMonIsFinalEvo ? 0.25 : 0.15; + if (AbstractRomHandler.this.random.nextDouble() < chance || dualTypeOnly) { + evTo.secondaryType = randomType(); + while (evTo.secondaryType == evTo.primaryType) { + evTo.secondaryType = randomType(); + } + } + } + }); + } else { + // Entirely random types + for (Pokemon pkmn : allPokes) { + if (pkmn != null) { + pkmn.primaryType = randomType(); + pkmn.secondaryType = null; + if (this.random.nextDouble() < 0.5||settings.isDualTypeOnly()) { + pkmn.secondaryType = randomType(); + while (pkmn.secondaryType == pkmn.primaryType) { + pkmn.secondaryType = randomType(); + } + } + } + } + } + + for (Pokemon pk : allPokes) { + if (pk != null && pk.actuallyCosmetic) { + pk.primaryType = pk.baseForme.primaryType; + pk.secondaryType = pk.baseForme.secondaryType; + } + } + + if (megaEvolutionSanity) { + List<MegaEvolution> allMegaEvos = getMegaEvolutions(); + for (MegaEvolution megaEvo: allMegaEvos) { + if (megaEvo.from.megaEvolutionsFrom.size() > 1) continue; + megaEvo.to.primaryType = megaEvo.from.primaryType; + megaEvo.to.secondaryType = megaEvo.from.secondaryType; + + if (megaEvo.to.secondaryType == null) { + if (this.random.nextDouble() < 0.25) { + megaEvo.to.secondaryType = randomType(); + while (megaEvo.to.secondaryType == megaEvo.to.primaryType) { + megaEvo.to.secondaryType = randomType(); + } + } + } + } + } + } + + @Override + public void randomizeAbilities(Settings settings) { + boolean evolutionSanity = settings.isAbilitiesFollowEvolutions(); + boolean allowWonderGuard = settings.isAllowWonderGuard(); + boolean banTrappingAbilities = settings.isBanTrappingAbilities(); + boolean banNegativeAbilities = settings.isBanNegativeAbilities(); + boolean banBadAbilities = settings.isBanBadAbilities(); + boolean megaEvolutionSanity = settings.isAbilitiesFollowMegaEvolutions(); + boolean weighDuplicatesTogether = settings.isWeighDuplicateAbilitiesTogether(); + boolean ensureTwoAbilities = settings.isEnsureTwoAbilities(); + boolean doubleBattleMode = settings.isDoubleBattleMode(); + + // Abilities don't exist in some games... + if (this.abilitiesPerPokemon() == 0) { + return; + } + + final boolean hasDWAbilities = (this.abilitiesPerPokemon() == 3); + + final List<Integer> bannedAbilities = this.getUselessAbilities(); + + if (!allowWonderGuard) { + bannedAbilities.add(Abilities.wonderGuard); + } + + if (banTrappingAbilities) { + bannedAbilities.addAll(GlobalConstants.battleTrappingAbilities); + } + + if (banNegativeAbilities) { + bannedAbilities.addAll(GlobalConstants.negativeAbilities); + } + + if (banBadAbilities) { + bannedAbilities.addAll(GlobalConstants.badAbilities); + if (!doubleBattleMode) { + bannedAbilities.addAll(GlobalConstants.doubleBattleAbilities); + } + } + + if (weighDuplicatesTogether) { + bannedAbilities.addAll(GlobalConstants.duplicateAbilities); + if (generationOfPokemon() == 3) { + bannedAbilities.add(Gen3Constants.airLockIndex); // Special case for Air Lock in Gen 3 + } + } + + final int maxAbility = this.highestAbilityIndex(); + + if (evolutionSanity) { + // copy abilities straight up evolution lines + // still keep WG as an exception, though + + copyUpEvolutionsHelper(pk -> { + if (pk.ability1 != Abilities.wonderGuard + && pk.ability2 != Abilities.wonderGuard + && pk.ability3 != Abilities.wonderGuard) { + // Pick first ability + pk.ability1 = pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether); + + // Second ability? + if (ensureTwoAbilities || AbstractRomHandler.this.random.nextDouble() < 0.5) { + // Yes, second ability + pk.ability2 = pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether, + pk.ability1); + } else { + // Nope + pk.ability2 = 0; + } + + // Third ability? + if (hasDWAbilities) { + pk.ability3 = pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether, + pk.ability1, pk.ability2); + } + } + }, (evFrom, evTo, toMonIsFinalEvo) -> { + if (evTo.ability1 != Abilities.wonderGuard + && evTo.ability2 != Abilities.wonderGuard + && evTo.ability3 != Abilities.wonderGuard) { + evTo.ability1 = evFrom.ability1; + evTo.ability2 = evFrom.ability2; + evTo.ability3 = evFrom.ability3; + } + }); + } else { + List<Pokemon> allPokes = this.getPokemonInclFormes(); + for (Pokemon pk : allPokes) { + if (pk == null) { + continue; + } + + // Don't remove WG if already in place. + if (pk.ability1 != Abilities.wonderGuard + && pk.ability2 != Abilities.wonderGuard + && pk.ability3 != Abilities.wonderGuard) { + // Pick first ability + pk.ability1 = this.pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether); + + // Second ability? + if (ensureTwoAbilities || this.random.nextDouble() < 0.5) { + // Yes, second ability + pk.ability2 = this.pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether, + pk.ability1); + } else { + // Nope + pk.ability2 = 0; + } + + // Third ability? + if (hasDWAbilities) { + pk.ability3 = pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether, + pk.ability1, pk.ability2); + } + } + } + } + + List<Pokemon> allPokes = this.getPokemonInclFormes(); + for (Pokemon pk : allPokes) { + if (pk != null && pk.actuallyCosmetic) { + pk.copyBaseFormeAbilities(pk.baseForme); + } + } + + if (megaEvolutionSanity) { + List<MegaEvolution> allMegaEvos = this.getMegaEvolutions(); + for (MegaEvolution megaEvo: allMegaEvos) { + if (megaEvo.from.megaEvolutionsFrom.size() > 1) continue; + megaEvo.to.ability1 = megaEvo.from.ability1; + megaEvo.to.ability2 = megaEvo.from.ability2; + megaEvo.to.ability3 = megaEvo.from.ability3; + } + } + } + + private int pickRandomAbilityVariation(int selectedAbility, int... alreadySetAbilities) { + int newAbility = selectedAbility; + + while (true) { + Map<Integer, List<Integer>> abilityVariations = getAbilityVariations(); + for (int baseAbility: abilityVariations.keySet()) { + if (selectedAbility == baseAbility) { + List<Integer> variationsForThisAbility = abilityVariations.get(selectedAbility); + newAbility = variationsForThisAbility.get(this.random.nextInt(variationsForThisAbility.size())); + break; + } + } + + boolean repeat = false; + for (int alreadySetAbility : alreadySetAbilities) { + if (alreadySetAbility == newAbility) { + repeat = true; + break; + } + } + + if (!repeat) { + break; + } + + + } + + return newAbility; + } + + private int pickRandomAbility(int maxAbility, List<Integer> bannedAbilities, boolean useVariations, + int... alreadySetAbilities) { + int newAbility; + + while (true) { + newAbility = this.random.nextInt(maxAbility) + 1; + + if (bannedAbilities.contains(newAbility)) { + continue; + } + + boolean repeat = false; + for (int alreadySetAbility : alreadySetAbilities) { + if (alreadySetAbility == newAbility) { + repeat = true; + break; + } + } + + if (!repeat) { + if (useVariations) { + newAbility = pickRandomAbilityVariation(newAbility, alreadySetAbilities); + } + break; + } + } + + return newAbility; + } + + @Override + public void randomEncounters(Settings settings) { + boolean useTimeOfDay = settings.isUseTimeBasedEncounters(); + boolean catchEmAll = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.CATCH_EM_ALL; + boolean typeThemed = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.TYPE_THEME_AREAS; + boolean usePowerLevels = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.SIMILAR_STRENGTH; + boolean noLegendaries = settings.isBlockWildLegendaries(); + boolean balanceShakingGrass = settings.isBalanceShakingGrass(); + int levelModifier = settings.isWildLevelsModified() ? settings.getWildLevelModifier() : 0; + boolean allowAltFormes = settings.isAllowWildAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + + List<EncounterSet> currentEncounters = this.getEncounters(useTimeOfDay); + + if (isORAS) { + List<EncounterSet> collapsedEncounters = collapseAreasORAS(currentEncounters); + area1to1EncountersImpl(collapsedEncounters, settings); + enhanceRandomEncountersORAS(collapsedEncounters, settings); + setEncounters(useTimeOfDay, currentEncounters); + return; + } + + checkPokemonRestrictions(); + + // New: randomize the order encounter sets are randomized in. + // Leads to less predictable results for various modifiers. + // Need to keep the original ordering around for saving though. + List<EncounterSet> scrambledEncounters = new ArrayList<>(currentEncounters); + Collections.shuffle(scrambledEncounters, this.random); + + List<Pokemon> banned = this.bannedForWildEncounters(); + banned.addAll(this.getBannedFormesForPlayerPokemon()); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + // Assume EITHER catch em all OR type themed OR match strength for now + if (catchEmAll) { + List<Pokemon> allPokes; + if (allowAltFormes) { + allPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) : new ArrayList<>( + mainPokemonListInclFormes); + allPokes.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + allPokes = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>( + mainPokemonList); + } + allPokes.removeAll(banned); + + for (EncounterSet area : scrambledEncounters) { + List<Pokemon> pickablePokemon = allPokes; + if (area.bannedPokemon.size() > 0) { + pickablePokemon = new ArrayList<>(allPokes); + pickablePokemon.removeAll(area.bannedPokemon); + } + for (Encounter enc : area.encounters) { + // In Catch 'Em All mode, don't randomize encounters for Pokemon that are banned for + // wild encounters. Otherwise, it may be impossible to obtain this Pokemon unless it + // randomly appears as a static or unless it becomes a random evolution. + if (banned.contains(enc.pokemon)) { + continue; + } + + // Pick a random pokemon + if (pickablePokemon.size() == 0) { + // Only banned pokes are left, ignore them and pick + // something else for now. + List<Pokemon> tempPickable; + if (allowAltFormes) { + tempPickable = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) : new ArrayList<>( + mainPokemonListInclFormes); + tempPickable.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + tempPickable = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>( + mainPokemonList); + } + tempPickable.removeAll(banned); + tempPickable.removeAll(area.bannedPokemon); + if (tempPickable.size() == 0) { + throw new RandomizationException("ERROR: Couldn't replace a wild Pokemon!"); + } + int picked = this.random.nextInt(tempPickable.size()); + enc.pokemon = tempPickable.get(picked); + setFormeForEncounter(enc, enc.pokemon); + } else { + // Picked this Pokemon, remove it + int picked = this.random.nextInt(pickablePokemon.size()); + enc.pokemon = pickablePokemon.get(picked); + pickablePokemon.remove(picked); + if (allPokes != pickablePokemon) { + allPokes.remove(enc.pokemon); + } + setFormeForEncounter(enc, enc.pokemon); + if (allPokes.size() == 0) { + // Start again + if (allowAltFormes) { + allPokes.addAll(noLegendaries ? noLegendaryListInclFormes : mainPokemonListInclFormes); + allPokes.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + allPokes.addAll(noLegendaries ? noLegendaryList : mainPokemonList); + } + allPokes.removeAll(banned); + if (pickablePokemon != allPokes) { + pickablePokemon.addAll(allPokes); + pickablePokemon.removeAll(area.bannedPokemon); + } + } + } + } + } + } else if (typeThemed) { + Map<Type, List<Pokemon>> cachedPokeLists = new TreeMap<>(); + for (EncounterSet area : scrambledEncounters) { + List<Pokemon> possiblePokemon = null; + int iterLoops = 0; + while (possiblePokemon == null && iterLoops < 10000) { + Type areaTheme = randomType(); + if (!cachedPokeLists.containsKey(areaTheme)) { + List<Pokemon> pType = allowAltFormes ? pokemonOfTypeInclFormes(areaTheme, noLegendaries) : + pokemonOfType(areaTheme, noLegendaries); + pType.removeAll(banned); + cachedPokeLists.put(areaTheme, pType); + } + possiblePokemon = cachedPokeLists.get(areaTheme); + if (area.bannedPokemon.size() > 0) { + possiblePokemon = new ArrayList<>(possiblePokemon); + possiblePokemon.removeAll(area.bannedPokemon); + } + if (possiblePokemon.size() == 0) { + // Can't use this type for this area + possiblePokemon = null; + } + iterLoops++; + } + if (possiblePokemon == null) { + throw new RandomizationException("Could not randomize an area in a reasonable amount of attempts."); + } + for (Encounter enc : area.encounters) { + // Pick a random themed pokemon + enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size())); + while (enc.pokemon.actuallyCosmetic) { + enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size())); + } + setFormeForEncounter(enc, enc.pokemon); + } + } + } else if (usePowerLevels) { + List<Pokemon> allowedPokes; + if (allowAltFormes) { + allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) + : new ArrayList<>(mainPokemonListInclFormes); + } else { + allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryList) + : new ArrayList<>(mainPokemonList); + } + allowedPokes.removeAll(banned); + for (EncounterSet area : scrambledEncounters) { + List<Pokemon> localAllowed = allowedPokes; + if (area.bannedPokemon.size() > 0) { + localAllowed = new ArrayList<>(allowedPokes); + localAllowed.removeAll(area.bannedPokemon); + } + for (Encounter enc : area.encounters) { + if (balanceShakingGrass) { + if (area.displayName.contains("Shaking")) { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, (enc.level + enc.maxLevel) / 2); + while (enc.pokemon.actuallyCosmetic) { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, (enc.level + enc.maxLevel) / 2); + } + setFormeForEncounter(enc, enc.pokemon); + } else { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100); + while (enc.pokemon.actuallyCosmetic) { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100); + } + setFormeForEncounter(enc, enc.pokemon); + } + } else { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100); + while (enc.pokemon.actuallyCosmetic) { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100); + } + setFormeForEncounter(enc, enc.pokemon); + } + } + } + } else { + // Entirely random + for (EncounterSet area : scrambledEncounters) { + for (Encounter enc : area.encounters) { + enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned); + setFormeForEncounter(enc, enc.pokemon); + } + } + } + if (levelModifier != 0) { + for (EncounterSet area : currentEncounters) { + for (Encounter enc : area.encounters) { + enc.level = Math.min(100, (int) Math.round(enc.level * (1 + levelModifier / 100.0))); + enc.maxLevel = Math.min(100, (int) Math.round(enc.maxLevel * (1 + levelModifier / 100.0))); + } + } + } + + setEncounters(useTimeOfDay, currentEncounters); + } + + @Override + public void area1to1Encounters(Settings settings) { + boolean useTimeOfDay = settings.isUseTimeBasedEncounters(); + + List<EncounterSet> currentEncounters = this.getEncounters(useTimeOfDay); + if (isORAS) { + List<EncounterSet> collapsedEncounters = collapseAreasORAS(currentEncounters); + area1to1EncountersImpl(collapsedEncounters, settings); + setEncounters(useTimeOfDay, currentEncounters); + return; + } else { + area1to1EncountersImpl(currentEncounters, settings); + setEncounters(useTimeOfDay, currentEncounters); + } + } + + private void area1to1EncountersImpl(List<EncounterSet> currentEncounters, Settings settings) { + boolean catchEmAll = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.CATCH_EM_ALL; + boolean typeThemed = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.TYPE_THEME_AREAS; + boolean usePowerLevels = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.SIMILAR_STRENGTH; + boolean noLegendaries = settings.isBlockWildLegendaries(); + int levelModifier = settings.isWildLevelsModified() ? settings.getWildLevelModifier() : 0; + boolean allowAltFormes = settings.isAllowWildAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + + checkPokemonRestrictions(); + List<Pokemon> banned = this.bannedForWildEncounters(); + banned.addAll(this.getBannedFormesForPlayerPokemon()); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + + // New: randomize the order encounter sets are randomized in. + // Leads to less predictable results for various modifiers. + // Need to keep the original ordering around for saving though. + List<EncounterSet> scrambledEncounters = new ArrayList<>(currentEncounters); + Collections.shuffle(scrambledEncounters, this.random); + + // Assume EITHER catch em all OR type themed for now + if (catchEmAll) { + List<Pokemon> allPokes; + if (allowAltFormes) { + allPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) : new ArrayList<>( + mainPokemonListInclFormes); + allPokes.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + allPokes = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>( + mainPokemonList); + } + allPokes.removeAll(banned); + for (EncounterSet area : scrambledEncounters) { + Set<Pokemon> inArea = pokemonInArea(area); + // Build area map using catch em all + Map<Pokemon, Pokemon> areaMap = new TreeMap<>(); + List<Pokemon> pickablePokemon = allPokes; + if (area.bannedPokemon.size() > 0) { + pickablePokemon = new ArrayList<>(allPokes); + pickablePokemon.removeAll(area.bannedPokemon); + } + for (Pokemon areaPk : inArea) { + if (pickablePokemon.size() == 0) { + // No more pickable pokes left, take a random one + List<Pokemon> tempPickable; + if (allowAltFormes) { + tempPickable = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) : new ArrayList<>( + mainPokemonListInclFormes); + tempPickable.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + tempPickable = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>( + mainPokemonList); + } + tempPickable.removeAll(banned); + tempPickable.removeAll(area.bannedPokemon); + if (tempPickable.size() == 0) { + throw new RandomizationException("ERROR: Couldn't replace a wild Pokemon!"); + } + int picked = this.random.nextInt(tempPickable.size()); + Pokemon pickedMN = tempPickable.get(picked); + areaMap.put(areaPk, pickedMN); + } else { + int picked = this.random.nextInt(allPokes.size()); + Pokemon pickedMN = allPokes.get(picked); + areaMap.put(areaPk, pickedMN); + pickablePokemon.remove(pickedMN); + if (allPokes != pickablePokemon) { + allPokes.remove(pickedMN); + } + if (allPokes.size() == 0) { + // Start again + if (allowAltFormes) { + allPokes.addAll(noLegendaries ? noLegendaryListInclFormes : mainPokemonListInclFormes); + allPokes.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + allPokes.addAll(noLegendaries ? noLegendaryList : mainPokemonList); + } + allPokes.removeAll(banned); + if (pickablePokemon != allPokes) { + pickablePokemon.addAll(allPokes); + pickablePokemon.removeAll(area.bannedPokemon); + } + } + } + } + for (Encounter enc : area.encounters) { + // In Catch 'Em All mode, don't randomize encounters for Pokemon that are banned for + // wild encounters. Otherwise, it may be impossible to obtain this Pokemon unless it + // randomly appears as a static or unless it becomes a random evolution. + if (banned.contains(enc.pokemon)) { + continue; + } + // Apply the map + enc.pokemon = areaMap.get(enc.pokemon); + setFormeForEncounter(enc, enc.pokemon); + } + } + } else if (typeThemed) { + Map<Type, List<Pokemon>> cachedPokeLists = new TreeMap<>(); + for (EncounterSet area : scrambledEncounters) { + // Poke-set + Set<Pokemon> inArea = pokemonInArea(area); + List<Pokemon> possiblePokemon = null; + int iterLoops = 0; + while (possiblePokemon == null && iterLoops < 10000) { + Type areaTheme = randomType(); + if (!cachedPokeLists.containsKey(areaTheme)) { + List<Pokemon> pType = allowAltFormes ? pokemonOfTypeInclFormes(areaTheme, noLegendaries) : + pokemonOfType(areaTheme, noLegendaries); + pType.removeAll(banned); + cachedPokeLists.put(areaTheme, pType); + } + possiblePokemon = new ArrayList<>(cachedPokeLists.get(areaTheme)); + if (area.bannedPokemon.size() > 0) { + possiblePokemon.removeAll(area.bannedPokemon); + } + if (possiblePokemon.size() < inArea.size()) { + // Can't use this type for this area + possiblePokemon = null; + } + iterLoops++; + } + if (possiblePokemon == null) { + throw new RandomizationException("Could not randomize an area in a reasonable amount of attempts."); + } + + // Build area map using type theme. + Map<Pokemon, Pokemon> areaMap = new TreeMap<>(); + for (Pokemon areaPk : inArea) { + int picked = this.random.nextInt(possiblePokemon.size()); + Pokemon pickedMN = possiblePokemon.get(picked); + while (pickedMN.actuallyCosmetic) { + picked = this.random.nextInt(possiblePokemon.size()); + pickedMN = possiblePokemon.get(picked); + } + areaMap.put(areaPk, pickedMN); + possiblePokemon.remove(picked); + } + for (Encounter enc : area.encounters) { + // Apply the map + enc.pokemon = areaMap.get(enc.pokemon); + setFormeForEncounter(enc, enc.pokemon); + } + } + } else if (usePowerLevels) { + List<Pokemon> allowedPokes; + if (allowAltFormes) { + allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) + : new ArrayList<>(mainPokemonListInclFormes); + } else { + allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryList) + : new ArrayList<>(mainPokemonList); + } + allowedPokes.removeAll(banned); + for (EncounterSet area : scrambledEncounters) { + // Poke-set + Set<Pokemon> inArea = pokemonInArea(area); + // Build area map using randoms + Map<Pokemon, Pokemon> areaMap = new TreeMap<>(); + List<Pokemon> usedPks = new ArrayList<>(); + List<Pokemon> localAllowed = allowedPokes; + if (area.bannedPokemon.size() > 0) { + localAllowed = new ArrayList<>(allowedPokes); + localAllowed.removeAll(area.bannedPokemon); + } + for (Pokemon areaPk : inArea) { + Pokemon picked = pickWildPowerLvlReplacement(localAllowed, areaPk, false, usedPks, 100); + while (picked.actuallyCosmetic) { + picked = pickWildPowerLvlReplacement(localAllowed, areaPk, false, usedPks, 100); + } + areaMap.put(areaPk, picked); + usedPks.add(picked); + } + for (Encounter enc : area.encounters) { + // Apply the map + enc.pokemon = areaMap.get(enc.pokemon); + setFormeForEncounter(enc, enc.pokemon); + } + } + } else { + // Entirely random + for (EncounterSet area : scrambledEncounters) { + // Poke-set + Set<Pokemon> inArea = pokemonInArea(area); + // Build area map using randoms + Map<Pokemon, Pokemon> areaMap = new TreeMap<>(); + for (Pokemon areaPk : inArea) { + Pokemon picked = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned); + while (areaMap.containsValue(picked)) { + picked = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned); + } + areaMap.put(areaPk, picked); + } + for (Encounter enc : area.encounters) { + // Apply the map + enc.pokemon = areaMap.get(enc.pokemon); + setFormeForEncounter(enc, enc.pokemon); + } + } + } + + if (levelModifier != 0) { + for (EncounterSet area : currentEncounters) { + for (Encounter enc : area.encounters) { + enc.level = Math.min(100, (int) Math.round(enc.level * (1 + levelModifier / 100.0))); + enc.maxLevel = Math.min(100, (int) Math.round(enc.maxLevel * (1 + levelModifier / 100.0))); + } + } + } + } + + @Override + public void game1to1Encounters(Settings settings) { + boolean useTimeOfDay = settings.isUseTimeBasedEncounters(); + boolean usePowerLevels = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.SIMILAR_STRENGTH; + boolean noLegendaries = settings.isBlockWildLegendaries(); + int levelModifier = settings.isWildLevelsModified() ? settings.getWildLevelModifier() : 0; + boolean allowAltFormes = settings.isAllowWildAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + + checkPokemonRestrictions(); + // Build the full 1-to-1 map + Map<Pokemon, Pokemon> translateMap = new TreeMap<>(); + List<Pokemon> remainingLeft = allPokemonInclFormesWithoutNull(); + remainingLeft.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + List<Pokemon> remainingRight; + if (allowAltFormes) { + remainingRight = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) + : new ArrayList<>(mainPokemonListInclFormes); + remainingRight.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + remainingRight = noLegendaries ? new ArrayList<>(noLegendaryList) + : new ArrayList<>(mainPokemonList); + } + List<Pokemon> banned = this.bannedForWildEncounters(); + banned.addAll(this.getBannedFormesForPlayerPokemon()); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + // Banned pokemon should be mapped to themselves + for (Pokemon bannedPK : banned) { + translateMap.put(bannedPK, bannedPK); + remainingLeft.remove(bannedPK); + remainingRight.remove(bannedPK); + } + while (!remainingLeft.isEmpty()) { + if (usePowerLevels) { + int pickedLeft = this.random.nextInt(remainingLeft.size()); + Pokemon pickedLeftP = remainingLeft.remove(pickedLeft); + Pokemon pickedRightP; + if (remainingRight.size() == 1) { + // pick this (it may or may not be the same poke) + pickedRightP = remainingRight.get(0); + } else { + // pick on power level with the current one blocked + pickedRightP = pickWildPowerLvlReplacement(remainingRight, pickedLeftP, true, null, 100); + } + remainingRight.remove(pickedRightP); + translateMap.put(pickedLeftP, pickedRightP); + } else { + int pickedLeft = this.random.nextInt(remainingLeft.size()); + int pickedRight = this.random.nextInt(remainingRight.size()); + Pokemon pickedLeftP = remainingLeft.remove(pickedLeft); + Pokemon pickedRightP = remainingRight.get(pickedRight); + while (pickedLeftP.number == pickedRightP.number && remainingRight.size() != 1) { + // Reroll for a different pokemon if at all possible + pickedRight = this.random.nextInt(remainingRight.size()); + pickedRightP = remainingRight.get(pickedRight); + } + remainingRight.remove(pickedRight); + translateMap.put(pickedLeftP, pickedRightP); + } + if (remainingRight.size() == 0) { + // restart + if (allowAltFormes) { + remainingRight.addAll(noLegendaries ? noLegendaryListInclFormes : mainPokemonListInclFormes); + remainingRight.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + remainingRight.addAll(noLegendaries ? noLegendaryList : mainPokemonList); + } + remainingRight.removeAll(banned); + } + } + + // Map remaining to themselves just in case + List<Pokemon> allPokes = allPokemonInclFormesWithoutNull(); + for (Pokemon poke : allPokes) { + if (!translateMap.containsKey(poke)) { + translateMap.put(poke, poke); + } + } + + List<EncounterSet> currentEncounters = this.getEncounters(useTimeOfDay); + + for (EncounterSet area : currentEncounters) { + for (Encounter enc : area.encounters) { + // Apply the map + enc.pokemon = translateMap.get(enc.pokemon); + if (area.bannedPokemon.contains(enc.pokemon)) { + // Ignore the map and put a random non-banned poke + List<Pokemon> tempPickable; + if (allowAltFormes) { + tempPickable = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) + : new ArrayList<>(mainPokemonListInclFormes); + tempPickable.removeIf(o -> ((Pokemon) o).actuallyCosmetic); + } else { + tempPickable = noLegendaries ? new ArrayList<>(noLegendaryList) + : new ArrayList<>(mainPokemonList); + } + tempPickable.removeAll(banned); + tempPickable.removeAll(area.bannedPokemon); + if (tempPickable.size() == 0) { + throw new RandomizationException("ERROR: Couldn't replace a wild Pokemon!"); + } + if (usePowerLevels) { + enc.pokemon = pickWildPowerLvlReplacement(tempPickable, enc.pokemon, false, null, 100); + } else { + int picked = this.random.nextInt(tempPickable.size()); + enc.pokemon = tempPickable.get(picked); + } + } + setFormeForEncounter(enc, enc.pokemon); + } + } + if (levelModifier != 0) { + for (EncounterSet area : currentEncounters) { + for (Encounter enc : area.encounters) { + enc.level = Math.min(100, (int) Math.round(enc.level * (1 + levelModifier / 100.0))); + enc.maxLevel = Math.min(100, (int) Math.round(enc.maxLevel * (1 + levelModifier / 100.0))); + } + } + } + + setEncounters(useTimeOfDay, currentEncounters); + + } + + @Override + public void onlyChangeWildLevels(Settings settings) { + int levelModifier = settings.getWildLevelModifier(); + + List<EncounterSet> currentEncounters = this.getEncounters(true); + + if (levelModifier != 0) { + for (EncounterSet area : currentEncounters) { + for (Encounter enc : area.encounters) { + enc.level = Math.min(100, (int) Math.round(enc.level * (1 + levelModifier / 100.0))); + enc.maxLevel = Math.min(100, (int) Math.round(enc.maxLevel * (1 + levelModifier / 100.0))); + } + } + setEncounters(true, currentEncounters); + } + } + + private void enhanceRandomEncountersORAS(List<EncounterSet> collapsedEncounters, Settings settings) { + boolean catchEmAll = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.CATCH_EM_ALL; + boolean typeThemed = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.TYPE_THEME_AREAS; + boolean usePowerLevels = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.SIMILAR_STRENGTH; + boolean noLegendaries = settings.isBlockWildLegendaries(); + boolean allowAltFormes = settings.isAllowWildAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + + List<Pokemon> banned = this.bannedForWildEncounters(); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + Map<Integer, List<EncounterSet>> zonesToEncounters = mapZonesToEncounters(collapsedEncounters); + Map<Type, List<Pokemon>> cachedPokeLists = new TreeMap<>(); + for (List<EncounterSet> encountersInZone : zonesToEncounters.values()) { + int currentAreaIndex = -1; + List<EncounterSet> nonRockSmashAreas = new ArrayList<>(); + Map<Integer, List<Integer>> areasAndEncountersToRandomize = new TreeMap<>(); + // Since Rock Smash Pokemon do not show up on DexNav, they can be fully randomized + for (EncounterSet area : encountersInZone) { + if (area.displayName.contains("Rock Smash")) { + // Assume EITHER catch em all OR type themed OR match strength for now + if (catchEmAll) { + for (Encounter enc : area.encounters) { + boolean shouldRandomize = doesAnotherEncounterWithSamePokemonExistInArea(enc, area); + if (shouldRandomize) { + enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned); + setFormeForEncounter(enc, enc.pokemon); + } + } + } else if (typeThemed) { + List<Pokemon> possiblePokemon = null; + int iterLoops = 0; + while (possiblePokemon == null && iterLoops < 10000) { + Type areaTheme = randomType(); + if (!cachedPokeLists.containsKey(areaTheme)) { + List<Pokemon> pType = allowAltFormes ? pokemonOfTypeInclFormes(areaTheme, noLegendaries) : + pokemonOfType(areaTheme, noLegendaries); + pType.removeAll(banned); + cachedPokeLists.put(areaTheme, pType); + } + possiblePokemon = cachedPokeLists.get(areaTheme); + if (area.bannedPokemon.size() > 0) { + possiblePokemon = new ArrayList<>(possiblePokemon); + possiblePokemon.removeAll(area.bannedPokemon); + } + if (possiblePokemon.size() == 0) { + // Can't use this type for this area + possiblePokemon = null; + } + iterLoops++; + } + if (possiblePokemon == null) { + throw new RandomizationException("Could not randomize an area in a reasonable amount of attempts."); + } + for (Encounter enc : area.encounters) { + // Pick a random themed pokemon + enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size())); + while (enc.pokemon.actuallyCosmetic) { + enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size())); + } + setFormeForEncounter(enc, enc.pokemon); + } + } else if (usePowerLevels) { + List<Pokemon> allowedPokes; + if (allowAltFormes) { + allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) + : new ArrayList<>(mainPokemonListInclFormes); + } else { + allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryList) + : new ArrayList<>(mainPokemonList); + } + allowedPokes.removeAll(banned); + List<Pokemon> localAllowed = allowedPokes; + if (area.bannedPokemon.size() > 0) { + localAllowed = new ArrayList<>(allowedPokes); + localAllowed.removeAll(area.bannedPokemon); + } + for (Encounter enc : area.encounters) { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100); + while (enc.pokemon.actuallyCosmetic) { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100); + } + setFormeForEncounter(enc, enc.pokemon); + } + } else { + // Entirely random + for (Encounter enc : area.encounters) { + enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned); + setFormeForEncounter(enc, enc.pokemon); + } + } + } else { + currentAreaIndex++; + nonRockSmashAreas.add(area); + List<Integer> encounterIndices = new ArrayList<>(); + for (int i = 0; i < area.encounters.size(); i++) { + encounterIndices.add(i); + } + areasAndEncountersToRandomize.put(currentAreaIndex, encounterIndices); + } + } + + // Now, randomize non-Rock Smash Pokemon until we hit the threshold for DexNav + int crashThreshold = computeDexNavCrashThreshold(encountersInZone); + while (crashThreshold < 18 && areasAndEncountersToRandomize.size() > 0) { + Set<Integer> areaIndices = areasAndEncountersToRandomize.keySet(); + int areaIndex = areaIndices.stream().skip(this.random.nextInt(areaIndices.size())).findFirst().orElse(-1); + List<Integer> encounterIndices = areasAndEncountersToRandomize.get(areaIndex); + int indexInListOfEncounterIndices = this.random.nextInt(encounterIndices.size()); + int randomEncounterIndex = encounterIndices.get(indexInListOfEncounterIndices); + EncounterSet area = nonRockSmashAreas.get(areaIndex); + Encounter enc = area.encounters.get(randomEncounterIndex); + // Assume EITHER catch em all OR type themed OR match strength for now + if (catchEmAll) { + boolean shouldRandomize = doesAnotherEncounterWithSamePokemonExistInArea(enc, area); + if (shouldRandomize) { + enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned); + setFormeForEncounter(enc, enc.pokemon); + } + } else if (typeThemed) { + List<Pokemon> possiblePokemon = null; + Type areaTheme = getTypeForArea(area); + if (!cachedPokeLists.containsKey(areaTheme)) { + List<Pokemon> pType = allowAltFormes ? pokemonOfTypeInclFormes(areaTheme, noLegendaries) : + pokemonOfType(areaTheme, noLegendaries); + pType.removeAll(banned); + cachedPokeLists.put(areaTheme, pType); + } + possiblePokemon = cachedPokeLists.get(areaTheme); + if (area.bannedPokemon.size() > 0) { + possiblePokemon = new ArrayList<>(possiblePokemon); + possiblePokemon.removeAll(area.bannedPokemon); + } + if (possiblePokemon.size() == 0) { + // Can't use this type for this area + throw new RandomizationException("Could not find a possible Pokemon of the correct type."); + } + // Pick a random themed pokemon + enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size())); + while (enc.pokemon.actuallyCosmetic) { + enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size())); + } + setFormeForEncounter(enc, enc.pokemon); + } else if (usePowerLevels) { + List<Pokemon> allowedPokes; + if (allowAltFormes) { + allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) + : new ArrayList<>(mainPokemonListInclFormes); + } else { + allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryList) + : new ArrayList<>(mainPokemonList); + } + allowedPokes.removeAll(banned); + List<Pokemon> localAllowed = allowedPokes; + if (area.bannedPokemon.size() > 0) { + localAllowed = new ArrayList<>(allowedPokes); + localAllowed.removeAll(area.bannedPokemon); + } + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100); + while (enc.pokemon.actuallyCosmetic) { + enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100); + } + setFormeForEncounter(enc, enc.pokemon); + } else { + // Entirely random + enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned); + setFormeForEncounter(enc, enc.pokemon); + } + crashThreshold = computeDexNavCrashThreshold(encountersInZone); + encounterIndices.remove(indexInListOfEncounterIndices); + if (encounterIndices.size() == 0) { + areasAndEncountersToRandomize.remove(areaIndex); + } + } + } + } + + private Type getTypeForArea(EncounterSet area) { + Pokemon firstPokemon = area.encounters.get(0).pokemon; + if (area.encounters.get(0).formeNumber != 0) { + firstPokemon = getAltFormeOfPokemon(firstPokemon, area.encounters.get(0).formeNumber); + } + Type primaryType = firstPokemon.primaryType; + int primaryCount = 1; + Type secondaryType = null; + int secondaryCount = 0; + if (firstPokemon.secondaryType != null) { + secondaryType = firstPokemon.secondaryType; + secondaryCount = 1; + } + for (int i = 1; i < area.encounters.size(); i++) { + Pokemon pokemon = area.encounters.get(i).pokemon; + if (area.encounters.get(i).formeNumber != 0) { + pokemon = getAltFormeOfPokemon(pokemon, area.encounters.get(i).formeNumber); + } + if (pokemon.primaryType == primaryType || pokemon.secondaryType == primaryType) { + primaryCount++; + } + if (pokemon.primaryType == secondaryType || pokemon.secondaryType == secondaryType) { + secondaryCount++; + } + } + return primaryCount > secondaryCount ? primaryType : secondaryType; + } + + private boolean doesAnotherEncounterWithSamePokemonExistInArea(Encounter enc, EncounterSet area) { + for (Encounter encounterToCheck : area.encounters) { + if (enc != encounterToCheck && enc.pokemon == encounterToCheck.pokemon) { + return true; + } + } + return false; + } + + private List<EncounterSet> collapseAreasORAS(List<EncounterSet> currentEncounters) { + List<EncounterSet> output = new ArrayList<>(); + Map<Integer, List<EncounterSet>> zonesToEncounters = mapZonesToEncounters(currentEncounters); + for (Integer zone : zonesToEncounters.keySet()) { + List<EncounterSet> encountersInZone = zonesToEncounters.get(zone); + int crashThreshold = computeDexNavCrashThreshold(encountersInZone); + if (crashThreshold <= 18) { + output.addAll(encountersInZone); + continue; + } + + // Naive Area 1-to-1 randomization will crash the game, so let's start collapsing areas to prevent this. + // Start with combining all the fishing rod encounters, since it's a little less noticeable when they've + // been collapsed. + List<EncounterSet> collapsedEncounters = new ArrayList<>(encountersInZone); + EncounterSet rodGroup = new EncounterSet(); + rodGroup.offset = zone; + rodGroup.displayName = "Rod Group"; + for (EncounterSet area : encountersInZone) { + if (area.displayName.contains("Old Rod") || area.displayName.contains("Good Rod") || area.displayName.contains("Super Rod")) { + collapsedEncounters.remove(area); + rodGroup.encounters.addAll(area.encounters); + } + } + if (rodGroup.encounters.size() > 0) { + collapsedEncounters.add(rodGroup); + } + crashThreshold = computeDexNavCrashThreshold(collapsedEncounters); + if (crashThreshold <= 18) { + output.addAll(collapsedEncounters); + continue; + } + + // Even after combining all the fishing rod encounters, we're still not below the threshold to prevent + // DexNav from crashing the game. Combine all the grass encounters now to drop us below the threshold; + // we've combined everything that DexNav normally combines, so at this point, we're *guaranteed* not + // to crash the game. + EncounterSet grassGroup = new EncounterSet(); + grassGroup.offset = zone; + grassGroup.displayName = "Grass Group"; + for (EncounterSet area : encountersInZone) { + if (area.displayName.contains("Grass/Cave") || area.displayName.contains("Long Grass") || area.displayName.contains("Horde")) { + collapsedEncounters.remove(area); + grassGroup.encounters.addAll(area.encounters); + } + } + if (grassGroup.encounters.size() > 0) { + collapsedEncounters.add(grassGroup); + } + + output.addAll(collapsedEncounters); + } + return output; + } + + private int computeDexNavCrashThreshold(List<EncounterSet> encountersInZone) { + int crashThreshold = 0; + for (EncounterSet area : encountersInZone) { + if (area.displayName.contains("Rock Smash")) { + continue; // Rock Smash Pokemon don't display on DexNav + } + Set<Pokemon> uniquePokemonInArea = new HashSet<>(); + for (Encounter enc : area.encounters) { + if (enc.pokemon.baseForme != null) { // DexNav treats different forms as one Pokemon + uniquePokemonInArea.add(enc.pokemon.baseForme); + } else { + uniquePokemonInArea.add(enc.pokemon); + } + } + crashThreshold += uniquePokemonInArea.size(); + } + return crashThreshold; + } + + private void setEvoChainAsIllegal(Pokemon newPK, List<Pokemon> illegalList, boolean willForceEvolve) { + // set pre-evos as illegal + setIllegalPreEvos(newPK, illegalList); + + // if the placed Pokemon will be forced fully evolved, set its evolutions as illegal + if (willForceEvolve) { + setIllegalEvos(newPK, illegalList); + } + } + + private void setIllegalPreEvos(Pokemon pk, List<Pokemon> illegalList) { + for (Evolution evo: pk.evolutionsTo) { + pk = evo.from; + illegalList.add(pk); + setIllegalPreEvos(pk, illegalList); + } + } + + private void setIllegalEvos(Pokemon pk, List<Pokemon> illegalList) { + for (Evolution evo: pk.evolutionsFrom) { + pk = evo.to; + illegalList.add(pk); + setIllegalEvos(pk, illegalList); + } + } + + private List<Pokemon> getFinalEvos(Pokemon pk) { + List<Pokemon> finalEvos = new ArrayList<>(); + traverseEvolutions(pk, finalEvos); + return finalEvos; + } + + private void traverseEvolutions(Pokemon pk, List<Pokemon> finalEvos) { + if (!pk.evolutionsFrom.isEmpty()) { + for (Evolution evo: pk.evolutionsFrom) { + pk = evo.to; + traverseEvolutions(pk, finalEvos); + } + } else { + finalEvos.add(pk); + } + } + + private void setFormeForTrainerPokemon(TrainerPokemon tp, Pokemon pk) { + boolean checkCosmetics = true; + tp.formeSuffix = ""; + tp.forme = 0; + if (pk.formeNumber > 0) { + tp.forme = pk.formeNumber; + tp.formeSuffix = pk.formeSuffix; + tp.pokemon = pk.baseForme; + checkCosmetics = false; + } + if (checkCosmetics && tp.pokemon.cosmeticForms > 0) { + tp.forme = tp.pokemon.getCosmeticFormNumber(this.random.nextInt(tp.pokemon.cosmeticForms)); + } else if (!checkCosmetics && pk.cosmeticForms > 0) { + tp.forme += pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms)); + } + } + + private void applyLevelModifierToTrainerPokemon(Trainer trainer, int levelModifier) { + if (levelModifier != 0) { + for (TrainerPokemon tp : trainer.pokemon) { + tp.level = Math.min(100, (int) Math.round(tp.level * (1 + levelModifier / 100.0))); + } + } + } + + @Override + public void randomizeTrainerPokes(Settings settings) { + boolean usePowerLevels = settings.isTrainersUsePokemonOfSimilarStrength(); + boolean weightByFrequency = settings.isTrainersMatchTypingDistribution(); + boolean noLegendaries = settings.isTrainersBlockLegendaries(); + boolean noEarlyWonderGuard = settings.isTrainersBlockEarlyWonderGuard(); + int levelModifier = settings.isTrainersLevelModified() ? settings.getTrainersLevelModifier() : 0; + boolean isTypeThemed = settings.getTrainersMod() == Settings.TrainersMod.TYPE_THEMED; + boolean isTypeThemedEliteFourGymOnly = settings.getTrainersMod() == Settings.TrainersMod.TYPE_THEMED_ELITE4_GYMS; + boolean distributionSetting = settings.getTrainersMod() == Settings.TrainersMod.DISTRIBUTED; + boolean mainPlaythroughSetting = settings.getTrainersMod() == Settings.TrainersMod.MAINPLAYTHROUGH; + boolean includeFormes = settings.isAllowTrainerAlternateFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + boolean swapMegaEvos = settings.isSwapTrainerMegaEvos(); + boolean shinyChance = settings.isShinyChance(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + int eliteFourUniquePokemonNumber = settings.getEliteFourUniquePokemonNumber(); + boolean forceFullyEvolved = settings.isTrainersForceFullyEvolved(); + int forceFullyEvolvedLevel = settings.getTrainersForceFullyEvolvedLevel(); + boolean forceChallengeMode = (settings.getCurrentMiscTweaks() & MiscTweak.FORCE_CHALLENGE_MODE.getValue()) > 0; + boolean rivalCarriesStarter = settings.isRivalCarriesStarterThroughout(); + + checkPokemonRestrictions(); + + // Set up Pokemon pool + cachedReplacementLists = new TreeMap<>(); + cachedAllList = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>( + mainPokemonList); + if (includeFormes) { + if (noLegendaries) { + cachedAllList.addAll(noLegendaryAltsList); + } else { + cachedAllList.addAll(altFormesList); + } + } + cachedAllList = + cachedAllList + .stream() + .filter(pk -> !pk.actuallyCosmetic) + .collect(Collectors.toList()); + + List<Pokemon> banned = this.getBannedFormesForTrainerPokemon(); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + cachedAllList.removeAll(banned); + + List<Trainer> currentTrainers = this.getTrainers(); + + // Type Themed related + Map<Trainer, Type> trainerTypes = new TreeMap<>(); + Set<Type> usedUberTypes = new TreeSet<>(); + if (isTypeThemed || isTypeThemedEliteFourGymOnly) { + typeWeightings = new TreeMap<>(); + totalTypeWeighting = 0; + // Construct groupings for types + // Anything starting with GYM or ELITE or CHAMPION is a group + Map<String, List<Trainer>> groups = new TreeMap<>(); + for (Trainer t : currentTrainers) { + if (t.tag != null && t.tag.equals("IRIVAL")) { + // This is the first rival in Yellow. His Pokemon is used to determine the non-player + // starter, so we can't change it here. Just skip it. + continue; + } + String group = t.tag == null ? "" : t.tag; + if (group.contains("-")) { + group = group.substring(0, group.indexOf('-')); + } + if (group.startsWith("GYM") || group.startsWith("ELITE") || + ((group.startsWith("CHAMPION") || group.startsWith("THEMED")) && !isTypeThemedEliteFourGymOnly)) { + // Yep this is a group + if (!groups.containsKey(group)) { + groups.put(group, new ArrayList<>()); + } + groups.get(group).add(t); + } else if (group.startsWith("GIO")) { + // Giovanni has same grouping as his gym, gym 8 + if (!groups.containsKey("GYM8")) { + groups.put("GYM8", new ArrayList<>()); + } + groups.get("GYM8").add(t); + } + } + + // Give a type to each group + // Gym & elite types have to be unique + // So do uber types, including the type we pick for champion + Set<Type> usedGymTypes = new TreeSet<>(); + Set<Type> usedEliteTypes = new TreeSet<>(); + for (String group : groups.keySet()) { + List<Trainer> trainersInGroup = groups.get(group); + // Shuffle ordering within group to promote randomness + Collections.shuffle(trainersInGroup, random); + Type typeForGroup = pickType(weightByFrequency, noLegendaries, includeFormes); + if (group.startsWith("GYM")) { + while (usedGymTypes.contains(typeForGroup)) { + typeForGroup = pickType(weightByFrequency, noLegendaries, includeFormes); + } + usedGymTypes.add(typeForGroup); + } + if (group.startsWith("ELITE")) { + while (usedEliteTypes.contains(typeForGroup)) { + typeForGroup = pickType(weightByFrequency, noLegendaries, includeFormes); + } + usedEliteTypes.add(typeForGroup); + } + if (group.equals("CHAMPION")) { + usedUberTypes.add(typeForGroup); + } + + for (Trainer t : trainersInGroup) { + trainerTypes.put(t, typeForGroup); + } + } + } + + // Randomize the order trainers are randomized in. + // Leads to less predictable results for various modifiers. + // Need to keep the original ordering around for saving though. + List<Trainer> scrambledTrainers = new ArrayList<>(currentTrainers); + Collections.shuffle(scrambledTrainers, this.random); + + // Elite Four Unique Pokemon related + boolean eliteFourUniquePokemon = eliteFourUniquePokemonNumber > 0; + List<Pokemon> illegalIfEvolvedList = new ArrayList<>(); + List<Pokemon> bannedFromUniqueList = new ArrayList<>(); + boolean illegalEvoChains = false; + List<Integer> eliteFourIndices = getEliteFourTrainers(forceChallengeMode); + if (eliteFourUniquePokemon) { + // Sort Elite Four Trainers to the start of the list + scrambledTrainers.sort((t1, t2) -> + Boolean.compare(eliteFourIndices.contains(currentTrainers.indexOf(t2)+1),eliteFourIndices.contains(currentTrainers.indexOf(t1)+1))); + illegalEvoChains = forceFullyEvolved; + if (rivalCarriesStarter) { + List<Pokemon> starterList = getStarters().subList(0,3); + for (Pokemon starter: starterList) { + // If rival/friend carries starter, the starters cannot be set as unique + bannedFromUniqueList.add(starter); + setEvoChainAsIllegal(starter, bannedFromUniqueList, true); + + // If the final boss is a rival/friend, the fully evolved starters will be unique + if (hasRivalFinalBattle()) { + cachedAllList.removeAll(getFinalEvos(starter)); + if (illegalEvoChains) { + illegalIfEvolvedList.add(starter); + setEvoChainAsIllegal(starter, illegalIfEvolvedList, true); + } + } + } + } + } + + List<Integer> mainPlaythroughTrainers = getMainPlaythroughTrainers(); + + // Randomize Trainer Pokemon + // The result after this is done will not be final if "Force Fully Evolved" or "Rival Carries Starter" + // are used, as they are applied later + for (Trainer t : scrambledTrainers) { + applyLevelModifierToTrainerPokemon(t, levelModifier); + if (t.tag != null && t.tag.equals("IRIVAL")) { + // This is the first rival in Yellow. His Pokemon is used to determine the non-player + // starter, so we can't change it here. Just skip it. + continue; + } + + // If type themed, give a type to each unassigned trainer + Type typeForTrainer = trainerTypes.get(t); + if (typeForTrainer == null && isTypeThemed) { + typeForTrainer = pickType(weightByFrequency, noLegendaries, includeFormes); + // Ubers: can't have the same type as each other + if (t.tag != null && t.tag.equals("UBER")) { + while (usedUberTypes.contains(typeForTrainer)) { + typeForTrainer = pickType(weightByFrequency, noLegendaries, includeFormes); + } + usedUberTypes.add(typeForTrainer); + } + } + + List<Pokemon> evolvesIntoTheWrongType = new ArrayList<>(); + if (typeForTrainer != null) { + List<Pokemon> pokemonOfType = includeFormes ? pokemonOfTypeInclFormes(typeForTrainer, noLegendaries) : + pokemonOfType(typeForTrainer, noLegendaries); + for (Pokemon pk : pokemonOfType) { + if (!pokemonOfType.contains(fullyEvolve(pk, t.index))) { + evolvesIntoTheWrongType.add(pk); + } + } + } + + List<TrainerPokemon> trainerPokemonList = new ArrayList<>(t.pokemon); + + // Elite Four Unique Pokemon related + boolean eliteFourTrackPokemon = false; + boolean eliteFourRival = false; + if (eliteFourUniquePokemon && eliteFourIndices.contains(t.index)) { + eliteFourTrackPokemon = true; + + // Sort Pokemon list back to front, and then put highest level Pokemon first + // (Only while randomizing, does not affect order in game) + Collections.reverse(trainerPokemonList); + trainerPokemonList.sort((tp1, tp2) -> Integer.compare(tp2.level, tp1.level)); + if (rivalCarriesStarter && (t.tag.contains("RIVAL") || t.tag.contains("FRIEND"))) { + eliteFourRival = true; + } + } + + for (TrainerPokemon tp : trainerPokemonList) { + boolean swapThisMegaEvo = swapMegaEvos && tp.canMegaEvolve(); + boolean wgAllowed = (!noEarlyWonderGuard) || tp.level >= 20; + boolean eliteFourSetUniquePokemon = + eliteFourTrackPokemon && eliteFourUniquePokemonNumber > trainerPokemonList.indexOf(tp); + boolean willForceEvolve = forceFullyEvolved && tp.level >= forceFullyEvolvedLevel; + + Pokemon oldPK = tp.pokemon; + if (tp.forme > 0) { + oldPK = getAltFormeOfPokemon(oldPK, tp.forme); + } + + bannedList = new ArrayList<>(); + bannedList.addAll(usedAsUniqueList); + if (illegalEvoChains && willForceEvolve) { + bannedList.addAll(illegalIfEvolvedList); + } + if (eliteFourSetUniquePokemon) { + bannedList.addAll(bannedFromUniqueList); + } + if (willForceEvolve) { + bannedList.addAll(evolvesIntoTheWrongType); + } + + Pokemon newPK = pickTrainerPokeReplacement( + oldPK, + usePowerLevels, + typeForTrainer, + noLegendaries, + wgAllowed, + distributionSetting || (mainPlaythroughSetting && mainPlaythroughTrainers.contains(t.index)), + swapThisMegaEvo, + abilitiesAreRandomized, + includeFormes, + banIrregularAltFormes + ); + + // Chosen Pokemon is locked in past here + if (distributionSetting || (mainPlaythroughSetting && mainPlaythroughTrainers.contains(t.index))) { + setPlacementHistory(newPK); + } + tp.pokemon = newPK; + setFormeForTrainerPokemon(tp, newPK); + tp.abilitySlot = getRandomAbilitySlot(newPK); + tp.resetMoves = true; + + if (!eliteFourRival) { + if (eliteFourSetUniquePokemon) { + List<Pokemon> actualPKList; + if (willForceEvolve) { + actualPKList = getFinalEvos(newPK); + } else { + actualPKList = new ArrayList<>(); + actualPKList.add(newPK); + } + // If the unique Pokemon will evolve, we have to set all its potential evolutions as unique + for (Pokemon actualPK: actualPKList) { + usedAsUniqueList.add(actualPK); + if (illegalEvoChains) { + setEvoChainAsIllegal(actualPK, illegalIfEvolvedList, willForceEvolve); + } + } + } + if (eliteFourTrackPokemon) { + bannedFromUniqueList.add(newPK); + if (illegalEvoChains) { + setEvoChainAsIllegal(newPK, bannedFromUniqueList, willForceEvolve); + } + } + } else { + // If the champion is a rival, the first Pokemon will be skipped - it's already + // set as unique since it's a starter + eliteFourRival = false; + } + + if (swapThisMegaEvo) { + tp.heldItem = newPK + .megaEvolutionsFrom + .get(this.random.nextInt(newPK.megaEvolutionsFrom.size())) + .argument; + } + + if (shinyChance) { + if (this.random.nextInt(256) == 0) { + tp.IVs |= (1 << 30); + } + } + } + } + + // Save it all up + this.setTrainers(currentTrainers, false); + } + + @Override + public void randomizeTrainerHeldItems(Settings settings) { + boolean giveToBossPokemon = settings.isRandomizeHeldItemsForBossTrainerPokemon(); + boolean giveToImportantPokemon = settings.isRandomizeHeldItemsForImportantTrainerPokemon(); + boolean giveToRegularPokemon = settings.isRandomizeHeldItemsForRegularTrainerPokemon(); + boolean highestLevelOnly = settings.isHighestLevelGetsItemsForTrainers(); + + List<Move> moves = this.getMoves(); + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + List<Trainer> currentTrainers = this.getTrainers(); + for (Trainer t : currentTrainers) { + if (trainerShouldNotGetBuffs(t)) { + continue; + } + if (!giveToRegularPokemon && (!t.isImportant() && !t.isBoss())) { + continue; + } + if (!giveToImportantPokemon && t.isImportant()) { + continue; + } + if (!giveToBossPokemon && t.isBoss()) { + continue; + } + t.setPokemonHaveItems(true); + if (highestLevelOnly) { + int maxLevel = -1; + TrainerPokemon highestLevelPoke = null; + for (TrainerPokemon tp : t.pokemon) { + if (tp.level > maxLevel) { + highestLevelPoke = tp; + maxLevel = tp.level; + } + } + if (highestLevelPoke == null) { + continue; // should never happen - trainer had zero pokes + } + int[] moveset = highestLevelPoke.resetMoves ? + RomFunctions.getMovesAtLevel(getAltFormeOfPokemon( + highestLevelPoke.pokemon, highestLevelPoke.forme).number, + movesets, + highestLevelPoke.level) : + highestLevelPoke.moves; + randomizeHeldItem(highestLevelPoke, settings, moves, moveset); + } else { + for (TrainerPokemon tp : t.pokemon) { + int[] moveset = tp.resetMoves ? + RomFunctions.getMovesAtLevel(getAltFormeOfPokemon( + tp.pokemon, tp.forme).number, + movesets, + tp.level) : + tp.moves; + randomizeHeldItem(tp, settings, moves, moveset); + if (t.requiresUniqueHeldItems) { + while (!t.pokemonHaveUniqueHeldItems()) { + randomizeHeldItem(tp, settings, moves, moveset); + } + } + } + } + } + this.setTrainers(currentTrainers, false); + } + + private void randomizeHeldItem(TrainerPokemon tp, Settings settings, List<Move> moves, int[] moveset) { + boolean sensibleItemsOnly = settings.isSensibleItemsOnlyForTrainers(); + boolean consumableItemsOnly = settings.isConsumableItemsOnlyForTrainers(); + boolean swapMegaEvolutions = settings.isSwapTrainerMegaEvos(); + if (tp.hasZCrystal) { + return; // Don't overwrite existing Z Crystals. + } + if (tp.hasMegaStone && swapMegaEvolutions) { + return; // Don't overwrite mega stones if another setting handled that. + } + List<Integer> toChooseFrom; + if (sensibleItemsOnly) { + toChooseFrom = getSensibleHeldItemsFor(tp, consumableItemsOnly, moves, moveset); + } else if (consumableItemsOnly) { + toChooseFrom = getAllConsumableHeldItems(); + } else { + toChooseFrom = getAllHeldItems(); + } + tp.heldItem = toChooseFrom.get(random.nextInt(toChooseFrom.size())); + } + + @Override + public void rivalCarriesStarter() { + checkPokemonRestrictions(); + List<Trainer> currentTrainers = this.getTrainers(); + rivalCarriesStarterUpdate(currentTrainers, "RIVAL", isORAS ? 0 : 1); + rivalCarriesStarterUpdate(currentTrainers, "FRIEND", 2); + this.setTrainers(currentTrainers, false); + } + + @Override + public boolean hasRivalFinalBattle() { + return false; + } + + @Override + public void forceFullyEvolvedTrainerPokes(Settings settings) { + int minLevel = settings.getTrainersForceFullyEvolvedLevel(); + + checkPokemonRestrictions(); + List<Trainer> currentTrainers = this.getTrainers(); + for (Trainer t : currentTrainers) { + for (TrainerPokemon tp : t.pokemon) { + if (tp.level >= minLevel) { + Pokemon newPokemon = fullyEvolve(tp.pokemon, t.index); + if (newPokemon != tp.pokemon) { + tp.pokemon = newPokemon; + setFormeForTrainerPokemon(tp, newPokemon); + tp.abilitySlot = getValidAbilitySlotFromOriginal(newPokemon, tp.abilitySlot); + tp.resetMoves = true; + } + } + } + } + this.setTrainers(currentTrainers, false); + } + + @Override + public void onlyChangeTrainerLevels(Settings settings) { + int levelModifier = settings.getTrainersLevelModifier(); + + List<Trainer> currentTrainers = this.getTrainers(); + for (Trainer t: currentTrainers) { + applyLevelModifierToTrainerPokemon(t, levelModifier); + } + this.setTrainers(currentTrainers, false); + } + + @Override + public void addTrainerPokemon(Settings settings) { + int additionalNormal = settings.getAdditionalRegularTrainerPokemon(); + int additionalImportant = settings.getAdditionalImportantTrainerPokemon(); + int additionalBoss = settings.getAdditionalBossTrainerPokemon(); + + List<Trainer> currentTrainers = this.getTrainers(); + for (Trainer t: currentTrainers) { + int additional; + if (t.isBoss()) { + additional = additionalBoss; + } else if (t.isImportant()) { + if (t.skipImportant()) continue; + additional = additionalImportant; + } else { + additional = additionalNormal; + } + + if (additional == 0) { + continue; + } + + int lowest = 100; + List<TrainerPokemon> potentialPokes = new ArrayList<>(); + + // First pass: find lowest level + for (TrainerPokemon tpk: t.pokemon) { + if (tpk.level < lowest) { + lowest = tpk.level; + } + } + + // Second pass: find all Pokemon at lowest level + for (TrainerPokemon tpk: t.pokemon) { + if (tpk.level == lowest) { + potentialPokes.add(tpk); + } + } + + // If a trainer can appear in a Multi Battle (i.e., a Double Battle where the enemy consists + // of two independent trainers), we want to be aware of that so we don't give them a team of + // six Pokemon and have a 6v12 battle + int maxPokemon = t.multiBattleStatus != Trainer.MultiBattleStatus.NEVER ? 3 : 6; + for (int i = 0; i < additional; i++) { + if (t.pokemon.size() >= maxPokemon) break; + + // We want to preserve the original last Pokemon because the order is sometimes used to + // determine the rival's starter + int secondToLastIndex = t.pokemon.size() - 1; + TrainerPokemon newPokemon = potentialPokes.get(i % potentialPokes.size()).copy(); + + // Clear out the held item because we only want one Pokemon with a mega stone if we're + // swapping mega evolvables + newPokemon.heldItem = 0; + t.pokemon.add(secondToLastIndex, newPokemon); + } + } + this.setTrainers(currentTrainers, false); + } + + @Override + public void doubleBattleMode() { + List<Trainer> currentTrainers = this.getTrainers(); + for (Trainer t: currentTrainers) { + if (t.pokemon.size() != 1 || t.multiBattleStatus == Trainer.MultiBattleStatus.ALWAYS || this.trainerShouldNotGetBuffs(t)) { + continue; + } + t.pokemon.add(t.pokemon.get(0).copy()); + } + this.setTrainers(currentTrainers, true); + } + + private Map<Integer, List<MoveLearnt>> allLevelUpMoves; + private Map<Integer, List<Integer>> allEggMoves; + private Map<Pokemon, boolean[]> allTMCompat, allTutorCompat; + private List<Integer> allTMMoves, allTutorMoves; + + @Override + public List<Move> getMoveSelectionPoolAtLevel(TrainerPokemon tp, boolean cyclicEvolutions) { + + List<Move> moves = getMoves(); + double eggMoveProbability = 0.1; + double preEvoMoveProbability = 0.5; + double tmMoveProbability = 0.6; + double tutorMoveProbability = 0.6; + + if (allLevelUpMoves == null) { + allLevelUpMoves = getMovesLearnt(); + } + + if (allEggMoves == null) { + allEggMoves = getEggMoves(); + } + + if (allTMCompat == null) { + allTMCompat = getTMHMCompatibility(); + } + + if (allTMMoves == null) { + allTMMoves = getTMMoves(); + } + + if (allTutorCompat == null && hasMoveTutors()) { + allTutorCompat = getMoveTutorCompatibility(); + } + + if (allTutorMoves == null) { + allTutorMoves = getMoveTutorMoves(); + } + + // Level-up Moves + List<Move> moveSelectionPoolAtLevel = allLevelUpMoves.get(getAltFormeOfPokemon(tp.pokemon, tp.forme).number) + .stream() + .filter(ml -> (ml.level <= tp.level && ml.level != 0) || (ml.level == 0 && tp.level >= 30)) + .map(ml -> moves.get(ml.move)) + .distinct() + .collect(Collectors.toList()); + + // Pre-Evo Moves + if (!cyclicEvolutions) { + Pokemon preEvo; + if (altFormesCanHaveDifferentEvolutions()) { + preEvo = getAltFormeOfPokemon(tp.pokemon, tp.forme); + } else { + preEvo = tp.pokemon; + } + while (!preEvo.evolutionsTo.isEmpty()) { + preEvo = preEvo.evolutionsTo.get(0).from; + moveSelectionPoolAtLevel.addAll(allLevelUpMoves.get(preEvo.number) + .stream() + .filter(ml -> ml.level <= tp.level) + .filter(ml -> this.random.nextDouble() < preEvoMoveProbability) + .map(ml -> moves.get(ml.move)) + .distinct() + .collect(Collectors.toList())); + } + } + + // TM Moves + boolean[] tmCompat = allTMCompat.get(getAltFormeOfPokemon(tp.pokemon, tp.forme)); + for (int tmMove: allTMMoves) { + if (tmCompat[allTMMoves.indexOf(tmMove) + 1]) { + Move thisMove = moves.get(tmMove); + if (thisMove.power > 1 && tp.level * 3 > thisMove.power * thisMove.hitCount && + this.random.nextDouble() < tmMoveProbability) { + moveSelectionPoolAtLevel.add(thisMove); + } else if ((thisMove.power <= 1 && this.random.nextInt(100) < tp.level) || + this.random.nextInt(200) < tp.level) { + moveSelectionPoolAtLevel.add(thisMove); + } + } + } + + // Move Tutor Moves + if (hasMoveTutors()) { + boolean[] tutorCompat = allTutorCompat.get(getAltFormeOfPokemon(tp.pokemon, tp.forme)); + for (int tutorMove: allTutorMoves) { + if (tutorCompat[allTutorMoves.indexOf(tutorMove) + 1]) { + Move thisMove = moves.get(tutorMove); + if (thisMove.power > 1 && tp.level * 3 > thisMove.power * thisMove.hitCount && + this.random.nextDouble() < tutorMoveProbability) { + moveSelectionPoolAtLevel.add(thisMove); + } else if ((thisMove.power <= 1 && this.random.nextInt(100) < tp.level) || + this.random.nextInt(200) < tp.level) { + moveSelectionPoolAtLevel.add(thisMove); + } + } + } + } + + // Egg Moves + if (!cyclicEvolutions) { + Pokemon firstEvo; + if (altFormesCanHaveDifferentEvolutions()) { + firstEvo = getAltFormeOfPokemon(tp.pokemon, tp.forme); + } else { + firstEvo = tp.pokemon; + } + while (!firstEvo.evolutionsTo.isEmpty()) { + firstEvo = firstEvo.evolutionsTo.get(0).from; + } + if (allEggMoves.get(firstEvo.number) != null) { + moveSelectionPoolAtLevel.addAll(allEggMoves.get(firstEvo.number) + .stream() + .filter(egm -> this.random.nextDouble() < eggMoveProbability) + .map(moves::get) + .collect(Collectors.toList())); + } + } + + + + return moveSelectionPoolAtLevel.stream().distinct().collect(Collectors.toList()); + } + + @Override + public void pickTrainerMovesets(Settings settings) { + boolean isCyclicEvolutions = settings.getEvolutionsMod() == Settings.EvolutionsMod.RANDOM_EVERY_LEVEL; + boolean doubleBattleMode = settings.isDoubleBattleMode(); + + List<Trainer> trainers = getTrainers(); + + for (Trainer t: trainers) { + t.setPokemonHaveCustomMoves(true); + + for (TrainerPokemon tp: t.pokemon) { + tp.resetMoves = false; + + List<Move> movesAtLevel = getMoveSelectionPoolAtLevel(tp, isCyclicEvolutions); + + movesAtLevel = trimMoveList(tp, movesAtLevel, doubleBattleMode); + + if (movesAtLevel.isEmpty()) { + continue; + } + + double trainerTypeModifier = 1; + if (t.isImportant()) { + trainerTypeModifier = 1.5; + } else if (t.isBoss()) { + trainerTypeModifier = 2; + } + double movePoolSizeModifier = movesAtLevel.size() / 10.0; + double bonusModifier = trainerTypeModifier * movePoolSizeModifier; + + double atkSpatkRatioModifier = 0.75; + double stabMoveBias = 0.25 * bonusModifier; + double hardAbilityMoveBias = 1 * bonusModifier; + double softAbilityMoveBias = 0.5 * bonusModifier; + double statBias = 0.5 * bonusModifier; + double softMoveBias = 0.25 * bonusModifier; + double hardMoveBias = 1 * bonusModifier; + double softMoveAntiBias = 0.5; + + // Add bias for STAB + + Pokemon pk = getAltFormeOfPokemon(tp.pokemon, tp.forme); + + List<Move> stabMoves = new ArrayList<>(movesAtLevel) + .stream() + .filter(mv -> mv.type == pk.primaryType && mv.category != MoveCategory.STATUS) + .collect(Collectors.toList()); + Collections.shuffle(stabMoves, this.random); + + for (int i = 0; i < stabMoveBias * stabMoves.size(); i++) { + int j = i % stabMoves.size(); + movesAtLevel.add(stabMoves.get(j)); + } + + if (pk.secondaryType != null) { + stabMoves = new ArrayList<>(movesAtLevel) + .stream() + .filter(mv -> mv.type == pk.secondaryType && mv.category != MoveCategory.STATUS) + .collect(Collectors.toList()); + Collections.shuffle(stabMoves, this.random); + + for (int i = 0; i < stabMoveBias * stabMoves.size(); i++) { + int j = i % stabMoves.size(); + movesAtLevel.add(stabMoves.get(j)); + } + } + + // Hard ability/move synergy + + List<Move> abilityMoveSynergyList = MoveSynergy.getHardAbilityMoveSynergy( + getAbilityForTrainerPokemon(tp), + pk.primaryType, + pk.secondaryType, + movesAtLevel, + generationOfPokemon(), + perfectAccuracy); + Collections.shuffle(abilityMoveSynergyList, this.random); + for (int i = 0; i < hardAbilityMoveBias * abilityMoveSynergyList.size(); i++) { + int j = i % abilityMoveSynergyList.size(); + movesAtLevel.add(abilityMoveSynergyList.get(j)); + } + + // Soft ability/move synergy + + List<Move> softAbilityMoveSynergyList = MoveSynergy.getSoftAbilityMoveSynergy( + getAbilityForTrainerPokemon(tp), + movesAtLevel, + pk.primaryType, + pk.secondaryType); + + Collections.shuffle(softAbilityMoveSynergyList, this.random); + for (int i = 0; i < softAbilityMoveBias * softAbilityMoveSynergyList.size(); i++) { + int j = i % softAbilityMoveSynergyList.size(); + movesAtLevel.add(softAbilityMoveSynergyList.get(j)); + } + + // Soft ability/move anti-synergy + + List<Move> softAbilityMoveAntiSynergyList = MoveSynergy.getSoftAbilityMoveAntiSynergy( + getAbilityForTrainerPokemon(tp), movesAtLevel); + List<Move> withoutSoftAntiSynergy = new ArrayList<>(movesAtLevel); + for (Move mv: softAbilityMoveAntiSynergyList) { + withoutSoftAntiSynergy.remove(mv); + } + if (withoutSoftAntiSynergy.size() > 0) { + movesAtLevel = withoutSoftAntiSynergy; + } + + List<Move> distinctMoveList = movesAtLevel.stream().distinct().collect(Collectors.toList()); + int movesLeft = distinctMoveList.size(); + + if (movesLeft <= 4) { + + for (int i = 0; i < 4; i++) { + if (i < movesLeft) { + tp.moves[i] = distinctMoveList.get(i).number; + } else { + tp.moves[i] = 0; + } + } + continue; + } + + // Stat/move synergy + + List<Move> statSynergyList = MoveSynergy.getStatMoveSynergy(pk, movesAtLevel); + Collections.shuffle(statSynergyList, this.random); + for (int i = 0; i < statBias * statSynergyList.size(); i++) { + int j = i % statSynergyList.size(); + movesAtLevel.add(statSynergyList.get(j)); + } + + // Stat/move anti-synergy + + List<Move> statAntiSynergyList = MoveSynergy.getStatMoveAntiSynergy(pk, movesAtLevel); + List<Move> withoutStatAntiSynergy = new ArrayList<>(movesAtLevel); + for (Move mv: statAntiSynergyList) { + withoutStatAntiSynergy.remove(mv); + } + if (withoutStatAntiSynergy.size() > 0) { + movesAtLevel = withoutStatAntiSynergy; + } + + distinctMoveList = movesAtLevel.stream().distinct().collect(Collectors.toList()); + movesLeft = distinctMoveList.size(); + + if (movesLeft <= 4) { + + for (int i = 0; i < 4; i++) { + if (i < movesLeft) { + tp.moves[i] = distinctMoveList.get(i).number; + } else { + tp.moves[i] = 0; + } + } + continue; + } + + // Add bias for atk/spatk ratio + + double atkSpatkRatio = (double)pk.attack / (double)pk.spatk; + switch(getAbilityForTrainerPokemon(tp)) { + case Abilities.hugePower: + case Abilities.purePower: + atkSpatkRatio *= 2; + break; + case Abilities.hustle: + case Abilities.gorillaTactics: + atkSpatkRatio *= 1.5; + break; + case Abilities.moxie: + atkSpatkRatio *= 1.1; + break; + case Abilities.soulHeart: + atkSpatkRatio *= 0.9; + break; + } + + List<Move> physicalMoves = new ArrayList<>(movesAtLevel) + .stream() + .filter(mv -> mv.category == MoveCategory.PHYSICAL) + .collect(Collectors.toList()); + List<Move> specialMoves = new ArrayList<>(movesAtLevel) + .stream() + .filter(mv -> mv.category == MoveCategory.SPECIAL) + .collect(Collectors.toList()); + + if (atkSpatkRatio < 1 && specialMoves.size() > 0) { + atkSpatkRatio = 1 / atkSpatkRatio; + double acceptedRatio = atkSpatkRatioModifier * atkSpatkRatio; + int additionalMoves = (int)(physicalMoves.size() * acceptedRatio) - specialMoves.size(); + for (int i = 0; i < additionalMoves; i++) { + Move mv = specialMoves.get(this.random.nextInt(specialMoves.size())); + movesAtLevel.add(mv); + } + } else if (physicalMoves.size() > 0) { + double acceptedRatio = atkSpatkRatioModifier * atkSpatkRatio; + int additionalMoves = (int)(specialMoves.size() * acceptedRatio) - physicalMoves.size(); + for (int i = 0; i < additionalMoves; i++) { + Move mv = physicalMoves.get(this.random.nextInt(physicalMoves.size())); + movesAtLevel.add(mv); + } + } + + // Pick moves + + List<Move> pickedMoves = new ArrayList<>(); + + for (int i = 1; i <= 4; i++) { + Move move; + List<Move> pickFrom; + + if (i == 1) { + pickFrom = movesAtLevel + .stream() + .filter(mv -> mv.isGoodDamaging(perfectAccuracy)) + .collect(Collectors.toList()); + if (pickFrom.isEmpty()) { + pickFrom = movesAtLevel; + } + } else { + pickFrom = movesAtLevel; + } + + if (i == 4) { + List<Move> requiresOtherMove = movesAtLevel + .stream() + .filter(mv -> GlobalConstants.requiresOtherMove.contains(mv.number)) + .distinct() + .collect(Collectors.toList()); + + for (Move dependentMove: requiresOtherMove) { + boolean hasRequiredMove = false; + for (Move requiredMove: MoveSynergy.requiresOtherMove(dependentMove, movesAtLevel)) { + if (pickedMoves.contains(requiredMove)) { + hasRequiredMove = true; + break; + } + } + if (!hasRequiredMove) { + movesAtLevel.removeAll(Collections.singletonList(dependentMove)); + } + } + } + + move = pickFrom.get(this.random.nextInt(pickFrom.size())); + pickedMoves.add(move); + + if (i == 4) { + break; + } + + movesAtLevel.removeAll(Collections.singletonList(move)); + + movesAtLevel.removeAll(MoveSynergy.getHardMoveAntiSynergy(move, movesAtLevel)); + + distinctMoveList = movesAtLevel.stream().distinct().collect(Collectors.toList()); + movesLeft = distinctMoveList.size(); + + if (movesLeft <= (4 - i)) { + pickedMoves.addAll(distinctMoveList); + break; + } + + List<Move> hardMoveSynergyList = MoveSynergy.getMoveSynergy( + move, + movesAtLevel, + generationOfPokemon()); + Collections.shuffle(hardMoveSynergyList, this.random); + for (int j = 0; j < hardMoveBias * hardMoveSynergyList.size(); j++) { + int k = j % hardMoveSynergyList.size(); + movesAtLevel.add(hardMoveSynergyList.get(k)); + } + + List<Move> softMoveSynergyList = MoveSynergy.getSoftMoveSynergy( + move, + movesAtLevel, + generationOfPokemon(), + isEffectivenessUpdated()); + Collections.shuffle(softMoveSynergyList, this.random); + for (int j = 0; j < softMoveBias * softMoveSynergyList.size(); j++) { + int k = j % softMoveSynergyList.size(); + movesAtLevel.add(softMoveSynergyList.get(k)); + } + + List<Move> softMoveAntiSynergyList = MoveSynergy.getSoftMoveAntiSynergy(move, movesAtLevel); + Collections.shuffle(softMoveAntiSynergyList, this.random); + for (int j = 0; j < softMoveAntiBias * softMoveAntiSynergyList.size(); j++) { + distinctMoveList = movesAtLevel.stream().distinct().collect(Collectors.toList()); + if (distinctMoveList.size() <= (4 - i)) { + break; + } + int k = j % softMoveAntiSynergyList.size(); + movesAtLevel.remove(softMoveAntiSynergyList.get(k)); + } + + distinctMoveList = movesAtLevel.stream().distinct().collect(Collectors.toList()); + movesLeft = distinctMoveList.size(); + + if (movesLeft <= (4 - i)) { + pickedMoves.addAll(distinctMoveList); + break; + } + } + + int movesPicked = pickedMoves.size(); + + for (int i = 0; i < 4; i++) { + if (i < movesPicked) { + tp.moves[i] = pickedMoves.get(i).number; + } else { + tp.moves[i] = 0; + } + } + } + } + setTrainers(trainers, false); + } + + private List<Move> trimMoveList(TrainerPokemon tp, List<Move> movesAtLevel, boolean doubleBattleMode) { + int movesLeft = movesAtLevel.size(); + + if (movesLeft <= 4) { + for (int i = 0; i < 4; i++) { + if (i < movesLeft) { + tp.moves[i] = movesAtLevel.get(i).number; + } else { + tp.moves[i] = 0; + } + } + return new ArrayList<>(); + } + + movesAtLevel = movesAtLevel + .stream() + .filter(mv -> !GlobalConstants.uselessMoves.contains(mv.number) && + (doubleBattleMode || !GlobalConstants.doubleBattleMoves.contains(mv.number))) + .collect(Collectors.toList()); + + movesLeft = movesAtLevel.size(); + + if (movesLeft <= 4) { + for (int i = 0; i < 4; i++) { + if (i < movesLeft) { + tp.moves[i] = movesAtLevel.get(i).number; + } else { + tp.moves[i] = 0; + } + } + return new ArrayList<>(); + } + + List<Move> obsoletedMoves = getObsoleteMoves(movesAtLevel); + + // Remove obsoleted moves + + movesAtLevel.removeAll(obsoletedMoves); + + movesLeft = movesAtLevel.size(); + + if (movesLeft <= 4) { + for (int i = 0; i < 4; i++) { + if (i < movesLeft) { + tp.moves[i] = movesAtLevel.get(i).number; + } else { + tp.moves[i] = 0; + } + } + return new ArrayList<>(); + } + + List<Move> requiresOtherMove = movesAtLevel + .stream() + .filter(mv -> GlobalConstants.requiresOtherMove.contains(mv.number)) + .collect(Collectors.toList()); + + for (Move dependentMove: requiresOtherMove) { + if (MoveSynergy.requiresOtherMove(dependentMove, movesAtLevel).isEmpty()) { + movesAtLevel.remove(dependentMove); + } + } + + movesLeft = movesAtLevel.size(); + + if (movesLeft <= 4) { + for (int i = 0; i < 4; i++) { + if (i < movesLeft) { + tp.moves[i] = movesAtLevel.get(i).number; + } else { + tp.moves[i] = 0; + } + } + return new ArrayList<>(); + } + + // Remove hard ability anti-synergy moves + + List<Move> withoutHardAntiSynergy = new ArrayList<>(movesAtLevel); + withoutHardAntiSynergy.removeAll(MoveSynergy.getHardAbilityMoveAntiSynergy( + getAbilityForTrainerPokemon(tp), + movesAtLevel)); + + if (withoutHardAntiSynergy.size() > 0) { + movesAtLevel = withoutHardAntiSynergy; + } + + movesLeft = movesAtLevel.size(); + + if (movesLeft <= 4) { + for (int i = 0; i < 4; i++) { + if (i < movesLeft) { + tp.moves[i] = movesAtLevel.get(i).number; + } else { + tp.moves[i] = 0; + } + } + return new ArrayList<>(); + } + return movesAtLevel; + } + + private List<Move> getObsoleteMoves(List<Move> movesAtLevel) { + List<Move> obsoletedMoves = new ArrayList<>(); + for (Move mv: movesAtLevel) { + if (GlobalConstants.cannotObsoleteMoves.contains(mv.number)) { + continue; + } + if (mv.power > 0) { + List<Move> obsoleteThis = movesAtLevel + .stream() + .filter(mv2 -> !GlobalConstants.cannotBeObsoletedMoves.contains(mv2.number) && + mv.type == mv2.type && + ((((mv.statChangeMoveType == mv2.statChangeMoveType && + mv.statChanges[0].equals(mv2.statChanges[0])) || + (mv2.statChangeMoveType == StatChangeMoveType.NONE_OR_UNKNOWN && + mv.hasBeneficialStatChange())) && + mv.absorbPercent >= mv2.absorbPercent && + !mv.isChargeMove && + !mv.isRechargeMove) || + mv2.power * mv2.hitCount <= 30) && + mv.hitratio >= mv2.hitratio && + mv.category == mv2.category && + mv.priority >= mv2.priority && + mv2.power > 0 && + mv.power * mv.hitCount > mv2.power * mv2.hitCount) + .collect(Collectors.toList()); + for (Move obsoleted: obsoleteThis) { + //System.out.println(obsoleted.name + " obsoleted by " + mv.name); + } + obsoletedMoves.addAll(obsoleteThis); + } else if (mv.statChangeMoveType == StatChangeMoveType.NO_DAMAGE_USER || + mv.statChangeMoveType == StatChangeMoveType.NO_DAMAGE_TARGET) { + List<Move> obsoleteThis = new ArrayList<>(); + List<Move.StatChange> statChanges1 = new ArrayList<>(); + for (Move.StatChange sc: mv.statChanges) { + if (sc.type != StatChangeType.NONE) { + statChanges1.add(sc); + } + } + for (Move mv2: movesAtLevel + .stream() + .filter(otherMv -> !otherMv.equals(mv) && + otherMv.power <= 0 && + otherMv.statChangeMoveType == mv.statChangeMoveType && + (otherMv.statusType == mv.statusType || + otherMv.statusType == StatusType.NONE)) + .collect(Collectors.toList())) { + List<Move.StatChange> statChanges2 = new ArrayList<>(); + for (Move.StatChange sc: mv2.statChanges) { + if (sc.type != StatChangeType.NONE) { + statChanges2.add(sc); + } + } + if (statChanges2.size() > statChanges1.size()) { + continue; + } + List<Move.StatChange> statChanges1Filtered = statChanges1 + .stream() + .filter(sc -> !statChanges2.contains(sc)) + .collect(Collectors.toList()); + statChanges2.removeAll(statChanges1); + if (!statChanges1Filtered.isEmpty() && statChanges2.isEmpty()) { + if (!GlobalConstants.cannotBeObsoletedMoves.contains(mv2.number)) { + obsoleteThis.add(mv2); + } + continue; + } + if (statChanges1Filtered.isEmpty() && statChanges2.isEmpty()) { + continue; + } + boolean maybeBetter = false; + for (Move.StatChange sc1: statChanges1Filtered) { + boolean canStillBeBetter = false; + for (Move.StatChange sc2: statChanges2) { + if (sc1.type == sc2.type) { + canStillBeBetter = true; + if ((mv.statChangeMoveType == StatChangeMoveType.NO_DAMAGE_USER && sc1.stages > sc2.stages) || + (mv.statChangeMoveType == StatChangeMoveType.NO_DAMAGE_TARGET && sc1.stages < sc2.stages)) { + maybeBetter = true; + } else { + canStillBeBetter = false; + } + } + } + if (!canStillBeBetter) { + maybeBetter = false; + break; + } + } + if (maybeBetter) { + if (!GlobalConstants.cannotBeObsoletedMoves.contains(mv2.number)) { + obsoleteThis.add(mv2); + } + } + } + for (Move obsoleted: obsoleteThis) { + //System.out.println(obsoleted.name + " obsoleted by " + mv.name); + } + obsoletedMoves.addAll(obsoleteThis); + } + } + + return obsoletedMoves.stream().distinct().collect(Collectors.toList()); + } + + private boolean trainerShouldNotGetBuffs(Trainer t) { + return t.tag != null && (t.tag.startsWith("RIVAL1-") || t.tag.startsWith("FRIEND1-") || t.tag.endsWith("NOTSTRONG")); + } + + public int getRandomAbilitySlot(Pokemon pokemon) { + if (abilitiesPerPokemon() == 0) { + return 0; + } + List<Integer> abilitiesList = Arrays.asList(pokemon.ability1, pokemon.ability2, pokemon.ability3); + int slot = random.nextInt(this.abilitiesPerPokemon()); + while (abilitiesList.get(slot) == 0) { + slot = random.nextInt(this.abilitiesPerPokemon()); + } + return slot + 1; + } + + public int getValidAbilitySlotFromOriginal(Pokemon pokemon, int originalAbilitySlot) { + // This is used in cases where one Trainer Pokemon evolves into another. If the unevolved Pokemon + // is using slot 2, but the evolved Pokemon doesn't actually have a second ability, then we + // want the evolved Pokemon to use slot 1 for safety's sake. + if (originalAbilitySlot == 2 && pokemon.ability2 == 0) { + return 1; + } + return originalAbilitySlot; + } + + // MOVE DATA + // All randomizers don't touch move ID 165 (Struggle) + // They also have other exclusions where necessary to stop things glitching. + + @Override + public void randomizeMovePowers() { + List<Move> moves = this.getMoves(); + for (Move mv : moves) { + if (mv != null && mv.internalId != Moves.struggle && mv.power >= 10) { + // "Generic" damaging move to randomize power + if (random.nextInt(3) != 2) { + // "Regular" move + mv.power = random.nextInt(11) * 5 + 50; // 50 ... 100 + } else { + // "Extreme" move + mv.power = random.nextInt(27) * 5 + 20; // 20 ... 150 + } + // Tiny chance for massive power jumps + for (int i = 0; i < 2; i++) { + if (random.nextInt(100) == 0) { + mv.power += 50; + } + } + + if (mv.hitCount != 1) { + // Divide randomized power by average hit count, round to + // nearest 5 + mv.power = (int) (Math.round(mv.power / mv.hitCount / 5) * 5); + if (mv.power == 0) { + mv.power = 5; + } + } + } + } + } + + @Override + public void randomizeMovePPs() { + List<Move> moves = this.getMoves(); + for (Move mv : moves) { + if (mv != null && mv.internalId != Moves.struggle) { + if (random.nextInt(3) != 2) { + // "average" PP: 15-25 + mv.pp = random.nextInt(3) * 5 + 15; + } else { + // "extreme" PP: 5-40 + mv.pp = random.nextInt(8) * 5 + 5; + } + } + } + } + + @Override + public void randomizeMoveAccuracies() { + List<Move> moves = this.getMoves(); + for (Move mv : moves) { + if (mv != null && mv.internalId != Moves.struggle && mv.hitratio >= 5) { + // "Sane" accuracy randomization + // Broken into three tiers based on original accuracy + // Designed to limit the chances of 100% accurate OHKO moves and + // keep a decent base of 100% accurate regular moves. + + if (mv.hitratio <= 50) { + // lowest tier (acc <= 50) + // new accuracy = rand(20...50) inclusive + // with a 10% chance to increase by 50% + mv.hitratio = random.nextInt(7) * 5 + 20; + if (random.nextInt(10) == 0) { + mv.hitratio = (mv.hitratio * 3 / 2) / 5 * 5; + } + } else if (mv.hitratio < 90) { + // middle tier (50 < acc < 90) + // count down from 100% to 20% in 5% increments with 20% + // chance to "stop" and use the current accuracy at each + // increment + // gives decent-but-not-100% accuracy most of the time + mv.hitratio = 100; + while (mv.hitratio > 20) { + if (random.nextInt(10) < 2) { + break; + } + mv.hitratio -= 5; + } + } else { + // highest tier (90 <= acc <= 100) + // count down from 100% to 20% in 5% increments with 40% + // chance to "stop" and use the current accuracy at each + // increment + // gives high accuracy most of the time + mv.hitratio = 100; + while (mv.hitratio > 20) { + if (random.nextInt(10) < 4) { + break; + } + mv.hitratio -= 5; + } + } + } + } + } + + @Override + public void randomizeMoveTypes() { + List<Move> moves = this.getMoves(); + for (Move mv : moves) { + if (mv != null && mv.internalId != Moves.struggle && mv.type != null) { + mv.type = randomType(); + } + } + } + + @Override + public void randomizeMoveCategory() { + if (!this.hasPhysicalSpecialSplit()) { + return; + } + List<Move> moves = this.getMoves(); + for (Move mv : moves) { + if (mv != null && mv.internalId != Moves.struggle && mv.category != MoveCategory.STATUS) { + if (random.nextInt(2) == 0) { + mv.category = (mv.category == MoveCategory.PHYSICAL) ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; + } + } + } + + } + + @Override + public void updateMoves(Settings settings) { + int generation = settings.getUpdateMovesToGeneration(); + + List<Move> moves = this.getMoves(); + + if (generation >= 2 && generationOfPokemon() < 2) { + // gen1 + // Karate Chop => FIGHTING (gen1) + updateMoveType(moves, Moves.karateChop, Type.FIGHTING); + // Gust => FLYING (gen1) + updateMoveType(moves, Moves.gust, Type.FLYING); + // Wing Attack => 60 power (gen1) + updateMovePower(moves, Moves.wingAttack, 60); + // Whirlwind => 100 accuracy (gen1) + updateMoveAccuracy(moves, Moves.whirlwind, 100); + // Sand Attack => GROUND (gen1) + updateMoveType(moves, Moves.sandAttack, Type.GROUND); + // Double-Edge => 120 power (gen1) + updateMovePower(moves, Moves.doubleEdge, 120); + // Move 44, Bite, becomes dark (but doesn't exist anyway) + // Blizzard => 70% accuracy (gen1) + updateMoveAccuracy(moves, Moves.blizzard, 70); + // Rock Throw => 90% accuracy (gen1) + updateMoveAccuracy(moves, Moves.rockThrow, 90); + // Hypnosis => 60% accuracy (gen1) + updateMoveAccuracy(moves, Moves.hypnosis, 60); + // SelfDestruct => 200power (gen1) + updateMovePower(moves, Moves.selfDestruct, 200); + // Explosion => 250 power (gen1) + updateMovePower(moves, Moves.explosion, 250); + // Dig => 60 power (gen1) + updateMovePower(moves, Moves.dig, 60); + } + + if (generation >= 3 && generationOfPokemon() < 3) { + // Razor Wind => 100% accuracy (gen1/2) + updateMoveAccuracy(moves, Moves.razorWind, 100); + // Move 67, Low Kick, has weight-based power in gen3+ + // Low Kick => 100% accuracy (gen1) + updateMoveAccuracy(moves, Moves.lowKick, 100); + } + + if (generation >= 4 && generationOfPokemon() < 4) { + // Fly => 90 power (gen1/2/3) + updateMovePower(moves, Moves.fly, 90); + // Vine Whip => 15 pp (gen1/2/3) + updateMovePP(moves, Moves.vineWhip, 15); + // Absorb => 25pp (gen1/2/3) + updateMovePP(moves, Moves.absorb, 25); + // Mega Drain => 15pp (gen1/2/3) + updateMovePP(moves, Moves.megaDrain, 15); + // Dig => 80 power (gen1/2/3) + updateMovePower(moves, Moves.dig, 80); + // Recover => 10pp (gen1/2/3) + updateMovePP(moves, Moves.recover, 10); + // Flash => 100% acc (gen1/2/3) + updateMoveAccuracy(moves, Moves.flash, 100); + // Petal Dance => 90 power (gen1/2/3) + updateMovePower(moves, Moves.petalDance, 90); + // Disable => 100% accuracy (gen1-4) + updateMoveAccuracy(moves, Moves.disable, 80); + // Jump Kick => 85 power + updateMovePower(moves, Moves.jumpKick, 85); + // Hi Jump Kick => 100 power + updateMovePower(moves, Moves.highJumpKick, 100); + + if (generationOfPokemon() >= 2) { + // Zap Cannon => 120 power (gen2-3) + updateMovePower(moves, Moves.zapCannon, 120); + // Outrage => 120 power (gen2-3) + updateMovePower(moves, Moves.outrage, 120); + updateMovePP(moves, Moves.outrage, 10); + // Giga Drain => 10pp (gen2-3) + updateMovePP(moves, Moves.gigaDrain, 10); + // Rock Smash => 40 power (gen2-3) + updateMovePower(moves, Moves.rockSmash, 40); + } + + if (generationOfPokemon() == 3) { + // Stockpile => 20 pp + updateMovePP(moves, Moves.stockpile, 20); + // Dive => 80 power + updateMovePower(moves, Moves.dive, 80); + // Leaf Blade => 90 power + updateMovePower(moves, Moves.leafBlade, 90); + } + } + + if (generation >= 5 && generationOfPokemon() < 5) { + // Bind => 85% accuracy (gen1-4) + updateMoveAccuracy(moves, Moves.bind, 85); + // Jump Kick => 10 pp, 100 power (gen1-4) + updateMovePP(moves, Moves.jumpKick, 10); + updateMovePower(moves, Moves.jumpKick, 100); + // Tackle => 50 power, 100% accuracy , gen1-4 + updateMovePower(moves, Moves.tackle, 50); + updateMoveAccuracy(moves, Moves.tackle, 100); + // Wrap => 90% accuracy (gen1-4) + updateMoveAccuracy(moves, Moves.wrap, 90); + // Thrash => 120 power, 10pp (gen1-4) + updateMovePP(moves, Moves.thrash, 10); + updateMovePower(moves, Moves.thrash, 120); + // Disable => 100% accuracy (gen1-4) + updateMoveAccuracy(moves, Moves.disable, 100); + // Petal Dance => 120power, 10pp (gen1-4) + updateMovePP(moves, Moves.petalDance, 10); + updateMovePower(moves, Moves.petalDance, 120); + // Fire Spin => 35 power, 85% acc (gen1-4) + updateMoveAccuracy(moves, Moves.fireSpin, 85); + updateMovePower(moves, Moves.fireSpin, 35); + // Toxic => 90% accuracy (gen1-4) + updateMoveAccuracy(moves, Moves.toxic, 90); + // Clamp => 15pp, 85% acc (gen1-4) + updateMoveAccuracy(moves, Moves.clamp, 85); + updateMovePP(moves, Moves.clamp, 15); + // HJKick => 130 power, 10pp (gen1-4) + updateMovePP(moves, Moves.highJumpKick, 10); + updateMovePower(moves, Moves.highJumpKick, 130); + // Glare => 90% acc (gen1-4) + updateMoveAccuracy(moves, Moves.glare, 90); + // Poison Gas => 80% acc (gen1-4) + updateMoveAccuracy(moves, Moves.poisonGas, 80); + // Crabhammer => 90% acc (gen1-4) + updateMoveAccuracy(moves, Moves.crabhammer, 90); + + if (generationOfPokemon() >= 2) { + // Curse => GHOST (gen2-4) + updateMoveType(moves, Moves.curse, Type.GHOST); + // Cotton Spore => 100% acc (gen2-4) + updateMoveAccuracy(moves, Moves.cottonSpore, 100); + // Scary Face => 100% acc (gen2-4) + updateMoveAccuracy(moves, Moves.scaryFace, 100); + // Bone Rush => 90% acc (gen2-4) + updateMoveAccuracy(moves, Moves.boneRush, 90); + // Giga Drain => 75 power (gen2-4) + updateMovePower(moves, Moves.gigaDrain, 75); + // Fury Cutter => 20 power (gen2-4) + updateMovePower(moves, Moves.furyCutter, 20); + // Future Sight => 10 pp, 100 power, 100% acc (gen2-4) + updateMovePP(moves, Moves.futureSight, 10); + updateMovePower(moves, Moves.futureSight, 100); + updateMoveAccuracy(moves, Moves.futureSight, 100); + // Whirlpool => 35 pow, 85% acc (gen2-4) + updateMovePower(moves, Moves.whirlpool, 35); + updateMoveAccuracy(moves, Moves.whirlpool, 85); + } + + if (generationOfPokemon() >= 3) { + // Uproar => 90 power (gen3-4) + updateMovePower(moves, Moves.uproar, 90); + // Sand Tomb => 35 pow, 85% acc (gen3-4) + updateMovePower(moves, Moves.sandTomb, 35); + updateMoveAccuracy(moves, Moves.sandTomb, 85); + // Bullet Seed => 25 power (gen3-4) + updateMovePower(moves, Moves.bulletSeed, 25); + // Icicle Spear => 25 power (gen3-4) + updateMovePower(moves, Moves.icicleSpear, 25); + // Covet => 60 power (gen3-4) + updateMovePower(moves, Moves.covet, 60); + // Rock Blast => 90% acc (gen3-4) + updateMoveAccuracy(moves, Moves.rockBlast, 90); + // Doom Desire => 140 pow, 100% acc, gen3-4 + updateMovePower(moves, Moves.doomDesire, 140); + updateMoveAccuracy(moves, Moves.doomDesire, 100); + } + + if (generationOfPokemon() == 4) { + // Feint => 30 pow + updateMovePower(moves, Moves.feint, 30); + // Last Resort => 140 pow + updateMovePower(moves, Moves.lastResort, 140); + // Drain Punch => 10 pp, 75 pow + updateMovePP(moves, Moves.drainPunch, 10); + updateMovePower(moves, Moves.drainPunch, 75); + // Magma Storm => 75% acc + updateMoveAccuracy(moves, Moves.magmaStorm, 75); + } + } + + if (generation >= 6 && generationOfPokemon() < 6) { + // gen 1 + // Swords Dance 20 PP + updateMovePP(moves, Moves.swordsDance, 20); + // Whirlwind can't miss + updateMoveAccuracy(moves, Moves.whirlwind, perfectAccuracy); + // Vine Whip 25 PP, 45 Power + updateMovePP(moves, Moves.vineWhip, 25); + updateMovePower(moves, Moves.vineWhip, 45); + // Pin Missile 25 Power, 95% Accuracy + updateMovePower(moves, Moves.pinMissile, 25); + updateMoveAccuracy(moves, Moves.pinMissile, 95); + // Flamethrower 90 Power + updateMovePower(moves, Moves.flamethrower, 90); + // Hydro Pump 110 Power + updateMovePower(moves, Moves.hydroPump, 110); + // Surf 90 Power + updateMovePower(moves, Moves.surf, 90); + // Ice Beam 90 Power + updateMovePower(moves, Moves.iceBeam, 90); + // Blizzard 110 Power + updateMovePower(moves, Moves.blizzard, 110); + // Growth 20 PP + updateMovePP(moves, Moves.growth, 20); + // Thunderbolt 90 Power + updateMovePower(moves, Moves.thunderbolt, 90); + // Thunder 110 Power + updateMovePower(moves, Moves.thunder, 110); + // Minimize 10 PP + updateMovePP(moves, Moves.minimize, 10); + // Barrier 20 PP + updateMovePP(moves, Moves.barrier, 20); + // Lick 30 Power + updateMovePower(moves, Moves.lick, 30); + // Smog 30 Power + updateMovePower(moves, Moves.smog, 30); + // Fire Blast 110 Power + updateMovePower(moves, Moves.fireBlast, 110); + // Skull Bash 10 PP, 130 Power + updateMovePP(moves, Moves.skullBash, 10); + updateMovePower(moves, Moves.skullBash, 130); + // Glare 100% Accuracy + updateMoveAccuracy(moves, Moves.glare, 100); + // Poison Gas 90% Accuracy + updateMoveAccuracy(moves, Moves.poisonGas, 90); + // Bubble 40 Power + updateMovePower(moves, Moves.bubble, 40); + // Psywave 100% Accuracy + updateMoveAccuracy(moves, Moves.psywave, 100); + // Acid Armor 20 PP + updateMovePP(moves, Moves.acidArmor, 20); + // Crabhammer 100 Power + updateMovePower(moves, Moves.crabhammer, 100); + + if (generationOfPokemon() >= 2) { + // Thief 25 PP, 60 Power + updateMovePP(moves, Moves.thief, 25); + updateMovePower(moves, Moves.thief, 60); + // Snore 50 Power + updateMovePower(moves, Moves.snore, 50); + // Fury Cutter 40 Power + updateMovePower(moves, Moves.furyCutter, 40); + // Future Sight 120 Power + updateMovePower(moves, Moves.futureSight, 120); + } + + if (generationOfPokemon() >= 3) { + // Heat Wave 95 Power + updateMovePower(moves, Moves.heatWave, 95); + // Will-o-Wisp 85% Accuracy + updateMoveAccuracy(moves, Moves.willOWisp, 85); + // Smellingsalt 70 Power + updateMovePower(moves, Moves.smellingSalts, 70); + // Knock off 65 Power + updateMovePower(moves, Moves.knockOff, 65); + // Meteor Mash 90 Power, 90% Accuracy + updateMovePower(moves, Moves.meteorMash, 90); + updateMoveAccuracy(moves, Moves.meteorMash, 90); + // Air Cutter 60 Power + updateMovePower(moves, Moves.airCutter, 60); + // Overheat 130 Power + updateMovePower(moves, Moves.overheat, 130); + // Rock Tomb 15 PP, 60 Power, 95% Accuracy + updateMovePP(moves, Moves.rockTomb, 15); + updateMovePower(moves, Moves.rockTomb, 60); + updateMoveAccuracy(moves, Moves.rockTomb, 95); + // Extrasensory 20 PP + updateMovePP(moves, Moves.extrasensory, 20); + // Muddy Water 90 Power + updateMovePower(moves, Moves.muddyWater, 90); + // Covet 25 PP + updateMovePP(moves, Moves.covet, 25); + } + + if (generationOfPokemon() >= 4) { + // Wake-Up Slap 70 Power + updateMovePower(moves, Moves.wakeUpSlap, 70); + // Tailwind 15 PP + updateMovePP(moves, Moves.tailwind, 15); + // Assurance 60 Power + updateMovePower(moves, Moves.assurance, 60); + // Psycho Shift 100% Accuracy + updateMoveAccuracy(moves, Moves.psychoShift, 100); + // Aura Sphere 80 Power + updateMovePower(moves, Moves.auraSphere, 80); + // Air Slash 15 PP + updateMovePP(moves, Moves.airSlash, 15); + // Dragon Pulse 85 Power + updateMovePower(moves, Moves.dragonPulse, 85); + // Power Gem 80 Power + updateMovePower(moves, Moves.powerGem, 80); + // Energy Ball 90 Power + updateMovePower(moves, Moves.energyBall, 90); + // Draco Meteor 130 Power + updateMovePower(moves, Moves.dracoMeteor, 130); + // Leaf Storm 130 Power + updateMovePower(moves, Moves.leafStorm, 130); + // Gunk Shot 80% Accuracy + updateMoveAccuracy(moves, Moves.gunkShot, 80); + // Chatter 65 Power + updateMovePower(moves, Moves.chatter, 65); + // Magma Storm 100 Power + updateMovePower(moves, Moves.magmaStorm, 100); + } + + if (generationOfPokemon() == 5) { + // Synchronoise 120 Power + updateMovePower(moves, Moves.synchronoise, 120); + // Low Sweep 65 Power + updateMovePower(moves, Moves.lowSweep, 65); + // Hex 65 Power + updateMovePower(moves, Moves.hex, 65); + // Incinerate 60 Power + updateMovePower(moves, Moves.incinerate, 60); + // Pledges 80 Power + updateMovePower(moves, Moves.waterPledge, 80); + updateMovePower(moves, Moves.firePledge, 80); + updateMovePower(moves, Moves.grassPledge, 80); + // Struggle Bug 50 Power + updateMovePower(moves, Moves.struggleBug, 50); + // Frost Breath and Storm Throw 45 Power + // Crits are 2x in these games, so we need to multiply BP by 3/4 + // Storm Throw was also updated to have a base BP of 60 + updateMovePower(moves, Moves.frostBreath, 45); + updateMovePower(moves, Moves.stormThrow, 45); + // Sacred Sword 15 PP + updateMovePP(moves, Moves.sacredSword, 15); + // Hurricane 110 Power + updateMovePower(moves, Moves.hurricane, 110); + // Techno Blast 120 Power + updateMovePower(moves, Moves.technoBlast, 120); + } + } + + if (generation >= 7 && generationOfPokemon() < 7) { + // Leech Life 80 Power, 10 PP + updateMovePower(moves, Moves.leechLife, 80); + updateMovePP(moves, Moves.leechLife, 10); + // Submission 20 PP + updateMovePP(moves, Moves.submission, 20); + // Tackle 40 Power + updateMovePower(moves, Moves.tackle, 40); + // Thunder Wave 90% Accuracy + updateMoveAccuracy(moves, Moves.thunderWave, 90); + + if (generationOfPokemon() >= 2) { + // Swagger 85% Accuracy + updateMoveAccuracy(moves, Moves.swagger, 85); + } + + if (generationOfPokemon() >= 3) { + // Knock Off 20 PP + updateMovePP(moves, Moves.knockOff, 20); + } + + if (generationOfPokemon() >= 4) { + // Dark Void 50% Accuracy + updateMoveAccuracy(moves, Moves.darkVoid, 50); + // Sucker Punch 70 Power + updateMovePower(moves, Moves.suckerPunch, 70); + } + + if (generationOfPokemon() == 6) { + // Aromatic Mist can't miss + updateMoveAccuracy(moves, Moves.aromaticMist, perfectAccuracy); + // Fell Stinger 50 Power + updateMovePower(moves, Moves.fellStinger, 50); + // Flying Press 100 Power + updateMovePower(moves, Moves.flyingPress, 100); + // Mat Block 10 PP + updateMovePP(moves, Moves.matBlock, 10); + // Mystical Fire 75 Power + updateMovePower(moves, Moves.mysticalFire, 75); + // Parabolic Charge 65 Power + updateMovePower(moves, Moves.parabolicCharge, 65); + // Topsy-Turvy can't miss + updateMoveAccuracy(moves, Moves.topsyTurvy, perfectAccuracy); + // Water Shuriken Special + updateMoveCategory(moves, Moves.waterShuriken, MoveCategory.SPECIAL); + } + } + + if (generation >= 8 && generationOfPokemon() < 8) { + if (generationOfPokemon() >= 2) { + // Rapid Spin 50 Power + updateMovePower(moves, Moves.rapidSpin, 50); + } + + if (generationOfPokemon() == 7) { + // Multi-Attack 120 Power + updateMovePower(moves, Moves.multiAttack, 120); + } + } + + if (generation >= 9 && generationOfPokemon() < 9) { + // Gen 1 + // Recover 5 PP + updateMovePP(moves, Moves.recover, 5); + // Soft-Boiled 5 PP + updateMovePP(moves, Moves.softBoiled, 5); + // Rest 5 PP + updateMovePP(moves, Moves.rest, 5); + + if (generationOfPokemon() >= 2) { + // Milk Drink 5 PP + updateMovePP(moves, Moves.milkDrink, 5); + } + + if (generationOfPokemon() >= 3) { + // Slack Off 5 PP + updateMovePP(moves, Moves.slackOff, 5); + } + + if (generationOfPokemon() >= 4) { + // Roost 5 PP + updateMovePP(moves, Moves.roost, 5); + } + + if (generationOfPokemon() >= 7) { + // Shore Up 5 PP + updateMovePP(moves, Moves.shoreUp, 5); + } + + if (generationOfPokemon() >= 8) { + // Grassy Glide 60 Power + updateMovePower(moves, Moves.grassyGlide, 60); + // Wicked Blow 75 Power + updateMovePower(moves, Moves.wickedBlow, 75); + // Glacial Lance 120 Power + updateMovePower(moves, Moves.glacialLance, 120); + } + } + } + + private Map<Integer, boolean[]> moveUpdates; + + @Override + public void initMoveUpdates() { + moveUpdates = new TreeMap<>(); + } + + @Override + public Map<Integer, boolean[]> getMoveUpdates() { + return moveUpdates; + } + + @Override + public void randomizeMovesLearnt(Settings settings) { + boolean typeThemed = settings.getMovesetsMod() == Settings.MovesetsMod.RANDOM_PREFER_SAME_TYPE; + boolean noBroken = settings.isBlockBrokenMovesetMoves(); + boolean forceStartingMoves = supportsFourStartingMoves() && settings.isStartWithGuaranteedMoves(); + int forceStartingMoveCount = settings.getGuaranteedMoveCount(); + double goodDamagingPercentage = + settings.isMovesetsForceGoodDamaging() ? settings.getMovesetsGoodDamagingPercent() / 100.0 : 0; + boolean evolutionMovesForAll = settings.isEvolutionMovesForAll(); + + // Get current sets + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + + // Build sets of moves + List<Move> validMoves = new ArrayList<>(); + List<Move> validDamagingMoves = new ArrayList<>(); + Map<Type, List<Move>> validTypeMoves = new HashMap<>(); + Map<Type, List<Move>> validTypeDamagingMoves = new HashMap<>(); + createSetsOfMoves(noBroken, validMoves, validDamagingMoves, validTypeMoves, validTypeDamagingMoves); + + for (Integer pkmnNum : movesets.keySet()) { + List<Integer> learnt = new ArrayList<>(); + List<MoveLearnt> moves = movesets.get(pkmnNum); + int lv1AttackingMove = 0; + Pokemon pkmn = findPokemonInPoolWithSpeciesID(mainPokemonListInclFormes, pkmnNum); + if (pkmn == null) { + continue; + } + + double atkSpAtkRatio = pkmn.getAttackSpecialAttackRatio(); + + // 4 starting moves? + if (forceStartingMoves) { + int lv1count = 0; + for (MoveLearnt ml : moves) { + if (ml.level == 1) { + lv1count++; + } + } + if (lv1count < forceStartingMoveCount) { + for (int i = 0; i < forceStartingMoveCount - lv1count; i++) { + MoveLearnt fakeLv1 = new MoveLearnt(); + fakeLv1.level = 1; + fakeLv1.move = 0; + moves.add(0, fakeLv1); + } + } + } + + if (evolutionMovesForAll) { + if (moves.get(0).level != 0) { + MoveLearnt fakeEvoMove = new MoveLearnt(); + fakeEvoMove.level = 0; + fakeEvoMove.move = 0; + moves.add(0, fakeEvoMove); + } + } + + if (pkmn.actuallyCosmetic) { + for (int i = 0; i < moves.size(); i++) { + moves.get(i).move = movesets.get(pkmn.baseForme.number).get(i).move; + } + continue; + } + + // Find last lv1 move + // lv1index ends up as the index of the first non-lv1 move + int lv1index = moves.get(0).level == 1 ? 0 : 1; // Evolution move handling (level 0 = evo move) + while (lv1index < moves.size() && moves.get(lv1index).level == 1) { + lv1index++; + } + + // last lv1 move is 1 before lv1index + if (lv1index != 0) { + lv1index--; + } + + // Force a certain amount of good damaging moves depending on the percentage + int goodDamagingLeft = (int)Math.round(goodDamagingPercentage * moves.size()); + + // Replace moves as needed + for (int i = 0; i < moves.size(); i++) { + // should this move be forced damaging? + boolean attemptDamaging = i == lv1index || goodDamagingLeft > 0; + + // type themed? + Type typeOfMove = null; + if (typeThemed) { + double picked = random.nextDouble(); + if ((pkmn.primaryType == Type.NORMAL && pkmn.secondaryType != null) || + (pkmn.secondaryType == Type.NORMAL)) { + + Type otherType = pkmn.primaryType == Type.NORMAL ? pkmn.secondaryType : pkmn.primaryType; + + // Normal/OTHER: 10% normal, 30% other, 60% random + if (picked < 0.1) { + typeOfMove = Type.NORMAL; + } else if (picked < 0.4) { + typeOfMove = otherType; + } + // else random + } else if (pkmn.secondaryType != null) { + // Primary/Secondary: 20% primary, 20% secondary, 60% random + if (picked < 0.2) { + typeOfMove = pkmn.primaryType; + } else if (picked < 0.4) { + typeOfMove = pkmn.secondaryType; + } + // else random + } else { + // Primary/None: 40% primary, 60% random + if (picked < 0.4) { + typeOfMove = pkmn.primaryType; + } + // else random + } + } + + // select a list to pick a move from that has at least one free + List<Move> pickList = validMoves; + if (attemptDamaging) { + if (typeOfMove != null) { + if (validTypeDamagingMoves.containsKey(typeOfMove) + && checkForUnusedMove(validTypeDamagingMoves.get(typeOfMove), learnt)) { + pickList = validTypeDamagingMoves.get(typeOfMove); + } else if (checkForUnusedMove(validDamagingMoves, learnt)) { + pickList = validDamagingMoves; + } + } else if (checkForUnusedMove(validDamagingMoves, learnt)) { + pickList = validDamagingMoves; + } + MoveCategory forcedCategory = random.nextDouble() < atkSpAtkRatio ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL; + List<Move> filteredList = pickList.stream().filter(mv -> mv.category == forcedCategory).collect(Collectors.toList()); + if (!filteredList.isEmpty() && checkForUnusedMove(filteredList, learnt)) { + pickList = filteredList; + } + } else if (typeOfMove != null) { + if (validTypeMoves.containsKey(typeOfMove) + && checkForUnusedMove(validTypeMoves.get(typeOfMove), learnt)) { + pickList = validTypeMoves.get(typeOfMove); + } + } + + // now pick a move until we get a valid one + Move mv = pickList.get(random.nextInt(pickList.size())); + while (learnt.contains(mv.number)) { + mv = pickList.get(random.nextInt(pickList.size())); + } + + if (i == lv1index) { + lv1AttackingMove = mv.number; + } else { + goodDamagingLeft--; + } + learnt.add(mv.number); + + } + + Collections.shuffle(learnt, random); + if (learnt.get(lv1index) != lv1AttackingMove) { + for (int i = 0; i < learnt.size(); i++) { + if (learnt.get(i) == lv1AttackingMove) { + learnt.set(i, learnt.get(lv1index)); + learnt.set(lv1index, lv1AttackingMove); + break; + } + } + } + + // write all moves for the pokemon + for (int i = 0; i < learnt.size(); i++) { + moves.get(i).move = learnt.get(i); + if (i == lv1index) { + // just in case, set this to lv1 + moves.get(i).level = 1; + } + } + } + // Done, save + this.setMovesLearnt(movesets); + + } + + @Override + public void randomizeEggMoves(Settings settings) { + boolean typeThemed = settings.getMovesetsMod() == Settings.MovesetsMod.RANDOM_PREFER_SAME_TYPE; + boolean noBroken = settings.isBlockBrokenMovesetMoves(); + double goodDamagingPercentage = + settings.isMovesetsForceGoodDamaging() ? settings.getMovesetsGoodDamagingPercent() / 100.0 : 0; + + // Get current sets + Map<Integer, List<Integer>> movesets = this.getEggMoves(); + + // Build sets of moves + List<Move> validMoves = new ArrayList<>(); + List<Move> validDamagingMoves = new ArrayList<>(); + Map<Type, List<Move>> validTypeMoves = new HashMap<>(); + Map<Type, List<Move>> validTypeDamagingMoves = new HashMap<>(); + createSetsOfMoves(noBroken, validMoves, validDamagingMoves, validTypeMoves, validTypeDamagingMoves); + + for (Integer pkmnNum : movesets.keySet()) { + List<Integer> learnt = new ArrayList<>(); + List<Integer> moves = movesets.get(pkmnNum); + Pokemon pkmn = findPokemonInPoolWithSpeciesID(mainPokemonListInclFormes, pkmnNum); + if (pkmn == null) { + continue; + } + + double atkSpAtkRatio = pkmn.getAttackSpecialAttackRatio(); + + if (pkmn.actuallyCosmetic) { + for (int i = 0; i < moves.size(); i++) { + moves.set(i, movesets.get(pkmn.baseForme.number).get(i)); + } + continue; + } + + // Force a certain amount of good damaging moves depending on the percentage + int goodDamagingLeft = (int)Math.round(goodDamagingPercentage * moves.size()); + + // Replace moves as needed + for (int i = 0; i < moves.size(); i++) { + // should this move be forced damaging? + boolean attemptDamaging = goodDamagingLeft > 0; + + // type themed? + Type typeOfMove = null; + if (typeThemed) { + double picked = random.nextDouble(); + if ((pkmn.primaryType == Type.NORMAL && pkmn.secondaryType != null) || + (pkmn.secondaryType == Type.NORMAL)) { + + Type otherType = pkmn.primaryType == Type.NORMAL ? pkmn.secondaryType : pkmn.primaryType; + + // Normal/OTHER: 10% normal, 30% other, 60% random + if (picked < 0.1) { + typeOfMove = Type.NORMAL; + } else if (picked < 0.4) { + typeOfMove = otherType; + } + // else random + } else if (pkmn.secondaryType != null) { + // Primary/Secondary: 20% primary, 20% secondary, 60% random + if (picked < 0.2) { + typeOfMove = pkmn.primaryType; + } else if (picked < 0.4) { + typeOfMove = pkmn.secondaryType; + } + // else random + } else { + // Primary/None: 40% primary, 60% random + if (picked < 0.4) { + typeOfMove = pkmn.primaryType; + } + // else random + } + } + + // select a list to pick a move from that has at least one free + List<Move> pickList = validMoves; + if (attemptDamaging) { + if (typeOfMove != null) { + if (validTypeDamagingMoves.containsKey(typeOfMove) + && checkForUnusedMove(validTypeDamagingMoves.get(typeOfMove), learnt)) { + pickList = validTypeDamagingMoves.get(typeOfMove); + } else if (checkForUnusedMove(validDamagingMoves, learnt)) { + pickList = validDamagingMoves; + } + } else if (checkForUnusedMove(validDamagingMoves, learnt)) { + pickList = validDamagingMoves; + } + MoveCategory forcedCategory = random.nextDouble() < atkSpAtkRatio ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL; + List<Move> filteredList = pickList.stream().filter(mv -> mv.category == forcedCategory).collect(Collectors.toList()); + if (!filteredList.isEmpty() && checkForUnusedMove(filteredList, learnt)) { + pickList = filteredList; + } + } else if (typeOfMove != null) { + if (validTypeMoves.containsKey(typeOfMove) + && checkForUnusedMove(validTypeMoves.get(typeOfMove), learnt)) { + pickList = validTypeMoves.get(typeOfMove); + } + } + + // now pick a move until we get a valid one + Move mv = pickList.get(random.nextInt(pickList.size())); + while (learnt.contains(mv.number)) { + mv = pickList.get(random.nextInt(pickList.size())); + } + + goodDamagingLeft--; + learnt.add(mv.number); + } + + // write all moves for the pokemon + Collections.shuffle(learnt, random); + for (int i = 0; i < learnt.size(); i++) { + moves.set(i, learnt.get(i)); + } + } + // Done, save + this.setEggMoves(movesets); + } + + private void createSetsOfMoves(boolean noBroken, List<Move> validMoves, List<Move> validDamagingMoves, + Map<Type, List<Move>> validTypeMoves, Map<Type, List<Move>> validTypeDamagingMoves) { + List<Move> allMoves = this.getMoves(); + List<Integer> hms = this.getHMMoves(); + Set<Integer> allBanned = new HashSet<Integer>(noBroken ? this.getGameBreakingMoves() : Collections.EMPTY_SET); + allBanned.addAll(hms); + allBanned.addAll(this.getMovesBannedFromLevelup()); + allBanned.addAll(GlobalConstants.zMoves); + allBanned.addAll(this.getIllegalMoves()); + + for (Move mv : allMoves) { + if (mv != null && !GlobalConstants.bannedRandomMoves[mv.number] && !allBanned.contains(mv.number)) { + validMoves.add(mv); + if (mv.type != null) { + if (!validTypeMoves.containsKey(mv.type)) { + validTypeMoves.put(mv.type, new ArrayList<>()); + } + validTypeMoves.get(mv.type).add(mv); + } + + if (!GlobalConstants.bannedForDamagingMove[mv.number]) { + if (mv.isGoodDamaging(perfectAccuracy)) { + validDamagingMoves.add(mv); + if (mv.type != null) { + if (!validTypeDamagingMoves.containsKey(mv.type)) { + validTypeDamagingMoves.put(mv.type, new ArrayList<>()); + } + validTypeDamagingMoves.get(mv.type).add(mv); + } + } + } + } + } + + Map<Type,Double> avgTypePowers = new TreeMap<>(); + double totalAvgPower = 0; + + for (Type type: validTypeMoves.keySet()) { + List<Move> typeMoves = validTypeMoves.get(type); + int attackingSum = 0; + for (Move typeMove: typeMoves) { + if (typeMove.power > 0) { + attackingSum += (typeMove.power * typeMove.hitCount); + } + } + double avgTypePower = (double)attackingSum / (double)typeMoves.size(); + avgTypePowers.put(type, avgTypePower); + totalAvgPower += (avgTypePower); + } + + totalAvgPower /= (double)validTypeMoves.keySet().size(); + + // Want the average power of each type to be within 25% both directions + double minAvg = totalAvgPower * 0.75; + double maxAvg = totalAvgPower * 1.25; + + // Add extra moves to type lists outside of the range to balance the average power of each type + + for (Type type: avgTypePowers.keySet()) { + double avgPowerForType = avgTypePowers.get(type); + List<Move> typeMoves = validTypeMoves.get(type); + List<Move> alreadyPicked = new ArrayList<>(); + int iterLoops = 0; + while (avgPowerForType < minAvg && iterLoops < 10000) { + final double finalAvgPowerForType = avgPowerForType; + List<Move> strongerThanAvgTypeMoves = typeMoves + .stream() + .filter(mv -> mv.power * mv.hitCount > finalAvgPowerForType) + .collect(Collectors.toList()); + if (strongerThanAvgTypeMoves.isEmpty()) break; + if (alreadyPicked.containsAll(strongerThanAvgTypeMoves)) { + alreadyPicked = new ArrayList<>(); + } else { + strongerThanAvgTypeMoves.removeAll(alreadyPicked); + } + Move extraMove = strongerThanAvgTypeMoves.get(random.nextInt(strongerThanAvgTypeMoves.size())); + avgPowerForType = (avgPowerForType * typeMoves.size() + extraMove.power * extraMove.hitCount) + / (typeMoves.size() + 1); + typeMoves.add(extraMove); + alreadyPicked.add(extraMove); + iterLoops++; + } + iterLoops = 0; + while (avgPowerForType > maxAvg && iterLoops < 10000) { + final double finalAvgPowerForType = avgPowerForType; + List<Move> weakerThanAvgTypeMoves = typeMoves + .stream() + .filter(mv -> mv.power * mv.hitCount < finalAvgPowerForType) + .collect(Collectors.toList()); + if (weakerThanAvgTypeMoves.isEmpty()) break; + if (alreadyPicked.containsAll(weakerThanAvgTypeMoves)) { + alreadyPicked = new ArrayList<>(); + } else { + weakerThanAvgTypeMoves.removeAll(alreadyPicked); + } + Move extraMove = weakerThanAvgTypeMoves.get(random.nextInt(weakerThanAvgTypeMoves.size())); + avgPowerForType = (avgPowerForType * typeMoves.size() + extraMove.power * extraMove.hitCount) + / (typeMoves.size() + 1); + typeMoves.add(extraMove); + alreadyPicked.add(extraMove); + iterLoops++; + } + } + } + + @Override + public void orderDamagingMovesByDamage() { + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + List<Move> allMoves = this.getMoves(); + for (Integer pkmn : movesets.keySet()) { + List<MoveLearnt> moves = movesets.get(pkmn); + + // Build up a list of damaging moves and their positions + List<Integer> damagingMoveIndices = new ArrayList<>(); + List<Move> damagingMoves = new ArrayList<>(); + for (int i = 0; i < moves.size(); i++) { + if (moves.get(i).level == 0) continue; // Don't reorder evolution move + Move mv = allMoves.get(moves.get(i).move); + if (mv.power > 1) { + // considered a damaging move for this purpose + damagingMoveIndices.add(i); + damagingMoves.add(mv); + } + } + + // Ties should be sorted randomly, so shuffle the list first. + Collections.shuffle(damagingMoves, random); + + // Sort the damaging moves by power + damagingMoves.sort(Comparator.comparingDouble(m -> m.power * m.hitCount)); + + // Reassign damaging moves in the ordered positions + for (int i = 0; i < damagingMoves.size(); i++) { + moves.get(damagingMoveIndices.get(i)).move = damagingMoves.get(i).number; + } + } + + // Done, save + this.setMovesLearnt(movesets); + } + + @Override + public void metronomeOnlyMode() { + + // movesets + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + + MoveLearnt metronomeML = new MoveLearnt(); + metronomeML.level = 1; + metronomeML.move = Moves.metronome; + + for (List<MoveLearnt> ms : movesets.values()) { + if (ms != null && ms.size() > 0) { + ms.clear(); + ms.add(metronomeML); + } + } + + this.setMovesLearnt(movesets); + + // trainers + // run this to remove all custom non-Metronome moves + List<Trainer> trainers = this.getTrainers(); + + for (Trainer t : trainers) { + for (TrainerPokemon tpk : t.pokemon) { + tpk.resetMoves = true; + } + } + + this.setTrainers(trainers, false); + + // tms + List<Integer> tmMoves = this.getTMMoves(); + + for (int i = 0; i < tmMoves.size(); i++) { + tmMoves.set(i, Moves.metronome); + } + + this.setTMMoves(tmMoves); + + // movetutors + if (this.hasMoveTutors()) { + List<Integer> mtMoves = this.getMoveTutorMoves(); + + for (int i = 0; i < mtMoves.size(); i++) { + mtMoves.set(i, Moves.metronome); + } + + this.setMoveTutorMoves(mtMoves); + } + + // move tweaks + List<Move> moveData = this.getMoves(); + + Move metronome = moveData.get(Moves.metronome); + + metronome.pp = 40; + + List<Integer> hms = this.getHMMoves(); + + for (int hm : hms) { + Move thisHM = moveData.get(hm); + thisHM.pp = 0; + } + } + + @Override + public void customStarters(Settings settings) { + boolean abilitiesUnchanged = settings.getAbilitiesMod() == Settings.AbilitiesMod.UNCHANGED; + int[] customStarters = settings.getCustomStarters(); + boolean allowAltFormes = settings.isAllowStarterAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + + List<Pokemon> romPokemon = getPokemonInclFormes() + .stream() + .filter(pk -> pk == null || !pk.actuallyCosmetic) + .collect(Collectors.toList()); + + List<Pokemon> banned = getBannedFormesForPlayerPokemon(); + pickedStarters = new ArrayList<>(); + if (abilitiesUnchanged) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + // loop to add chosen pokemon to banned, preventing it from being a random option. + for (int i = 0; i < customStarters.length; i = i + 1){ + if (!(customStarters[i] - 1 == 0)){ + banned.add(romPokemon.get(customStarters[i] - 1)); + } + } + if (customStarters[0] - 1 == 0){ + Pokemon pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon(); + while (pickedStarters.contains(pkmn) || banned.contains(pkmn) || pkmn.actuallyCosmetic) { + pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon(); + } + pickedStarters.add(pkmn); + } else { + Pokemon pkmn1 = romPokemon.get(customStarters[0] - 1); + pickedStarters.add(pkmn1); + } + if (customStarters[1] - 1 == 0){ + Pokemon pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon(); + while (pickedStarters.contains(pkmn) || banned.contains(pkmn) || pkmn.actuallyCosmetic) { + pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon(); + } + pickedStarters.add(pkmn); + } else { + Pokemon pkmn2 = romPokemon.get(customStarters[1] - 1); + pickedStarters.add(pkmn2); + } + + if (isYellow()) { + setStarters(pickedStarters); + } else { + if (customStarters[2] - 1 == 0){ + Pokemon pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon(); + while (pickedStarters.contains(pkmn) || banned.contains(pkmn) || pkmn.actuallyCosmetic) { + pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon(); + } + pickedStarters.add(pkmn); + } else { + Pokemon pkmn3 = romPokemon.get(customStarters[2] - 1); + pickedStarters.add(pkmn3); + } + if (starterCount() > 3) { + for (int i = 3; i < starterCount(); i++) { + Pokemon pkmn = random2EvosPokemon(allowAltFormes); + while (pickedStarters.contains(pkmn)) { + pkmn = random2EvosPokemon(allowAltFormes); + } + pickedStarters.add(pkmn); + } + setStarters(pickedStarters); + } else { + setStarters(pickedStarters); + } + } + } + + @Override + public void randomizeStarters(Settings settings) { + boolean abilitiesUnchanged = settings.getAbilitiesMod() == Settings.AbilitiesMod.UNCHANGED; + boolean allowAltFormes = settings.isAllowStarterAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + + int starterCount = starterCount(); + pickedStarters = new ArrayList<>(); + List<Pokemon> banned = getBannedFormesForPlayerPokemon(); + if (abilitiesUnchanged) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + for (int i = 0; i < starterCount; i++) { + Pokemon pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon(); + while (pickedStarters.contains(pkmn) || banned.contains(pkmn) || pkmn.actuallyCosmetic) { + pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon(); + } + pickedStarters.add(pkmn); + } + setStarters(pickedStarters); + } + + @Override + public void randomizeBasicTwoEvosStarters(Settings settings) { + boolean abilitiesUnchanged = settings.getAbilitiesMod() == Settings.AbilitiesMod.UNCHANGED; + boolean allowAltFormes = settings.isAllowStarterAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + + int starterCount = starterCount(); + pickedStarters = new ArrayList<>(); + List<Pokemon> banned = getBannedFormesForPlayerPokemon(); + if (abilitiesUnchanged) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + for (int i = 0; i < starterCount; i++) { + Pokemon pkmn = random2EvosPokemon(allowAltFormes); + while (pickedStarters.contains(pkmn) || banned.contains(pkmn)) { + pkmn = random2EvosPokemon(allowAltFormes); + } + pickedStarters.add(pkmn); + } + setStarters(pickedStarters); + } + + @Override + public List<Pokemon> getPickedStarters() { + return pickedStarters; + } + + + @Override + public void randomizeStaticPokemon(Settings settings) { + boolean swapLegendaries = settings.getStaticPokemonMod() == Settings.StaticPokemonMod.RANDOM_MATCHING; + boolean similarStrength = settings.getStaticPokemonMod() == Settings.StaticPokemonMod.SIMILAR_STRENGTH; + boolean limitMainGameLegendaries = settings.isLimitMainGameLegendaries(); + boolean limit600 = settings.isLimit600(); + boolean allowAltFormes = settings.isAllowStaticAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + boolean swapMegaEvos = settings.isSwapStaticMegaEvos(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + int levelModifier = settings.isStaticLevelModified() ? settings.getStaticLevelModifier() : 0; + boolean correctStaticMusic = settings.isCorrectStaticMusic(); + + // Load + checkPokemonRestrictions(); + List<StaticEncounter> currentStaticPokemon = this.getStaticPokemon(); + List<StaticEncounter> replacements = new ArrayList<>(); + List<Pokemon> banned = this.bannedForStaticPokemon(); + banned.addAll(this.getBannedFormesForPlayerPokemon()); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + boolean reallySwapMegaEvos = forceSwapStaticMegaEvos() || swapMegaEvos; + + Map<Integer,Integer> specialMusicStaticChanges = new HashMap<>(); + List<Integer> changeMusicStatics = new ArrayList<>(); + if (correctStaticMusic) { + changeMusicStatics = getSpecialMusicStatics(); + } + + if (swapLegendaries) { + List<Pokemon> legendariesLeft = new ArrayList<>(onlyLegendaryList); + if (allowAltFormes) { + legendariesLeft.addAll(onlyLegendaryAltsList); + legendariesLeft = + legendariesLeft + .stream() + .filter(pk -> !pk.actuallyCosmetic) + .collect(Collectors.toList()); + } + List<Pokemon> nonlegsLeft = new ArrayList<>(noLegendaryList); + if (allowAltFormes) { + nonlegsLeft.addAll(noLegendaryAltsList); + nonlegsLeft = + nonlegsLeft + .stream() + .filter(pk -> !pk.actuallyCosmetic) + .collect(Collectors.toList()); + } + List<Pokemon> ultraBeastsLeft = new ArrayList<>(ultraBeastList); + legendariesLeft.removeAll(banned); + nonlegsLeft.removeAll(banned); + ultraBeastsLeft.removeAll(banned); + + // Full pools for easier refilling later + List<Pokemon> legendariesPool = new ArrayList<>(legendariesLeft); + List<Pokemon> nonlegsPool = new ArrayList<>(nonlegsLeft); + List<Pokemon> ultraBeastsPool = new ArrayList<>(ultraBeastsLeft); + + for (StaticEncounter old : currentStaticPokemon) { + StaticEncounter newStatic = cloneStaticEncounter(old); + Pokemon newPK; + if (old.pkmn.isLegendary()) { + if (reallySwapMegaEvos && old.canMegaEvolve()) { + newPK = getMegaEvoPokemon(onlyLegendaryList, legendariesLeft, newStatic); + } else { + if (old.restrictedPool) { + newPK = getRestrictedPokemon(legendariesPool, legendariesLeft, old); + } else { + newPK = legendariesLeft.remove(this.random.nextInt(legendariesLeft.size())); + } + } + + setPokemonAndFormeForStaticEncounter(newStatic, newPK); + + if (legendariesLeft.size() == 0) { + legendariesLeft.addAll(legendariesPool); + } + } else if (ultraBeastList.contains(old.pkmn)) { + if (old.restrictedPool) { + newPK = getRestrictedPokemon(ultraBeastsPool, ultraBeastsLeft, old); + } else { + newPK = ultraBeastsLeft.remove(this.random.nextInt(ultraBeastsLeft.size())); + } + + setPokemonAndFormeForStaticEncounter(newStatic, newPK); + + if (ultraBeastsLeft.size() == 0) { + ultraBeastsLeft.addAll(ultraBeastsPool); + } + } else { + if (reallySwapMegaEvos && old.canMegaEvolve()) { + newPK = getMegaEvoPokemon(noLegendaryList, nonlegsLeft, newStatic); + } else { + if (old.restrictedPool) { + newPK = getRestrictedPokemon(nonlegsPool, nonlegsLeft, old); + } else { + newPK = nonlegsLeft.remove(this.random.nextInt(nonlegsLeft.size())); + } + } + setPokemonAndFormeForStaticEncounter(newStatic, newPK); + + if (nonlegsLeft.size() == 0) { + nonlegsLeft.addAll(nonlegsPool); + } + } + replacements.add(newStatic); + if (changeMusicStatics.contains(old.pkmn.number)) { + specialMusicStaticChanges.put(old.pkmn.number, newPK.number); + } + } + } else if (similarStrength) { + List<Pokemon> listInclFormesExclCosmetics = + mainPokemonListInclFormes + .stream() + .filter(pk -> !pk.actuallyCosmetic) + .collect(Collectors.toList()); + List<Pokemon> pokemonLeft = new ArrayList<>(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics); + pokemonLeft.removeAll(banned); + + List<Pokemon> pokemonPool = new ArrayList<>(pokemonLeft); + + List<Integer> mainGameLegendaries = getMainGameLegendaries(); + for (StaticEncounter old : currentStaticPokemon) { + StaticEncounter newStatic = cloneStaticEncounter(old); + Pokemon newPK; + Pokemon oldPK = old.pkmn; + if (old.forme > 0) { + oldPK = getAltFormeOfPokemon(oldPK, old.forme); + } + Integer oldBST = oldPK.bstForPowerLevels(); + if (oldBST >= 600 && limit600) { + if (reallySwapMegaEvos && old.canMegaEvolve()) { + newPK = getMegaEvoPokemon(mainPokemonList, pokemonLeft, newStatic); + } else { + if (old.restrictedPool) { + newPK = getRestrictedPokemon(pokemonPool, pokemonLeft, old); + } else { + newPK = pokemonLeft.remove(this.random.nextInt(pokemonLeft.size())); + } + } + setPokemonAndFormeForStaticEncounter(newStatic, newPK); + } else { + boolean limitBST = oldPK.baseForme == null ? + limitMainGameLegendaries && mainGameLegendaries.contains(oldPK.number) : + limitMainGameLegendaries && mainGameLegendaries.contains(oldPK.baseForme.number); + if (reallySwapMegaEvos && old.canMegaEvolve()) { + List<Pokemon> megaEvoPokemonLeft = + megaEvolutionsList + .stream() + .filter(mega -> mega.method == 1) + .map(mega -> mega.from) + .distinct() + .filter(pokemonLeft::contains) + .collect(Collectors.toList()); + if (megaEvoPokemonLeft.isEmpty()) { + megaEvoPokemonLeft = + megaEvolutionsList + .stream() + .filter(mega -> mega.method == 1) + .map(mega -> mega.from) + .distinct() + .filter(mainPokemonList::contains) + .collect(Collectors.toList()); + } + newPK = pickStaticPowerLvlReplacement( + megaEvoPokemonLeft, + oldPK, + true, + limitBST); + newStatic.heldItem = newPK + .megaEvolutionsFrom + .get(this.random.nextInt(newPK.megaEvolutionsFrom.size())) + .argument; + } else { + if (old.restrictedPool) { + List<Pokemon> restrictedPool = pokemonLeft + .stream() + .filter(pk -> old.restrictedList.contains(pk)) + .collect(Collectors.toList()); + if (restrictedPool.isEmpty()) { + restrictedPool = pokemonPool + .stream() + .filter(pk -> old.restrictedList.contains(pk)) + .collect(Collectors.toList()); + } + newPK = pickStaticPowerLvlReplacement( + restrictedPool, + oldPK, + false, // Allow same Pokemon just in case + limitBST); + } else { + newPK = pickStaticPowerLvlReplacement( + pokemonLeft, + oldPK, + true, + limitBST); + } + } + pokemonLeft.remove(newPK); + setPokemonAndFormeForStaticEncounter(newStatic, newPK); + } + + if (pokemonLeft.size() == 0) { + pokemonLeft.addAll(pokemonPool); + } + replacements.add(newStatic); + if (changeMusicStatics.contains(old.pkmn.number)) { + specialMusicStaticChanges.put(old.pkmn.number, newPK.number); + } + } + } else { // Completely random + List<Pokemon> listInclFormesExclCosmetics = + mainPokemonListInclFormes + .stream() + .filter(pk -> !pk.actuallyCosmetic) + .collect(Collectors.toList()); + List<Pokemon> pokemonLeft = new ArrayList<>(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics); + pokemonLeft.removeAll(banned); + + List<Pokemon> pokemonPool = new ArrayList<>(pokemonLeft); + + for (StaticEncounter old : currentStaticPokemon) { + StaticEncounter newStatic = cloneStaticEncounter(old); + Pokemon newPK; + if (reallySwapMegaEvos && old.canMegaEvolve()) { + newPK = getMegaEvoPokemon(mainPokemonList, pokemonLeft, newStatic); + } else { + if (old.restrictedPool) { + newPK = getRestrictedPokemon(pokemonPool, pokemonLeft, old); + } else { + newPK = pokemonLeft.remove(this.random.nextInt(pokemonLeft.size())); + } + } + pokemonLeft.remove(newPK); + setPokemonAndFormeForStaticEncounter(newStatic, newPK); + if (pokemonLeft.size() == 0) { + pokemonLeft.addAll(pokemonPool); + } + replacements.add(newStatic); + if (changeMusicStatics.contains(old.pkmn.number)) { + specialMusicStaticChanges.put(old.pkmn.number, newPK.number); + } + } + } + + if (levelModifier != 0) { + for (StaticEncounter se : replacements) { + if (!se.isEgg) { + se.level = Math.min(100, (int) Math.round(se.level * (1 + levelModifier / 100.0))); + se.maxLevel = Math.min(100, (int) Math.round(se.maxLevel * (1 + levelModifier / 100.0))); + for (StaticEncounter linkedStatic : se.linkedEncounters) { + if (!linkedStatic.isEgg) { + linkedStatic.level = Math.min(100, (int) Math.round(linkedStatic.level * (1 + levelModifier / 100.0))); + linkedStatic.maxLevel = Math.min(100, (int) Math.round(linkedStatic.maxLevel * (1 + levelModifier / 100.0))); + } + } + } + } + } + + if (specialMusicStaticChanges.size() > 0) { + applyCorrectStaticMusic(specialMusicStaticChanges); + } + + // Save + this.setStaticPokemon(replacements); + } + + private Pokemon getRestrictedPokemon(List<Pokemon> fullList, List<Pokemon> pokemonLeft, StaticEncounter old) { + Pokemon newPK; + List<Pokemon> restrictedPool = pokemonLeft.stream().filter(pk -> old.restrictedList.contains(pk)).collect(Collectors.toList()); + if (restrictedPool.isEmpty()) { + restrictedPool = fullList + .stream() + .filter(pk -> old.restrictedList.contains(pk)) + .collect(Collectors.toList()); + } + newPK = restrictedPool.remove(this.random.nextInt(restrictedPool.size())); + pokemonLeft.remove(newPK); + return newPK; + } + + @Override + public void onlyChangeStaticLevels(Settings settings) { + int levelModifier = settings.getStaticLevelModifier(); + + List<StaticEncounter> currentStaticPokemon = this.getStaticPokemon(); + for (StaticEncounter se : currentStaticPokemon) { + if (!se.isEgg) { + se.level = Math.min(100, (int) Math.round(se.level * (1 + levelModifier / 100.0))); + for (StaticEncounter linkedStatic : se.linkedEncounters) { + if (!linkedStatic.isEgg) { + linkedStatic.level = Math.min(100, (int) Math.round(linkedStatic.level * (1 + levelModifier / 100.0))); + } + } + } + setPokemonAndFormeForStaticEncounter(se, se.pkmn); + } + this.setStaticPokemon(currentStaticPokemon); + } + + private StaticEncounter cloneStaticEncounter(StaticEncounter old) { + StaticEncounter newStatic = new StaticEncounter(); + newStatic.pkmn = old.pkmn; + newStatic.level = old.level; + newStatic.maxLevel = old.maxLevel; + newStatic.heldItem = old.heldItem; + newStatic.isEgg = old.isEgg; + newStatic.resetMoves = true; + for (StaticEncounter oldLinked : old.linkedEncounters) { + StaticEncounter newLinked = new StaticEncounter(); + newLinked.pkmn = oldLinked.pkmn; + newLinked.level = oldLinked.level; + newLinked.maxLevel = oldLinked.maxLevel; + newLinked.heldItem = oldLinked.heldItem; + newLinked.isEgg = oldLinked.isEgg; + newLinked.resetMoves = true; + newStatic.linkedEncounters.add(newLinked); + } + return newStatic; + } + + private void setPokemonAndFormeForStaticEncounter(StaticEncounter newStatic, Pokemon pk) { + boolean checkCosmetics = true; + Pokemon newPK = pk; + int newForme = 0; + if (pk.formeNumber > 0) { + newForme = pk.formeNumber; + newPK = pk.baseForme; + checkCosmetics = false; + } + if (checkCosmetics && pk.cosmeticForms > 0) { + newForme = pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms)); + } else if (!checkCosmetics && pk.cosmeticForms > 0) { + newForme += pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms)); + } + newStatic.pkmn = newPK; + newStatic.forme = newForme; + for (StaticEncounter linked : newStatic.linkedEncounters) { + linked.pkmn = newPK; + linked.forme = newForme; + } + } + + private void setFormeForStaticEncounter(StaticEncounter newStatic, Pokemon pk) { + boolean checkCosmetics = true; + newStatic.forme = 0; + if (pk.formeNumber > 0) { + newStatic.forme = pk.formeNumber; + newStatic.pkmn = pk.baseForme; + checkCosmetics = false; + } + if (checkCosmetics && newStatic.pkmn.cosmeticForms > 0) { + newStatic.forme = newStatic.pkmn.getCosmeticFormNumber(this.random.nextInt(newStatic.pkmn.cosmeticForms)); + } else if (!checkCosmetics && pk.cosmeticForms > 0) { + newStatic.forme += pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms)); + } + } + + private Pokemon getMegaEvoPokemon(List<Pokemon> fullList, List<Pokemon> pokemonLeft, StaticEncounter newStatic) { + List<MegaEvolution> megaEvos = megaEvolutionsList; + List<Pokemon> megaEvoPokemon = + megaEvos + .stream() + .filter(mega -> mega.method == 1) + .map(mega -> mega.from) + .distinct() + .collect(Collectors.toList()); + Pokemon newPK; + List<Pokemon> megaEvoPokemonLeft = + megaEvoPokemon + .stream() + .filter(pokemonLeft::contains) + .collect(Collectors.toList()); + if (megaEvoPokemonLeft.isEmpty()) { + megaEvoPokemonLeft = megaEvoPokemon + .stream() + .filter(fullList::contains) + .collect(Collectors.toList()); + } + newPK = megaEvoPokemonLeft.remove(this.random.nextInt(megaEvoPokemonLeft.size())); + pokemonLeft.remove(newPK); + newStatic.heldItem = newPK + .megaEvolutionsFrom + .get(this.random.nextInt(newPK.megaEvolutionsFrom.size())) + .argument; + return newPK; + } + + @Override + public void randomizeTMMoves(Settings settings) { + boolean noBroken = settings.isBlockBrokenTMMoves(); + boolean preserveField = settings.isKeepFieldMoveTMs(); + double goodDamagingPercentage = settings.isTmsForceGoodDamaging() ? settings.getTmsGoodDamagingPercent() / 100.0 : 0; + + // Pick some random TM moves. + int tmCount = this.getTMCount(); + List<Move> allMoves = this.getMoves(); + List<Integer> hms = this.getHMMoves(); + List<Integer> oldTMs = this.getTMMoves(); + @SuppressWarnings("unchecked") + List<Integer> banned = new ArrayList<Integer>(noBroken ? this.getGameBreakingMoves() : Collections.EMPTY_LIST); + banned.addAll(getMovesBannedFromLevelup()); + banned.addAll(this.getIllegalMoves()); + // field moves? + List<Integer> fieldMoves = this.getFieldMoves(); + int preservedFieldMoveCount = 0; + + if (preserveField) { + List<Integer> banExistingField = new ArrayList<>(oldTMs); + banExistingField.retainAll(fieldMoves); + preservedFieldMoveCount = banExistingField.size(); + banned.addAll(banExistingField); + } + + // Determine which moves are pickable + List<Move> usableMoves = new ArrayList<>(allMoves); + usableMoves.remove(0); // remove null entry + Set<Move> unusableMoves = new HashSet<>(); + Set<Move> unusableDamagingMoves = new HashSet<>(); + + for (Move mv : usableMoves) { + if (GlobalConstants.bannedRandomMoves[mv.number] || GlobalConstants.zMoves.contains(mv.number) || + hms.contains(mv.number) || banned.contains(mv.number)) { + unusableMoves.add(mv); + } else if (GlobalConstants.bannedForDamagingMove[mv.number] || !mv.isGoodDamaging(perfectAccuracy)) { + unusableDamagingMoves.add(mv); + } + } + + usableMoves.removeAll(unusableMoves); + List<Move> usableDamagingMoves = new ArrayList<>(usableMoves); + usableDamagingMoves.removeAll(unusableDamagingMoves); + + // pick (tmCount - preservedFieldMoveCount) moves + List<Integer> pickedMoves = new ArrayList<>(); + + // Force a certain amount of good damaging moves depending on the percentage + int goodDamagingLeft = (int)Math.round(goodDamagingPercentage * (tmCount - preservedFieldMoveCount)); + + for (int i = 0; i < tmCount - preservedFieldMoveCount; i++) { + Move chosenMove; + if (goodDamagingLeft > 0 && usableDamagingMoves.size() > 0) { + chosenMove = usableDamagingMoves.get(random.nextInt(usableDamagingMoves.size())); + } else { + chosenMove = usableMoves.get(random.nextInt(usableMoves.size())); + } + pickedMoves.add(chosenMove.number); + usableMoves.remove(chosenMove); + usableDamagingMoves.remove(chosenMove); + goodDamagingLeft--; + } + + // shuffle the picked moves because high goodDamagingPercentage + // will bias them towards early numbers otherwise + + Collections.shuffle(pickedMoves, random); + + // finally, distribute them as tms + int pickedMoveIndex = 0; + List<Integer> newTMs = new ArrayList<>(); + + for (int i = 0; i < tmCount; i++) { + if (preserveField && fieldMoves.contains(oldTMs.get(i))) { + newTMs.add(oldTMs.get(i)); + } else { + newTMs.add(pickedMoves.get(pickedMoveIndex++)); + } + } + + this.setTMMoves(newTMs); + } + + @Override + public void randomizeTMHMCompatibility(Settings settings) { + boolean preferSameType = settings.getTmsHmsCompatibilityMod() == Settings.TMsHMsCompatibilityMod.RANDOM_PREFER_TYPE; + boolean followEvolutions = settings.isTmsFollowEvolutions(); + + // Get current compatibility + // increase HM chances if required early on + List<Integer> requiredEarlyOn = this.getEarlyRequiredHMMoves(); + Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility(); + List<Integer> tmHMs = new ArrayList<>(this.getTMMoves()); + tmHMs.addAll(this.getHMMoves()); + + if (followEvolutions) { + copyUpEvolutionsHelper(pk -> randomizePokemonMoveCompatibility( + pk, compat.get(pk), tmHMs, requiredEarlyOn, preferSameType), + (evFrom, evTo, toMonIsFinalEvo) -> copyPokemonMoveCompatibilityUpEvolutions( + evFrom, evTo, compat.get(evFrom), compat.get(evTo), tmHMs, preferSameType + ), null, true); + } + else { + for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) { + randomizePokemonMoveCompatibility(compatEntry.getKey(), compatEntry.getValue(), tmHMs, + requiredEarlyOn, preferSameType); + } + } + + // Set the new compatibility + this.setTMHMCompatibility(compat); + } + + private void randomizePokemonMoveCompatibility(Pokemon pkmn, boolean[] moveCompatibilityFlags, + List<Integer> moveIDs, List<Integer> prioritizedMoves, + boolean preferSameType) { + List<Move> moveData = this.getMoves(); + for (int i = 1; i <= moveIDs.size(); i++) { + int move = moveIDs.get(i - 1); + Move mv = moveData.get(move); + double probability = getMoveCompatibilityProbability( + pkmn, + mv, + prioritizedMoves.contains(move), + preferSameType + ); + moveCompatibilityFlags[i] = (this.random.nextDouble() < probability); + } + } + + private void copyPokemonMoveCompatibilityUpEvolutions(Pokemon evFrom, Pokemon evTo, boolean[] prevCompatibilityFlags, + boolean[] toCompatibilityFlags, List<Integer> moveIDs, + boolean preferSameType) { + List<Move> moveData = this.getMoves(); + for (int i = 1; i <= moveIDs.size(); i++) { + if (!prevCompatibilityFlags[i]) { + // Slight chance to gain TM/HM compatibility for a move if not learned by an earlier evolution step + // Without prefer same type: 25% chance + // With prefer same type: 10% chance, 90% chance for a type new to this evolution + int move = moveIDs.get(i - 1); + Move mv = moveData.get(move); + double probability = 0.25; + if (preferSameType) { + probability = 0.1; + if (evTo.primaryType.equals(mv.type) + && !evTo.primaryType.equals(evFrom.primaryType) && !evTo.primaryType.equals(evFrom.secondaryType) + || evTo.secondaryType != null && evTo.secondaryType.equals(mv.type) + && !evTo.secondaryType.equals(evFrom.secondaryType) && !evTo.secondaryType.equals(evFrom.primaryType)) { + probability = 0.9; + } + } + toCompatibilityFlags[i] = (this.random.nextDouble() < probability); + } + else { + toCompatibilityFlags[i] = prevCompatibilityFlags[i]; + } + } + } + + private double getMoveCompatibilityProbability(Pokemon pkmn, Move mv, boolean requiredEarlyOn, + boolean preferSameType) { + double probability = 0.5; + if (preferSameType) { + if (pkmn.primaryType.equals(mv.type) + || (pkmn.secondaryType != null && pkmn.secondaryType.equals(mv.type))) { + probability = 0.9; + } else if (mv.type != null && mv.type.equals(Type.NORMAL)) { + probability = 0.5; + } else { + probability = 0.25; + } + } + if (requiredEarlyOn) { + probability = Math.min(1.0, probability * 1.8); + } + return probability; + } + + @Override + public void fullTMHMCompatibility() { + Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility(); + for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) { + boolean[] flags = compatEntry.getValue(); + for (int i = 1; i < flags.length; i++) { + flags[i] = true; + } + } + this.setTMHMCompatibility(compat); + } + + @Override + public void ensureTMCompatSanity() { + // if a pokemon learns a move in its moveset + // and there is a TM of that move, make sure + // that TM can be learned. + Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility(); + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + List<Integer> tmMoves = this.getTMMoves(); + for (Pokemon pkmn : compat.keySet()) { + List<MoveLearnt> moveset = movesets.get(pkmn.number); + boolean[] pkmnCompat = compat.get(pkmn); + for (MoveLearnt ml : moveset) { + if (tmMoves.contains(ml.move)) { + int tmIndex = tmMoves.indexOf(ml.move); + pkmnCompat[tmIndex + 1] = true; + } + } + } + this.setTMHMCompatibility(compat); + } + + @Override + public void ensureTMEvolutionSanity() { + Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility(); + // Don't do anything with the base, just copy upwards to ensure later evolutions retain learn compatibility + copyUpEvolutionsHelper(pk -> {}, ((evFrom, evTo, toMonIsFinalEvo) -> { + boolean[] fromCompat = compat.get(evFrom); + boolean[] toCompat = compat.get(evTo); + for (int i = 1; i < toCompat.length; i++) { + toCompat[i] |= fromCompat[i]; + } + }), null, true); + this.setTMHMCompatibility(compat); + } + + @Override + public void fullHMCompatibility() { + Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility(); + int tmCount = this.getTMCount(); + for (boolean[] flags : compat.values()) { + for (int i = tmCount + 1; i < flags.length; i++) { + flags[i] = true; + } + } + + // Set the new compatibility + this.setTMHMCompatibility(compat); + } + + @Override + public void copyTMCompatibilityToCosmeticFormes() { + Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility(); + + for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) { + Pokemon pkmn = compatEntry.getKey(); + boolean[] flags = compatEntry.getValue(); + if (pkmn.actuallyCosmetic) { + boolean[] baseFlags = compat.get(pkmn.baseForme); + for (int i = 1; i < flags.length; i++) { + flags[i] = baseFlags[i]; + } + } + } + + this.setTMHMCompatibility(compat); + } + + @Override + public void randomizeMoveTutorMoves(Settings settings) { + boolean noBroken = settings.isBlockBrokenTutorMoves(); + boolean preserveField = settings.isKeepFieldMoveTutors(); + double goodDamagingPercentage = settings.isTutorsForceGoodDamaging() ? settings.getTutorsGoodDamagingPercent() / 100.0 : 0; + + if (!this.hasMoveTutors()) { + return; + } + + // Pick some random Move Tutor moves, excluding TMs. + List<Move> allMoves = this.getMoves(); + List<Integer> tms = this.getTMMoves(); + List<Integer> oldMTs = this.getMoveTutorMoves(); + int mtCount = oldMTs.size(); + List<Integer> hms = this.getHMMoves(); + @SuppressWarnings("unchecked") + List<Integer> banned = new ArrayList<Integer>(noBroken ? this.getGameBreakingMoves() : Collections.EMPTY_LIST); + banned.addAll(getMovesBannedFromLevelup()); + banned.addAll(this.getIllegalMoves()); + + // field moves? + List<Integer> fieldMoves = this.getFieldMoves(); + int preservedFieldMoveCount = 0; + if (preserveField) { + List<Integer> banExistingField = new ArrayList<>(oldMTs); + banExistingField.retainAll(fieldMoves); + preservedFieldMoveCount = banExistingField.size(); + banned.addAll(banExistingField); + } + + // Determine which moves are pickable + List<Move> usableMoves = new ArrayList<>(allMoves); + usableMoves.remove(0); // remove null entry + Set<Move> unusableMoves = new HashSet<>(); + Set<Move> unusableDamagingMoves = new HashSet<>(); + + for (Move mv : usableMoves) { + if (GlobalConstants.bannedRandomMoves[mv.number] || tms.contains(mv.number) || hms.contains(mv.number) + || banned.contains(mv.number) || GlobalConstants.zMoves.contains(mv.number)) { + unusableMoves.add(mv); + } else if (GlobalConstants.bannedForDamagingMove[mv.number] || !mv.isGoodDamaging(perfectAccuracy)) { + unusableDamagingMoves.add(mv); + } + } + + usableMoves.removeAll(unusableMoves); + List<Move> usableDamagingMoves = new ArrayList<>(usableMoves); + usableDamagingMoves.removeAll(unusableDamagingMoves); + + // pick (tmCount - preservedFieldMoveCount) moves + List<Integer> pickedMoves = new ArrayList<>(); + + // Force a certain amount of good damaging moves depending on the percentage + int goodDamagingLeft = (int)Math.round(goodDamagingPercentage * (mtCount - preservedFieldMoveCount)); + + for (int i = 0; i < mtCount - preservedFieldMoveCount; i++) { + Move chosenMove; + if (goodDamagingLeft > 0 && usableDamagingMoves.size() > 0) { + chosenMove = usableDamagingMoves.get(random.nextInt(usableDamagingMoves.size())); + } else { + chosenMove = usableMoves.get(random.nextInt(usableMoves.size())); + } + pickedMoves.add(chosenMove.number); + usableMoves.remove(chosenMove); + usableDamagingMoves.remove(chosenMove); + goodDamagingLeft--; + } + + // shuffle the picked moves because high goodDamagingPercentage + // will bias them towards early numbers otherwise + + Collections.shuffle(pickedMoves, random); + + // finally, distribute them as tutors + int pickedMoveIndex = 0; + List<Integer> newMTs = new ArrayList<>(); + + for (Integer oldMT : oldMTs) { + if (preserveField && fieldMoves.contains(oldMT)) { + newMTs.add(oldMT); + } else { + newMTs.add(pickedMoves.get(pickedMoveIndex++)); + } + } + + this.setMoveTutorMoves(newMTs); + } + + @Override + public void randomizeMoveTutorCompatibility(Settings settings) { + boolean preferSameType = settings.getMoveTutorsCompatibilityMod() == Settings.MoveTutorsCompatibilityMod.RANDOM_PREFER_TYPE; + boolean followEvolutions = settings.isTutorFollowEvolutions(); + + if (!this.hasMoveTutors()) { + return; + } + // Get current compatibility + Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility(); + List<Integer> mts = this.getMoveTutorMoves(); + + // Empty list + List<Integer> priorityTutors = new ArrayList<Integer>(); + + if (followEvolutions) { + copyUpEvolutionsHelper(pk -> randomizePokemonMoveCompatibility( + pk, compat.get(pk), mts, priorityTutors, preferSameType), + (evFrom, evTo, toMonIsFinalEvo) -> copyPokemonMoveCompatibilityUpEvolutions( + evFrom, evTo, compat.get(evFrom), compat.get(evTo), mts, preferSameType + ), null, true); + } + else { + for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) { + randomizePokemonMoveCompatibility(compatEntry.getKey(), compatEntry.getValue(), mts, priorityTutors, preferSameType); + } + } + + // Set the new compatibility + this.setMoveTutorCompatibility(compat); + } + + @Override + public void fullMoveTutorCompatibility() { + if (!this.hasMoveTutors()) { + return; + } + Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility(); + for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) { + boolean[] flags = compatEntry.getValue(); + for (int i = 1; i < flags.length; i++) { + flags[i] = true; + } + } + this.setMoveTutorCompatibility(compat); + } + + @Override + public void ensureMoveTutorCompatSanity() { + if (!this.hasMoveTutors()) { + return; + } + // if a pokemon learns a move in its moveset + // and there is a tutor of that move, make sure + // that tutor can be learned. + Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility(); + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + List<Integer> mtMoves = this.getMoveTutorMoves(); + for (Pokemon pkmn : compat.keySet()) { + List<MoveLearnt> moveset = movesets.get(pkmn.number); + boolean[] pkmnCompat = compat.get(pkmn); + for (MoveLearnt ml : moveset) { + if (mtMoves.contains(ml.move)) { + int mtIndex = mtMoves.indexOf(ml.move); + pkmnCompat[mtIndex + 1] = true; + } + } + } + this.setMoveTutorCompatibility(compat); + } + + @Override + public void ensureMoveTutorEvolutionSanity() { + if (!this.hasMoveTutors()) { + return; + } + Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility(); + // Don't do anything with the base, just copy upwards to ensure later evolutions retain learn compatibility + copyUpEvolutionsHelper(pk -> {}, ((evFrom, evTo, toMonIsFinalEvo) -> { + boolean[] fromCompat = compat.get(evFrom); + boolean[] toCompat = compat.get(evTo); + for (int i = 1; i < toCompat.length; i++) { + toCompat[i] |= fromCompat[i]; + } + }), null, true); + this.setMoveTutorCompatibility(compat); + } + + @Override + public void copyMoveTutorCompatibilityToCosmeticFormes() { + Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility(); + + for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) { + Pokemon pkmn = compatEntry.getKey(); + boolean[] flags = compatEntry.getValue(); + if (pkmn.actuallyCosmetic) { + boolean[] baseFlags = compat.get(pkmn.baseForme); + for (int i = 1; i < flags.length; i++) { + flags[i] = baseFlags[i]; + } + } + } + + this.setMoveTutorCompatibility(compat); + } + + @SuppressWarnings("unchecked") + @Override + public void randomizeTrainerNames(Settings settings) { + CustomNamesSet customNames = settings.getCustomNames(); + + if (!this.canChangeTrainerText()) { + return; + } + + // index 0 = singles, 1 = doubles + List<String>[] allTrainerNames = new List[] { new ArrayList<String>(), new ArrayList<String>() }; + Map<Integer, List<String>> trainerNamesByLength[] = new Map[] { new TreeMap<Integer, List<String>>(), + new TreeMap<Integer, List<String>>() }; + + List<String> repeatedTrainerNames = Arrays.asList(new String[] { "GRUNT", "EXECUTIVE", "SHADOW", "ADMIN", "GOON", "EMPLOYEE" }); + + // Read name lists + for (String trainername : customNames.getTrainerNames()) { + int len = this.internalStringLength(trainername); + if (len <= 10) { + allTrainerNames[0].add(trainername); + if (trainerNamesByLength[0].containsKey(len)) { + trainerNamesByLength[0].get(len).add(trainername); + } else { + List<String> namesOfThisLength = new ArrayList<>(); + namesOfThisLength.add(trainername); + trainerNamesByLength[0].put(len, namesOfThisLength); + } + } + } + + for (String trainername : customNames.getDoublesTrainerNames()) { + int len = this.internalStringLength(trainername); + if (len <= 10) { + allTrainerNames[1].add(trainername); + if (trainerNamesByLength[1].containsKey(len)) { + trainerNamesByLength[1].get(len).add(trainername); + } else { + List<String> namesOfThisLength = new ArrayList<>(); + namesOfThisLength.add(trainername); + trainerNamesByLength[1].put(len, namesOfThisLength); + } + } + } + + // Get the current trainer names data + List<String> currentTrainerNames = this.getTrainerNames(); + if (currentTrainerNames.size() == 0) { + // RBY have no trainer names + return; + } + TrainerNameMode mode = this.trainerNameMode(); + int maxLength = this.maxTrainerNameLength(); + int totalMaxLength = this.maxSumOfTrainerNameLengths(); + + boolean success = false; + int tries = 0; + + // Init the translation map and new list + Map<String, String> translation = new HashMap<>(); + List<String> newTrainerNames = new ArrayList<>(); + List<Integer> tcNameLengths = this.getTCNameLengthsByTrainer(); + + // loop until we successfully pick names that fit + // should always succeed first attempt except for gen2. + while (!success && tries < 10000) { + success = true; + translation.clear(); + newTrainerNames.clear(); + int totalLength = 0; + + // Start choosing + int tnIndex = -1; + for (String trainerName : currentTrainerNames) { + tnIndex++; + if (translation.containsKey(trainerName) && !repeatedTrainerNames.contains(trainerName.toUpperCase())) { + // use an already picked translation + newTrainerNames.add(translation.get(trainerName)); + totalLength += this.internalStringLength(translation.get(trainerName)); + } else { + int idx = trainerName.contains("&") ? 1 : 0; + List<String> pickFrom = allTrainerNames[idx]; + int intStrLen = this.internalStringLength(trainerName); + if (mode == TrainerNameMode.SAME_LENGTH) { + pickFrom = trainerNamesByLength[idx].get(intStrLen); + } + String changeTo = trainerName; + int ctl = intStrLen; + if (pickFrom != null && pickFrom.size() > 0 && intStrLen > 0) { + int innerTries = 0; + changeTo = pickFrom.get(this.cosmeticRandom.nextInt(pickFrom.size())); + ctl = this.internalStringLength(changeTo); + while ((mode == TrainerNameMode.MAX_LENGTH && ctl > maxLength) + || (mode == TrainerNameMode.MAX_LENGTH_WITH_CLASS && ctl + tcNameLengths.get(tnIndex) > maxLength)) { + innerTries++; + if (innerTries == 100) { + changeTo = trainerName; + ctl = intStrLen; + break; + } + changeTo = pickFrom.get(this.cosmeticRandom.nextInt(pickFrom.size())); + ctl = this.internalStringLength(changeTo); + } + } + translation.put(trainerName, changeTo); + newTrainerNames.add(changeTo); + totalLength += ctl; + } + + if (totalLength > totalMaxLength) { + success = false; + tries++; + break; + } + } + } + + if (!success) { + throw new RandomizationException("Could not randomize trainer names in a reasonable amount of attempts." + + "\nPlease add some shorter names to your custom trainer names."); + } + + // Done choosing, save + this.setTrainerNames(newTrainerNames); + } + + @SuppressWarnings("unchecked") + @Override + public void randomizeTrainerClassNames(Settings settings) { + CustomNamesSet customNames = settings.getCustomNames(); + + if (!this.canChangeTrainerText()) { + return; + } + + // index 0 = singles, index 1 = doubles + List<String> allTrainerClasses[] = new List[] { new ArrayList<String>(), new ArrayList<String>() }; + Map<Integer, List<String>> trainerClassesByLength[] = new Map[] { new HashMap<Integer, List<String>>(), + new HashMap<Integer, List<String>>() }; + + // Read names data + for (String trainerClassName : customNames.getTrainerClasses()) { + allTrainerClasses[0].add(trainerClassName); + int len = this.internalStringLength(trainerClassName); + if (trainerClassesByLength[0].containsKey(len)) { + trainerClassesByLength[0].get(len).add(trainerClassName); + } else { + List<String> namesOfThisLength = new ArrayList<>(); + namesOfThisLength.add(trainerClassName); + trainerClassesByLength[0].put(len, namesOfThisLength); + } + } + + for (String trainerClassName : customNames.getDoublesTrainerClasses()) { + allTrainerClasses[1].add(trainerClassName); + int len = this.internalStringLength(trainerClassName); + if (trainerClassesByLength[1].containsKey(len)) { + trainerClassesByLength[1].get(len).add(trainerClassName); + } else { + List<String> namesOfThisLength = new ArrayList<>(); + namesOfThisLength.add(trainerClassName); + trainerClassesByLength[1].put(len, namesOfThisLength); + } + } + + // Get the current trainer names data + List<String> currentClassNames = this.getTrainerClassNames(); + boolean mustBeSameLength = this.fixedTrainerClassNamesLength(); + int maxLength = this.maxTrainerClassNameLength(); + + // Init the translation map and new list + Map<String, String> translation = new HashMap<>(); + List<String> newClassNames = new ArrayList<>(); + + int numTrainerClasses = currentClassNames.size(); + List<Integer> doublesClasses = this.getDoublesTrainerClasses(); + + // Start choosing + for (int i = 0; i < numTrainerClasses; i++) { + String trainerClassName = currentClassNames.get(i); + if (translation.containsKey(trainerClassName)) { + // use an already picked translation + newClassNames.add(translation.get(trainerClassName)); + } else { + int idx = doublesClasses.contains(i) ? 1 : 0; + List<String> pickFrom = allTrainerClasses[idx]; + int intStrLen = this.internalStringLength(trainerClassName); + if (mustBeSameLength) { + pickFrom = trainerClassesByLength[idx].get(intStrLen); + } + String changeTo = trainerClassName; + if (pickFrom != null && pickFrom.size() > 0) { + changeTo = pickFrom.get(this.cosmeticRandom.nextInt(pickFrom.size())); + while (changeTo.length() > maxLength) { + changeTo = pickFrom.get(this.cosmeticRandom.nextInt(pickFrom.size())); + } + } + translation.put(trainerClassName, changeTo); + newClassNames.add(changeTo); + } + } + + // Done choosing, save + this.setTrainerClassNames(newClassNames); + } + + @Override + public void randomizeWildHeldItems(Settings settings) { + boolean banBadItems = settings.isBanBadRandomWildPokemonHeldItems(); + + List<Pokemon> pokemon = allPokemonInclFormesWithoutNull(); + ItemList possibleItems = banBadItems ? this.getNonBadItems() : this.getAllowedItems(); + for (Pokemon pk : pokemon) { + if (pk.guaranteedHeldItem == -1 && pk.commonHeldItem == -1 && pk.rareHeldItem == -1 + && pk.darkGrassHeldItem == -1) { + // No held items at all, abort + return; + } + boolean canHaveDarkGrass = pk.darkGrassHeldItem != -1; + if (pk.guaranteedHeldItem != -1) { + // Guaranteed held items are supported. + if (pk.guaranteedHeldItem > 0) { + // Currently have a guaranteed item + double decision = this.random.nextDouble(); + if (decision < 0.9) { + // Stay as guaranteed + canHaveDarkGrass = false; + pk.guaranteedHeldItem = possibleItems.randomItem(this.random); + } else { + // Change to 25% or 55% chance + pk.guaranteedHeldItem = 0; + pk.commonHeldItem = possibleItems.randomItem(this.random); + pk.rareHeldItem = possibleItems.randomItem(this.random); + while (pk.rareHeldItem == pk.commonHeldItem) { + pk.rareHeldItem = possibleItems.randomItem(this.random); + } + } + } else { + // No guaranteed item atm + double decision = this.random.nextDouble(); + if (decision < 0.5) { + // No held item at all + pk.commonHeldItem = 0; + pk.rareHeldItem = 0; + } else if (decision < 0.65) { + // Just a rare item + pk.commonHeldItem = 0; + pk.rareHeldItem = possibleItems.randomItem(this.random); + } else if (decision < 0.8) { + // Just a common item + pk.commonHeldItem = possibleItems.randomItem(this.random); + pk.rareHeldItem = 0; + } else if (decision < 0.95) { + // Both a common and rare item + pk.commonHeldItem = possibleItems.randomItem(this.random); + pk.rareHeldItem = possibleItems.randomItem(this.random); + while (pk.rareHeldItem == pk.commonHeldItem) { + pk.rareHeldItem = possibleItems.randomItem(this.random); + } + } else { + // Guaranteed item + canHaveDarkGrass = false; + pk.guaranteedHeldItem = possibleItems.randomItem(this.random); + pk.commonHeldItem = 0; + pk.rareHeldItem = 0; + } + } + } else { + // Code for no guaranteed items + double decision = this.random.nextDouble(); + if (decision < 0.5) { + // No held item at all + pk.commonHeldItem = 0; + pk.rareHeldItem = 0; + } else if (decision < 0.65) { + // Just a rare item + pk.commonHeldItem = 0; + pk.rareHeldItem = possibleItems.randomItem(this.random); + } else if (decision < 0.8) { + // Just a common item + pk.commonHeldItem = possibleItems.randomItem(this.random); + pk.rareHeldItem = 0; + } else { + // Both a common and rare item + pk.commonHeldItem = possibleItems.randomItem(this.random); + pk.rareHeldItem = possibleItems.randomItem(this.random); + while (pk.rareHeldItem == pk.commonHeldItem) { + pk.rareHeldItem = possibleItems.randomItem(this.random); + } + } + } + + if (canHaveDarkGrass) { + double dgDecision = this.random.nextDouble(); + if (dgDecision < 0.5) { + // Yes, dark grass item + pk.darkGrassHeldItem = possibleItems.randomItem(this.random); + } else { + pk.darkGrassHeldItem = 0; + } + } else if (pk.darkGrassHeldItem != -1) { + pk.darkGrassHeldItem = 0; + } + } + + } + + @Override + public void randomizeStarterHeldItems(Settings settings) { + boolean banBadItems = settings.isBanBadRandomStarterHeldItems(); + + List<Integer> oldHeldItems = this.getStarterHeldItems(); + List<Integer> newHeldItems = new ArrayList<>(); + ItemList possibleItems = banBadItems ? this.getNonBadItems() : this.getAllowedItems(); + for (int i = 0; i < oldHeldItems.size(); i++) { + newHeldItems.add(possibleItems.randomItem(this.random)); + } + this.setStarterHeldItems(newHeldItems); + } + + @Override + public void shuffleFieldItems() { + List<Integer> currentItems = this.getRegularFieldItems(); + List<Integer> currentTMs = this.getCurrentFieldTMs(); + + Collections.shuffle(currentItems, this.random); + Collections.shuffle(currentTMs, this.random); + + this.setRegularFieldItems(currentItems); + this.setFieldTMs(currentTMs); + } + + @Override + public void randomizeFieldItems(Settings settings) { + boolean banBadItems = settings.isBanBadRandomFieldItems(); + boolean distributeItemsControl = settings.getFieldItemsMod() == Settings.FieldItemsMod.RANDOM_EVEN; + boolean uniqueItems = !settings.isBalanceShopPrices(); + + ItemList possibleItems = banBadItems ? this.getNonBadItems().copy() : this.getAllowedItems().copy(); + List<Integer> currentItems = this.getRegularFieldItems(); + List<Integer> currentTMs = this.getCurrentFieldTMs(); + List<Integer> requiredTMs = this.getRequiredFieldTMs(); + List<Integer> uniqueNoSellItems = this.getUniqueNoSellItems(); + // System.out.println("distributeItemsControl: "+ distributeItemsControl); + + int fieldItemCount = currentItems.size(); + int fieldTMCount = currentTMs.size(); + int reqTMCount = requiredTMs.size(); + int totalTMCount = this.getTMCount(); + + List<Integer> newItems = new ArrayList<>(); + List<Integer> newTMs = new ArrayList<>(requiredTMs); + + // List<Integer> chosenItems = new ArrayList<Integer>(); // collecting chosenItems for later process + + if (distributeItemsControl) { + for (int i = 0; i < fieldItemCount; i++) { + int chosenItem = possibleItems.randomNonTM(this.random); + int iterNum = 0; + while ((this.getItemPlacementHistory(chosenItem) > this.getItemPlacementAverage()) && iterNum < 100) { + chosenItem = possibleItems.randomNonTM(this.random); + iterNum +=1; + } + newItems.add(chosenItem); + if (uniqueItems && uniqueNoSellItems.contains(chosenItem)) { + possibleItems.banSingles(chosenItem); + } else { + this.setItemPlacementHistory(chosenItem); + } + } + } else { + for (int i = 0; i < fieldItemCount; i++) { + int chosenItem = possibleItems.randomNonTM(this.random); + newItems.add(chosenItem); + if (uniqueItems && uniqueNoSellItems.contains(chosenItem)) { + possibleItems.banSingles(chosenItem); + } + } + } + + for (int i = reqTMCount; i < fieldTMCount; i++) { + while (true) { + int tm = this.random.nextInt(totalTMCount) + 1; + if (!newTMs.contains(tm)) { + newTMs.add(tm); + break; + } + } + } + + + Collections.shuffle(newItems, this.random); + Collections.shuffle(newTMs, this.random); + + this.setRegularFieldItems(newItems); + this.setFieldTMs(newTMs); + } + + @Override + public void randomizeIngameTrades(Settings settings) { + boolean randomizeRequest = settings.getInGameTradesMod() == Settings.InGameTradesMod.RANDOMIZE_GIVEN_AND_REQUESTED; + boolean randomNickname = settings.isRandomizeInGameTradesNicknames(); + boolean randomOT = settings.isRandomizeInGameTradesOTs(); + boolean randomStats = settings.isRandomizeInGameTradesIVs(); + boolean randomItem = settings.isRandomizeInGameTradesItems(); + CustomNamesSet customNames = settings.getCustomNames(); + + checkPokemonRestrictions(); + // Process trainer names + List<String> trainerNames = new ArrayList<>(); + // Check for the file + if (randomOT) { + int maxOT = this.maxTradeOTNameLength(); + for (String trainername : customNames.getTrainerNames()) { + int len = this.internalStringLength(trainername); + if (len <= maxOT && !trainerNames.contains(trainername)) { + trainerNames.add(trainername); + } + } + } + + // Process nicknames + List<String> nicknames = new ArrayList<>(); + // Check for the file + if (randomNickname) { + int maxNN = this.maxTradeNicknameLength(); + for (String nickname : customNames.getPokemonNicknames()) { + int len = this.internalStringLength(nickname); + if (len <= maxNN && !nicknames.contains(nickname)) { + nicknames.add(nickname); + } + } + } + + // get old trades + List<IngameTrade> trades = this.getIngameTrades(); + List<Pokemon> usedRequests = new ArrayList<>(); + List<Pokemon> usedGivens = new ArrayList<>(); + List<String> usedOTs = new ArrayList<>(); + List<String> usedNicknames = new ArrayList<>(); + ItemList possibleItems = this.getAllowedItems(); + + int nickCount = nicknames.size(); + int trnameCount = trainerNames.size(); + + for (IngameTrade trade : trades) { + // pick new given pokemon + Pokemon oldgiven = trade.givenPokemon; + Pokemon given = this.randomPokemon(); + while (usedGivens.contains(given)) { + given = this.randomPokemon(); + } + usedGivens.add(given); + trade.givenPokemon = given; + + // requested pokemon? + if (oldgiven == trade.requestedPokemon) { + // preserve trades for the same pokemon + trade.requestedPokemon = given; + } else if (randomizeRequest) { + if (trade.requestedPokemon != null) { + Pokemon request = this.randomPokemon(); + while (usedRequests.contains(request) || request == given) { + request = this.randomPokemon(); + } + usedRequests.add(request); + trade.requestedPokemon = request; + } + } + + // nickname? + if (randomNickname && nickCount > usedNicknames.size()) { + String nickname = nicknames.get(this.random.nextInt(nickCount)); + while (usedNicknames.contains(nickname)) { + nickname = nicknames.get(this.random.nextInt(nickCount)); + } + usedNicknames.add(nickname); + trade.nickname = nickname; + } else if (trade.nickname.equalsIgnoreCase(oldgiven.name)) { + // change the name for sanity + trade.nickname = trade.givenPokemon.name; + } + + if (randomOT && trnameCount > usedOTs.size()) { + String ot = trainerNames.get(this.random.nextInt(trnameCount)); + while (usedOTs.contains(ot)) { + ot = trainerNames.get(this.random.nextInt(trnameCount)); + } + usedOTs.add(ot); + trade.otName = ot; + trade.otId = this.random.nextInt(65536); + } + + if (randomStats) { + int maxIV = this.hasDVs() ? 16 : 32; + for (int i = 0; i < trade.ivs.length; i++) { + trade.ivs[i] = this.random.nextInt(maxIV); + } + } + + if (randomItem) { + trade.item = possibleItems.randomItem(this.random); + } + } + + // things that the game doesn't support should just be ignored + this.setIngameTrades(trades); + } + + @Override + public void condenseLevelEvolutions(int maxLevel, int maxIntermediateLevel) { + List<Pokemon> allPokemon = this.getPokemon(); + // search for level evolutions + for (Pokemon pk : allPokemon) { + if (pk != null) { + for (Evolution checkEvo : pk.evolutionsFrom) { + if (checkEvo.type.usesLevel()) { + // If evo is intermediate and too high, bring it down + // Else if it's just too high, bring it down + if (checkEvo.extraInfo > maxIntermediateLevel && checkEvo.to.evolutionsFrom.size() > 0) { + checkEvo.extraInfo = maxIntermediateLevel; + addEvoUpdateCondensed(easierEvolutionUpdates, checkEvo, false); + } else if (checkEvo.extraInfo > maxLevel) { + checkEvo.extraInfo = maxLevel; + addEvoUpdateCondensed(easierEvolutionUpdates, checkEvo, false); + } + } + if (checkEvo.type == EvolutionType.LEVEL_UPSIDE_DOWN) { + checkEvo.type = EvolutionType.LEVEL; + addEvoUpdateCondensed(easierEvolutionUpdates, checkEvo, false); + } + } + } + } + + } + + @Override + public Set<EvolutionUpdate> getImpossibleEvoUpdates() { + return impossibleEvolutionUpdates; + } + + @Override + public Set<EvolutionUpdate> getEasierEvoUpdates() { + return easierEvolutionUpdates; + } + + @Override + public Set<EvolutionUpdate> getTimeBasedEvoUpdates() { + return timeBasedEvolutionUpdates; + } + + @Override + public void randomizeEvolutions(Settings settings) { + boolean similarStrength = settings.isEvosSimilarStrength(); + boolean sameType = settings.isEvosSameTyping(); + boolean limitToThreeStages = settings.isEvosMaxThreeStages(); + boolean forceChange = settings.isEvosForceChange(); + boolean allowAltFormes = settings.isEvosAllowAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + + checkPokemonRestrictions(); + List<Pokemon> pokemonPool; + if (this.altFormesCanHaveDifferentEvolutions()) { + pokemonPool = new ArrayList<>(mainPokemonListInclFormes); + } else { + pokemonPool = new ArrayList<>(mainPokemonList); + } + List<Pokemon> actuallyCosmeticPokemonPool = new ArrayList<>(); + int stageLimit = limitToThreeStages ? 3 : 10; + + List<Pokemon> banned = this.getBannedFormesForPlayerPokemon(); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + + for (int i = 0; i < pokemonPool.size(); i++) { + Pokemon pk = pokemonPool.get(i); + if (pk.actuallyCosmetic) { + pokemonPool.remove(pk); + i--; + actuallyCosmeticPokemonPool.add(pk); + } + } + + // Cache old evolutions for data later + Map<Pokemon, List<Evolution>> originalEvos = new HashMap<>(); + for (Pokemon pk : pokemonPool) { + originalEvos.put(pk, new ArrayList<>(pk.evolutionsFrom)); + } + + Set<EvolutionPair> newEvoPairs = new HashSet<>(); + Set<EvolutionPair> oldEvoPairs = new HashSet<>(); + + if (forceChange) { + for (Pokemon pk : pokemonPool) { + for (Evolution ev : pk.evolutionsFrom) { + oldEvoPairs.add(new EvolutionPair(ev.from, ev.to)); + if (generationOfPokemon() >= 7 && ev.from.number == Species.cosmoem) { // Special case for Cosmoem to add Lunala/Solgaleo since we remove the split evo + int oppositeVersionLegendary = ev.to.number == Species.solgaleo ? Species.lunala : Species.solgaleo; + Pokemon toPkmn = findPokemonInPoolWithSpeciesID(pokemonPool, oppositeVersionLegendary); + if (toPkmn != null) { + oldEvoPairs.add(new EvolutionPair(ev.from, toPkmn)); + } + } + } + } + } + + List<Pokemon> replacements = new ArrayList<>(); + + int loops = 0; + while (loops < 1) { + // Setup for this loop. + boolean hadError = false; + for (Pokemon pk : pokemonPool) { + pk.evolutionsFrom.clear(); + pk.evolutionsTo.clear(); + } + newEvoPairs.clear(); + + // Shuffle pokemon list so the results aren't overly predictable. + Collections.shuffle(pokemonPool, this.random); + + for (Pokemon fromPK : pokemonPool) { + List<Evolution> oldEvos = originalEvos.get(fromPK); + for (Evolution ev : oldEvos) { + // Pick a Pokemon as replacement + replacements.clear(); + + List<Pokemon> chosenList = + allowAltFormes ? + mainPokemonListInclFormes + .stream() + .filter(pk -> !pk.actuallyCosmetic) + .collect(Collectors.toList()) : + mainPokemonList; + // Step 1: base filters + for (Pokemon pk : chosenList) { + // Prevent evolving into oneself (mandatory) + if (pk == fromPK) { + continue; + } + + // Force same EXP curve (mandatory) + if (pk.growthCurve != fromPK.growthCurve) { + continue; + } + + // Prevent evolving into banned Pokemon (mandatory) + if (banned.contains(pk)) { + continue; + } + + EvolutionPair ep = new EvolutionPair(fromPK, pk); + // Prevent split evos choosing the same Pokemon + // (mandatory) + if (newEvoPairs.contains(ep)) { + continue; + } + + // Prevent evolving into old thing if flagged + if (forceChange && oldEvoPairs.contains(ep)) { + continue; + } + + // Prevent evolution that causes cycle (mandatory) + if (evoCycleCheck(fromPK, pk)) { + continue; + } + + // Prevent evolution that exceeds stage limit + Evolution tempEvo = new Evolution(fromPK, pk, false, EvolutionType.NONE, 0); + fromPK.evolutionsFrom.add(tempEvo); + pk.evolutionsTo.add(tempEvo); + boolean exceededLimit = false; + + Set<Pokemon> related = relatedPokemon(fromPK); + + for (Pokemon pk2 : related) { + int numPreEvos = numPreEvolutions(pk2, stageLimit); + if (numPreEvos >= stageLimit) { + exceededLimit = true; + break; + } else if (numPreEvos == stageLimit - 1 && pk2.evolutionsFrom.size() == 0 + && originalEvos.get(pk2).size() > 0) { + exceededLimit = true; + break; + } + } + + fromPK.evolutionsFrom.remove(tempEvo); + pk.evolutionsTo.remove(tempEvo); + + if (exceededLimit) { + continue; + } + + // Passes everything, add as a candidate. + replacements.add(pk); + } + + // If we don't have any candidates after Step 1, severe + // failure + // exit out of this loop and try again from scratch + if (replacements.size() == 0) { + hadError = true; + break; + } + + // Step 2: filter by type, if needed + if (replacements.size() > 1 && sameType) { + Set<Pokemon> includeType = new HashSet<>(); + for (Pokemon pk : replacements) { + // Special case for Eevee + if (fromPK.number == Species.eevee) { + if (pk.primaryType == ev.to.primaryType + || (pk.secondaryType != null) && pk.secondaryType == ev.to.primaryType) { + includeType.add(pk); + } + } else if (pk.primaryType == fromPK.primaryType + || (fromPK.secondaryType != null && pk.primaryType == fromPK.secondaryType) + || (pk.secondaryType != null && pk.secondaryType == fromPK.primaryType) + || (fromPK.secondaryType != null && pk.secondaryType != null && pk.secondaryType == fromPK.secondaryType)) { + includeType.add(pk); + } + } + + if (includeType.size() != 0) { + replacements.retainAll(includeType); + } + } + + if (!alreadyPicked.containsAll(replacements) && !similarStrength) { + replacements.removeAll(alreadyPicked); + } + + // Step 3: pick - by similar strength or otherwise + Pokemon picked; + + if (replacements.size() == 1) { + // Foregone conclusion. + picked = replacements.get(0); + alreadyPicked.add(picked); + } else if (similarStrength) { + picked = pickEvoPowerLvlReplacement(replacements, ev.to); + alreadyPicked.add(picked); + } else { + picked = replacements.get(this.random.nextInt(replacements.size())); + alreadyPicked.add(picked); + } + + // Step 4: add it to the new evos pool + Evolution newEvo = new Evolution(fromPK, picked, ev.carryStats, ev.type, ev.extraInfo); + boolean checkCosmetics = true; + if (picked.formeNumber > 0) { + newEvo.forme = picked.formeNumber; + newEvo.formeSuffix = picked.formeSuffix; + checkCosmetics = false; + } + if (checkCosmetics && newEvo.to.cosmeticForms > 0) { + newEvo.forme = newEvo.to.getCosmeticFormNumber(this.random.nextInt(newEvo.to.cosmeticForms)); + } else if (!checkCosmetics && picked.cosmeticForms > 0) { + newEvo.forme += picked.getCosmeticFormNumber(this.random.nextInt(picked.cosmeticForms)); + } + if (newEvo.type == EvolutionType.LEVEL_FEMALE_ESPURR) { + newEvo.type = EvolutionType.LEVEL_FEMALE_ONLY; + } + fromPK.evolutionsFrom.add(newEvo); + picked.evolutionsTo.add(newEvo); + newEvoPairs.add(new EvolutionPair(fromPK, picked)); + } + + if (hadError) { + // No need to check the other Pokemon if we already errored + break; + } + } + + // If no error, done and return + if (!hadError) { + for (Pokemon pk: actuallyCosmeticPokemonPool) { + pk.copyBaseFormeEvolutions(pk.baseForme); + } + return; + } else { + loops++; + } + } + + // If we made it out of the loop, we weren't able to randomize evos. + throw new RandomizationException("Not able to randomize evolutions in a sane amount of retries."); + } + + @Override + public void randomizeEvolutionsEveryLevel(Settings settings) { + boolean sameType = settings.isEvosSameTyping(); + boolean forceChange = settings.isEvosForceChange(); + boolean allowAltFormes = settings.isEvosAllowAltFormes(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + + checkPokemonRestrictions(); + List<Pokemon> pokemonPool; + if (this.altFormesCanHaveDifferentEvolutions()) { + pokemonPool = new ArrayList<>(mainPokemonListInclFormes); + } else { + pokemonPool = new ArrayList<>(mainPokemonList); + } + List<Pokemon> actuallyCosmeticPokemonPool = new ArrayList<>(); + + List<Pokemon> banned = this.getBannedFormesForPlayerPokemon(); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + + for (int i = 0; i < pokemonPool.size(); i++) { + Pokemon pk = pokemonPool.get(i); + if (pk.actuallyCosmetic) { + pokemonPool.remove(pk); + i--; + actuallyCosmeticPokemonPool.add(pk); + } + } + + Set<EvolutionPair> oldEvoPairs = new HashSet<>(); + + if (forceChange) { + for (Pokemon pk : pokemonPool) { + for (Evolution ev : pk.evolutionsFrom) { + oldEvoPairs.add(new EvolutionPair(ev.from, ev.to)); + if (generationOfPokemon() >= 7 && ev.from.number == Species.cosmoem) { // Special case for Cosmoem to add Lunala/Solgaleo since we remove the split evo + int oppositeVersionLegendary = ev.to.number == Species.solgaleo ? Species.lunala : Species.solgaleo; + Pokemon toPkmn = findPokemonInPoolWithSpeciesID(pokemonPool, oppositeVersionLegendary); + if (toPkmn != null) { + oldEvoPairs.add(new EvolutionPair(ev.from, toPkmn)); + } + } + } + } + } + + List<Pokemon> replacements = new ArrayList<>(); + + int loops = 0; + while (loops < 1) { + // Setup for this loop. + boolean hadError = false; + for (Pokemon pk : pokemonPool) { + pk.evolutionsFrom.clear(); + pk.evolutionsTo.clear(); + } + + // Shuffle pokemon list so the results aren't overly predictable. + Collections.shuffle(pokemonPool, this.random); + + for (Pokemon fromPK : pokemonPool) { + // Pick a Pokemon as replacement + replacements.clear(); + + List<Pokemon> chosenList = + allowAltFormes ? + mainPokemonListInclFormes + .stream() + .filter(pk -> !pk.actuallyCosmetic) + .collect(Collectors.toList()) : + mainPokemonList; + // Step 1: base filters + for (Pokemon pk : chosenList) { + // Prevent evolving into oneself (mandatory) + if (pk == fromPK) { + continue; + } + + // Force same EXP curve (mandatory) + if (pk.growthCurve != fromPK.growthCurve) { + continue; + } + + // Prevent evolving into banned Pokemon (mandatory) + if (banned.contains(pk)) { + continue; + } + + // Prevent evolving into old thing if flagged + EvolutionPair ep = new EvolutionPair(fromPK, pk); + if (forceChange && oldEvoPairs.contains(ep)) { + continue; + } + + // Passes everything, add as a candidate. + replacements.add(pk); + } + + // If we don't have any candidates after Step 1, severe failure + // exit out of this loop and try again from scratch + if (replacements.size() == 0) { + hadError = true; + break; + } + + // Step 2: filter by type, if needed + if (replacements.size() > 1 && sameType) { + Set<Pokemon> includeType = new HashSet<>(); + for (Pokemon pk : replacements) { + if (pk.primaryType == fromPK.primaryType + || (fromPK.secondaryType != null && pk.primaryType == fromPK.secondaryType) + || (pk.secondaryType != null && pk.secondaryType == fromPK.primaryType) + || (pk.secondaryType != null && pk.secondaryType == fromPK.secondaryType)) { + includeType.add(pk); + } + } + + if (includeType.size() != 0) { + replacements.retainAll(includeType); + } + } + + // Step 3: pick - by similar strength or otherwise + Pokemon picked; + + if (replacements.size() == 1) { + // Foregone conclusion. + picked = replacements.get(0); + } else { + picked = replacements.get(this.random.nextInt(replacements.size())); + } + + // Step 4: create new level 1 evo and add it to the new evos pool + Evolution newEvo = new Evolution(fromPK, picked, false, EvolutionType.LEVEL, 1); + newEvo.level = 1; + boolean checkCosmetics = true; + if (picked.formeNumber > 0) { + newEvo.forme = picked.formeNumber; + newEvo.formeSuffix = picked.formeSuffix; + checkCosmetics = false; + } + if (checkCosmetics && newEvo.to.cosmeticForms > 0) { + newEvo.forme = newEvo.to.getCosmeticFormNumber(this.random.nextInt(newEvo.to.cosmeticForms)); + } else if (!checkCosmetics && picked.cosmeticForms > 0) { + newEvo.forme += picked.getCosmeticFormNumber(this.random.nextInt(picked.cosmeticForms)); + } + fromPK.evolutionsFrom.add(newEvo); + picked.evolutionsTo.add(newEvo); + } + + // If no error, done and return + if (!hadError) { + for (Pokemon pk: actuallyCosmeticPokemonPool) { + pk.copyBaseFormeEvolutions(pk.baseForme); + } + return; + } else { + loops++; + } + } + + // If we made it out of the loop, we weren't able to randomize evos. + throw new RandomizationException("Not able to randomize evolutions in a sane amount of retries."); + } + + @Override + public void changeCatchRates(Settings settings) { + int minimumCatchRateLevel = settings.getMinimumCatchRateLevel(); + + if (minimumCatchRateLevel == 5) { + enableGuaranteedPokemonCatching(); + } else { + int normalMin, legendaryMin; + switch (minimumCatchRateLevel) { + case 1: + default: + normalMin = 75; + legendaryMin = 37; + break; + case 2: + normalMin = 128; + legendaryMin = 64; + break; + case 3: + normalMin = 200; + legendaryMin = 100; + break; + case 4: + normalMin = legendaryMin = 255; + break; + } + minimumCatchRate(normalMin, legendaryMin); + } + } + + @Override + public void shuffleShopItems() { + Map<Integer, Shop> currentItems = this.getShopItems(); + if (currentItems == null) return; + List<Integer> itemList = new ArrayList<>(); + for (Shop shop: currentItems.values()) { + itemList.addAll(shop.items); + } + Collections.shuffle(itemList, this.random); + + Iterator<Integer> itemListIter = itemList.iterator(); + + for (Shop shop: currentItems.values()) { + for (int i = 0; i < shop.items.size(); i++) { + shop.items.remove(i); + shop.items.add(i, itemListIter.next()); + } + } + + this.setShopItems(currentItems); + } + + // Note: If you use this on a game where the amount of randomizable shop items is greater than the amount of + // possible items, you will get owned by the while loop + @Override + public void randomizeShopItems(Settings settings) { + boolean banBadItems = settings.isBanBadRandomShopItems(); + boolean banRegularShopItems = settings.isBanRegularShopItems(); + boolean banOPShopItems = settings.isBanOPShopItems(); + boolean balancePrices = settings.isBalanceShopPrices(); + boolean placeEvolutionItems = settings.isGuaranteeEvolutionItems(); + boolean placeXItems = settings.isGuaranteeXItems(); + + if (this.getShopItems() == null) return; + ItemList possibleItems = banBadItems ? this.getNonBadItems() : this.getAllowedItems(); + if (banRegularShopItems) { + possibleItems.banSingles(this.getRegularShopItems().stream().mapToInt(Integer::intValue).toArray()); + } + if (banOPShopItems) { + possibleItems.banSingles(this.getOPShopItems().stream().mapToInt(Integer::intValue).toArray()); + } + Map<Integer, Shop> currentItems = this.getShopItems(); + + int shopItemCount = currentItems.values().stream().mapToInt(s -> s.items.size()).sum(); + + List<Integer> newItems = new ArrayList<>(); + Map<Integer, Shop> newItemsMap = new TreeMap<>(); + int newItem; + List<Integer> guaranteedItems = new ArrayList<>(); + if (placeEvolutionItems) { + guaranteedItems.addAll(getEvolutionItems()); + } + if (placeXItems) { + guaranteedItems.addAll(getXItems()); + } + if (placeEvolutionItems || placeXItems) { + newItems.addAll(guaranteedItems); + shopItemCount = shopItemCount - newItems.size(); + + for (int i = 0; i < shopItemCount; i++) { + while (newItems.contains(newItem = possibleItems.randomNonTM(this.random))); + newItems.add(newItem); + } + + // Guarantee main-game + List<Integer> mainGameShops = new ArrayList<>(); + List<Integer> nonMainGameShops = new ArrayList<>(); + for (int i: currentItems.keySet()) { + if (currentItems.get(i).isMainGame) { + mainGameShops.add(i); + } else { + nonMainGameShops.add(i); + } + } + + // Place items in non-main-game shops; skip over guaranteed items + Collections.shuffle(newItems, this.random); + for (int i: nonMainGameShops) { + int j = 0; + List<Integer> newShopItems = new ArrayList<>(); + Shop oldShop = currentItems.get(i); + for (Integer ignored: oldShop.items) { + Integer item = newItems.get(j); + while (guaranteedItems.contains(item)) { + j++; + item = newItems.get(j); + } + newShopItems.add(item); + newItems.remove(item); + } + Shop shop = new Shop(oldShop); + shop.items = newShopItems; + newItemsMap.put(i, shop); + } + + // Place items in main-game shops + Collections.shuffle(newItems, this.random); + for (int i: mainGameShops) { + List<Integer> newShopItems = new ArrayList<>(); + Shop oldShop = currentItems.get(i); + for (Integer ignored: oldShop.items) { + Integer item = newItems.get(0); + newShopItems.add(item); + newItems.remove(0); + } + Shop shop = new Shop(oldShop); + shop.items = newShopItems; + newItemsMap.put(i, shop); + } + } else { + for (int i = 0; i < shopItemCount; i++) { + while (newItems.contains(newItem = possibleItems.randomNonTM(this.random))); + newItems.add(newItem); + } + + Iterator<Integer> newItemsIter = newItems.iterator(); + + for (int i: currentItems.keySet()) { + List<Integer> newShopItems = new ArrayList<>(); + Shop oldShop = currentItems.get(i); + for (Integer ignored: oldShop.items) { + newShopItems.add(newItemsIter.next()); + } + Shop shop = new Shop(oldShop); + shop.items = newShopItems; + newItemsMap.put(i, shop); + } + } + + this.setShopItems(newItemsMap); + if (balancePrices) { + this.setShopPrices(); + } + } + + @Override + public void randomizePickupItems(Settings settings) { + boolean banBadItems = settings.isBanBadRandomPickupItems(); + + ItemList possibleItems = banBadItems ? this.getNonBadItems() : this.getAllowedItems(); + List<PickupItem> currentItems = this.getPickupItems(); + List<PickupItem> newItems = new ArrayList<>(); + for (int i = 0; i < currentItems.size(); i++) { + int item; + if (this.generationOfPokemon() == 3 || this.generationOfPokemon() == 4) { + // Allow TMs in Gen 3/4 since they aren't infinite (and you get TMs from Pickup in the vanilla game) + item = possibleItems.randomItem(this.random); + } else { + item = possibleItems.randomNonTM(this.random); + } + PickupItem pickupItem = new PickupItem(item); + pickupItem.probabilities = Arrays.copyOf(currentItems.get(i).probabilities, currentItems.size()); + newItems.add(pickupItem); + } + + this.setPickupItems(newItems); + } + + @Override + public void minimumCatchRate(int rateNonLegendary, int rateLegendary) { + List<Pokemon> pokes = getPokemonInclFormes(); + for (Pokemon pkmn : pokes) { + if (pkmn == null) { + continue; + } + int minCatchRate = pkmn.isLegendary() ? rateLegendary : rateNonLegendary; + pkmn.catchRate = Math.max(pkmn.catchRate, minCatchRate); + } + + } + + @Override + public void standardizeEXPCurves(Settings settings) { + Settings.ExpCurveMod mod = settings.getExpCurveMod(); + ExpCurve expCurve = settings.getSelectedEXPCurve(); + + List<Pokemon> pokes = getPokemonInclFormes(); + switch (mod) { + case LEGENDARIES: + for (Pokemon pkmn : pokes) { + if (pkmn == null) { + continue; + } + pkmn.growthCurve = pkmn.isLegendary() ? ExpCurve.SLOW : expCurve; + } + break; + case STRONG_LEGENDARIES: + for (Pokemon pkmn : pokes) { + if (pkmn == null) { + continue; + } + pkmn.growthCurve = pkmn.isStrongLegendary() ? ExpCurve.SLOW : expCurve; + } + break; + case ALL: + for (Pokemon pkmn : pokes) { + if (pkmn == null) { + continue; + } + pkmn.growthCurve = expCurve; + } + break; + } + } + + /* Private methods/structs used internally by the above methods */ + + private void updateMovePower(List<Move> moves, int moveNum, int power) { + Move mv = moves.get(moveNum); + if (mv.power != power) { + mv.power = power; + addMoveUpdate(moveNum, 0); + } + } + + private void updateMovePP(List<Move> moves, int moveNum, int pp) { + Move mv = moves.get(moveNum); + if (mv.pp != pp) { + mv.pp = pp; + addMoveUpdate(moveNum, 1); + } + } + + private void updateMoveAccuracy(List<Move> moves, int moveNum, int accuracy) { + Move mv = moves.get(moveNum); + if (Math.abs(mv.hitratio - accuracy) >= 1) { + mv.hitratio = accuracy; + addMoveUpdate(moveNum, 2); + } + } + + private void updateMoveType(List<Move> moves, int moveNum, Type type) { + Move mv = moves.get(moveNum); + if (mv.type != type) { + mv.type = type; + addMoveUpdate(moveNum, 3); + } + } + + private void updateMoveCategory(List<Move> moves, int moveNum, MoveCategory category) { + Move mv = moves.get(moveNum); + if (mv.category != category) { + mv.category = category; + addMoveUpdate(moveNum, 4); + } + } + + private void addMoveUpdate(int moveNum, int updateType) { + if (!moveUpdates.containsKey(moveNum)) { + boolean[] updateField = new boolean[5]; + updateField[updateType] = true; + moveUpdates.put(moveNum, updateField); + } else { + moveUpdates.get(moveNum)[updateType] = true; + } + } + + protected Set<EvolutionUpdate> impossibleEvolutionUpdates = new TreeSet<>(); + protected Set<EvolutionUpdate> timeBasedEvolutionUpdates = new TreeSet<>(); + protected Set<EvolutionUpdate> easierEvolutionUpdates = new TreeSet<>(); + + protected void addEvoUpdateLevel(Set<EvolutionUpdate> evolutionUpdates, Evolution evo) { + Pokemon pkFrom = evo.from; + Pokemon pkTo = evo.to; + int level = evo.extraInfo; + evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.LEVEL, String.valueOf(level), + false, false)); + } + + protected void addEvoUpdateStone(Set<EvolutionUpdate> evolutionUpdates, Evolution evo, String item) { + Pokemon pkFrom = evo.from; + Pokemon pkTo = evo.to; + evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.STONE, item, + false, false)); + } + + protected void addEvoUpdateHappiness(Set<EvolutionUpdate> evolutionUpdates, Evolution evo) { + Pokemon pkFrom = evo.from; + Pokemon pkTo = evo.to; + evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.HAPPINESS, "", + false, false)); + } + + protected void addEvoUpdateHeldItem(Set<EvolutionUpdate> evolutionUpdates, Evolution evo, String item) { + Pokemon pkFrom = evo.from; + Pokemon pkTo = evo.to; + evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.LEVEL_ITEM_DAY, item, + false, false)); + } + + protected void addEvoUpdateParty(Set<EvolutionUpdate> evolutionUpdates, Evolution evo, String otherPk) { + Pokemon pkFrom = evo.from; + Pokemon pkTo = evo.to; + evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.LEVEL_WITH_OTHER, otherPk, + false, false)); + } + + protected void addEvoUpdateCondensed(Set<EvolutionUpdate> evolutionUpdates, Evolution evo, boolean additional) { + Pokemon pkFrom = evo.from; + Pokemon pkTo = evo.to; + int level = evo.extraInfo; + evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.LEVEL, String.valueOf(level), + true, additional)); + } + + private Pokemon pickEvoPowerLvlReplacement(List<Pokemon> pokemonPool, Pokemon current) { + // start with within 10% and add 5% either direction till we find + // something + int currentBST = current.bstForPowerLevels(); + int minTarget = currentBST - currentBST / 10; + int maxTarget = currentBST + currentBST / 10; + List<Pokemon> canPick = new ArrayList<>(); + List<Pokemon> emergencyPick = new ArrayList<>(); + int expandRounds = 0; + while (canPick.isEmpty() || (canPick.size() < 3 && expandRounds < 3)) { + for (Pokemon pk : pokemonPool) { + if (pk.bstForPowerLevels() >= minTarget && pk.bstForPowerLevels() <= maxTarget && !canPick.contains(pk) && !emergencyPick.contains(pk)) { + if (alreadyPicked.contains(pk)) { + emergencyPick.add(pk); + } else { + canPick.add(pk); + } + } + } + if (expandRounds >= 2 && canPick.isEmpty()) { + canPick.addAll(emergencyPick); + } + minTarget -= currentBST / 20; + maxTarget += currentBST / 20; + expandRounds++; + } + return canPick.get(this.random.nextInt(canPick.size())); + } + + // Note that this is slow and somewhat hacky. + private Pokemon findPokemonInPoolWithSpeciesID(List<Pokemon> pokemonPool, int speciesID) { + for (int i = 0; i < pokemonPool.size(); i++) { + if (pokemonPool.get(i).number == speciesID) { + return pokemonPool.get(i); + } + } + return null; + } + + private List<Pokemon> getEvolutionaryRelatives(Pokemon pk) { + List<Pokemon> evolutionaryRelatives = new ArrayList<>(); + for (Evolution ev : pk.evolutionsFrom) { + if (!evolutionaryRelatives.contains(ev.to)) { + Pokemon evo = ev.to; + evolutionaryRelatives.add(evo); + Queue<Evolution> evolutionsList = new LinkedList<>(); + evolutionsList.addAll(evo.evolutionsFrom); + while (evolutionsList.size() > 0) { + evo = evolutionsList.remove().to; + if (!evolutionaryRelatives.contains(evo)) { + evolutionaryRelatives.add(evo); + evolutionsList.addAll(evo.evolutionsFrom); + } + } + } + } + + for (Evolution ev : pk.evolutionsTo) { + if (!evolutionaryRelatives.contains(ev.from)) { + Pokemon preEvo = ev.from; + evolutionaryRelatives.add(preEvo); + + // At this point, preEvo is basically the "parent" of pk. Run + // getEvolutionaryRelatives on preEvo in order to get pk's + // "sibling" evolutions too. For example, if pk is Espeon, then + // preEvo here will be Eevee, and this will add all the other + // eeveelutions to the relatives list. + List<Pokemon> relativesForPreEvo = getEvolutionaryRelatives(preEvo); + for (Pokemon preEvoRelative : relativesForPreEvo) { + if (!evolutionaryRelatives.contains(preEvoRelative)) { + evolutionaryRelatives.add(preEvoRelative); + } + } + + while (preEvo.evolutionsTo.size() > 0) { + preEvo = preEvo.evolutionsTo.get(0).from; + if (!evolutionaryRelatives.contains(preEvo)) { + evolutionaryRelatives.add(preEvo); + + // Similar to above, get the "sibling" evolutions here too. + relativesForPreEvo = getEvolutionaryRelatives(preEvo); + for (Pokemon preEvoRelative : relativesForPreEvo) { + if (!evolutionaryRelatives.contains(preEvoRelative)) { + evolutionaryRelatives.add(preEvoRelative); + } + } + } + } + } + } + + return evolutionaryRelatives; + } + + private static class EvolutionPair { + private Pokemon from; + private Pokemon to; + + EvolutionPair(Pokemon from, Pokemon to) { + this.from = from; + this.to = to; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((from == null) ? 0 : from.hashCode()); + result = prime * result + ((to == null) ? 0 : to.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + EvolutionPair other = (EvolutionPair) obj; + if (from == null) { + if (other.from != null) + return false; + } else if (!from.equals(other.from)) + return false; + if (to == null) { + return other.to == null; + } else return to.equals(other.to); + } + } + + /** + * Check whether adding an evolution from one Pokemon to another will cause + * an evolution cycle. + * + * @param from Pokemon that is evolving + * @param to Pokemon to evolve to + * @return True if there is an evolution cycle, else false + */ + private boolean evoCycleCheck(Pokemon from, Pokemon to) { + Evolution tempEvo = new Evolution(from, to, false, EvolutionType.NONE, 0); + from.evolutionsFrom.add(tempEvo); + Set<Pokemon> visited = new HashSet<>(); + Set<Pokemon> recStack = new HashSet<>(); + boolean recur = isCyclic(from, visited, recStack); + from.evolutionsFrom.remove(tempEvo); + return recur; + } + + private boolean isCyclic(Pokemon pk, Set<Pokemon> visited, Set<Pokemon> recStack) { + if (!visited.contains(pk)) { + visited.add(pk); + recStack.add(pk); + for (Evolution ev : pk.evolutionsFrom) { + if (!visited.contains(ev.to) && isCyclic(ev.to, visited, recStack)) { + return true; + } else if (recStack.contains(ev.to)) { + return true; + } + } + } + recStack.remove(pk); + return false; + } + + private interface BasePokemonAction { + void applyTo(Pokemon pk); + } + + private interface EvolvedPokemonAction { + void applyTo(Pokemon evFrom, Pokemon evTo, boolean toMonIsFinalEvo); + } + + private interface CosmeticFormAction { + void applyTo(Pokemon pk, Pokemon baseForme); + } + + /** + * Universal implementation for things that have "copy X up evolutions" + * support. + * @param bpAction + * Method to run on all base or no-copy Pokemon + * @param epAction + * Method to run on all evolved Pokemon with a linear chain of + * @param copySplitEvos + * If true, treat split evolutions the same way as base Pokemon + */ + private void copyUpEvolutionsHelper(BasePokemonAction bpAction, EvolvedPokemonAction epAction, + EvolvedPokemonAction splitAction, boolean copySplitEvos) { + List<Pokemon> allPokes = this.getPokemonInclFormes(); + for (Pokemon pk : allPokes) { + if (pk != null) { + pk.temporaryFlag = false; + } + } + + // Get evolution data. + Set<Pokemon> basicPokes = RomFunctions.getBasicPokemon(this); + Set<Pokemon> splitEvos = RomFunctions.getSplitEvolutions(this); + Set<Pokemon> middleEvos = RomFunctions.getMiddleEvolutions(this, copySplitEvos); + + for (Pokemon pk : basicPokes) { + bpAction.applyTo(pk); + pk.temporaryFlag = true; + } + + if (!copySplitEvos) { + for (Pokemon pk : splitEvos) { + bpAction.applyTo(pk); + pk.temporaryFlag = true; + } + } + + // go "up" evolutions looking for pre-evos to do first + for (Pokemon pk : allPokes) { + if (pk != null && !pk.temporaryFlag) { + + // Non-randomized pokes at this point must have + // a linear chain of single evolutions down to + // a randomized poke. + Stack<Evolution> currentStack = new Stack<>(); + Evolution ev = pk.evolutionsTo.get(0); + while (!ev.from.temporaryFlag) { + currentStack.push(ev); + ev = ev.from.evolutionsTo.get(0); + } + + // Now "ev" is set to an evolution from a Pokemon that has had + // the base action done on it to one that hasn't. + // Do the evolution action for everything left on the stack. + + if (copySplitEvos && splitAction != null && splitEvos.contains(ev.to)) { + splitAction.applyTo(ev.from, ev.to, !middleEvos.contains(ev.to)); + } else { + epAction.applyTo(ev.from, ev.to, !middleEvos.contains(ev.to)); + } + ev.to.temporaryFlag = true; + while (!currentStack.isEmpty()) { + ev = currentStack.pop(); + if (copySplitEvos && splitAction != null && splitEvos.contains(pk)) { + splitAction.applyTo(ev.from, ev.to, !middleEvos.contains(ev.to)); + } else { + epAction.applyTo(ev.from, ev.to, !middleEvos.contains(ev.to)); + } + ev.to.temporaryFlag = true; + } + + } + } + } + + private void copyUpEvolutionsHelper(BasePokemonAction bpAction, EvolvedPokemonAction epAction) { + copyUpEvolutionsHelper(bpAction, epAction, null, false); + } + + private boolean checkForUnusedMove(List<Move> potentialList, List<Integer> alreadyUsed) { + for (Move mv : potentialList) { + if (!alreadyUsed.contains(mv.number)) { + return true; + } + } + return false; + } + + private List<Pokemon> pokemonOfType(Type type, boolean noLegendaries) { + List<Pokemon> typedPokes = new ArrayList<>(); + for (Pokemon pk : mainPokemonList) { + if (pk != null && (!noLegendaries || !pk.isLegendary()) && !pk.actuallyCosmetic) { + if (pk.primaryType == type || pk.secondaryType == type) { + typedPokes.add(pk); + } + } + } + return typedPokes; + } + + private List<Pokemon> pokemonOfTypeInclFormes(Type type, boolean noLegendaries) { + List<Pokemon> typedPokes = new ArrayList<>(); + for (Pokemon pk : mainPokemonListInclFormes) { + if (pk != null && !pk.actuallyCosmetic && (!noLegendaries || !pk.isLegendary())) { + if (pk.primaryType == type || pk.secondaryType == type) { + typedPokes.add(pk); + } + } + } + return typedPokes; + } + + private List<Pokemon> allPokemonWithoutNull() { + List<Pokemon> allPokes = new ArrayList<>(this.getPokemon()); + allPokes.remove(0); + return allPokes; + } + + private List<Pokemon> allPokemonInclFormesWithoutNull() { + List<Pokemon> allPokes = new ArrayList<>(this.getPokemonInclFormes()); + allPokes.remove(0); + return allPokes; + } + + private Set<Pokemon> pokemonInArea(EncounterSet area) { + Set<Pokemon> inArea = new TreeSet<>(); + for (Encounter enc : area.encounters) { + inArea.add(enc.pokemon); + } + return inArea; + } + + private Map<Type, Integer> typeWeightings; + private int totalTypeWeighting; + + private Type pickType(boolean weightByFrequency, boolean noLegendaries, boolean allowAltFormes) { + if (totalTypeWeighting == 0) { + // Determine weightings + for (Type t : Type.values()) { + if (typeInGame(t)) { + List<Pokemon> pokemonOfType = allowAltFormes ? pokemonOfTypeInclFormes(t, noLegendaries) : + pokemonOfType(t, noLegendaries); + int pkWithTyping = pokemonOfType.size(); + typeWeightings.put(t, pkWithTyping); + totalTypeWeighting += pkWithTyping; + } + } + } + + if (weightByFrequency) { + int typePick = this.random.nextInt(totalTypeWeighting); + int typePos = 0; + for (Type t : typeWeightings.keySet()) { + int weight = typeWeightings.get(t); + if (typePos + weight > typePick) { + return t; + } + typePos += weight; + } + return null; + } else { + return randomType(); + } + } + + private void rivalCarriesStarterUpdate(List<Trainer> currentTrainers, String prefix, int pokemonOffset) { + // Find the highest rival battle # + int highestRivalNum = 0; + for (Trainer t : currentTrainers) { + if (t.tag != null && t.tag.startsWith(prefix)) { + highestRivalNum = Math.max(highestRivalNum, + Integer.parseInt(t.tag.substring(prefix.length(), t.tag.indexOf('-')))); + } + } + + if (highestRivalNum == 0) { + // This rival type not used in this game + return; + } + + // Get the starters + // us 0 1 2 => them 0+n 1+n 2+n + List<Pokemon> starters = this.getStarters(); + + // Yellow needs its own case, unfortunately. + if (isYellow()) { + // The rival's starter is index 1 + Pokemon rivalStarter = starters.get(1); + int timesEvolves = numEvolutions(rivalStarter, 2); + // Yellow does not have abilities + int abilitySlot = 0; + // Apply evolutions as appropriate + if (timesEvolves == 0) { + for (int j = 1; j <= 3; j++) { + changeStarterWithTag(currentTrainers, prefix + j + "-0", rivalStarter, abilitySlot); + } + for (int j = 4; j <= 7; j++) { + for (int i = 0; i < 3; i++) { + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, rivalStarter, abilitySlot); + } + } + } else if (timesEvolves == 1) { + for (int j = 1; j <= 3; j++) { + changeStarterWithTag(currentTrainers, prefix + j + "-0", rivalStarter, abilitySlot); + } + rivalStarter = pickRandomEvolutionOf(rivalStarter, false); + for (int j = 4; j <= 7; j++) { + for (int i = 0; i < 3; i++) { + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, rivalStarter, abilitySlot); + } + } + } else if (timesEvolves == 2) { + for (int j = 1; j <= 2; j++) { + changeStarterWithTag(currentTrainers, prefix + j + "-" + 0, rivalStarter, abilitySlot); + } + rivalStarter = pickRandomEvolutionOf(rivalStarter, true); + changeStarterWithTag(currentTrainers, prefix + "3-0", rivalStarter, abilitySlot); + for (int i = 0; i < 3; i++) { + changeStarterWithTag(currentTrainers, prefix + "4-" + i, rivalStarter, abilitySlot); + } + rivalStarter = pickRandomEvolutionOf(rivalStarter, false); + for (int j = 5; j <= 7; j++) { + for (int i = 0; i < 3; i++) { + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, rivalStarter, abilitySlot); + } + } + } + } else { + // Replace each starter as appropriate + // Use level to determine when to evolve, not number anymore + for (int i = 0; i < 3; i++) { + // Rival's starters are pokemonOffset over from each of ours + int starterToUse = (i + pokemonOffset) % 3; + Pokemon thisStarter = starters.get(starterToUse); + int timesEvolves = numEvolutions(thisStarter, 2); + int abilitySlot = getRandomAbilitySlot(thisStarter); + while (abilitySlot == 3) { + // Since starters never have hidden abilities, the rival's starter shouldn't either + abilitySlot = getRandomAbilitySlot(thisStarter); + } + // If a fully evolved pokemon, use throughout + // Otherwise split by evolutions as appropriate + if (timesEvolves == 0) { + for (int j = 1; j <= highestRivalNum; j++) { + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, abilitySlot); + } + } else if (timesEvolves == 1) { + int j = 1; + for (; j <= highestRivalNum / 2; j++) { + if (getLevelOfStarter(currentTrainers, prefix + j + "-" + i) >= 30) { + break; + } + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, abilitySlot); + } + thisStarter = pickRandomEvolutionOf(thisStarter, false); + int evolvedAbilitySlot = getValidAbilitySlotFromOriginal(thisStarter, abilitySlot); + for (; j <= highestRivalNum; j++) { + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, evolvedAbilitySlot); + } + } else if (timesEvolves == 2) { + int j = 1; + for (; j <= highestRivalNum; j++) { + if (getLevelOfStarter(currentTrainers, prefix + j + "-" + i) >= 16) { + break; + } + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, abilitySlot); + } + thisStarter = pickRandomEvolutionOf(thisStarter, true); + int evolvedAbilitySlot = getValidAbilitySlotFromOriginal(thisStarter, abilitySlot); + for (; j <= highestRivalNum; j++) { + if (getLevelOfStarter(currentTrainers, prefix + j + "-" + i) >= 36) { + break; + } + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, evolvedAbilitySlot); + } + thisStarter = pickRandomEvolutionOf(thisStarter, false); + evolvedAbilitySlot = getValidAbilitySlotFromOriginal(thisStarter, abilitySlot); + for (; j <= highestRivalNum; j++) { + changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, evolvedAbilitySlot); + } + } + } + } + + } + + private Pokemon pickRandomEvolutionOf(Pokemon base, boolean mustEvolveItself) { + // Used for "rival carries starter" + // Pick a random evolution of base Pokemon, subject to + // "must evolve itself" if appropriate. + List<Pokemon> candidates = new ArrayList<>(); + for (Evolution ev : base.evolutionsFrom) { + if (!mustEvolveItself || ev.to.evolutionsFrom.size() > 0) { + candidates.add(ev.to); + } + } + + if (candidates.size() == 0) { + throw new RandomizationException("Random evolution called on a Pokemon without any usable evolutions."); + } + + return candidates.get(random.nextInt(candidates.size())); + } + + private int getLevelOfStarter(List<Trainer> currentTrainers, String tag) { + for (Trainer t : currentTrainers) { + if (t.tag != null && t.tag.equals(tag)) { + // Bingo, get highest level + // last pokemon is given priority +2 but equal priority + // = first pokemon wins, so its effectively +1 + // If it's tagged the same we can assume it's the same team + // just the opposite gender or something like that... + // So no need to check other trainers with same tag. + int highestLevel = t.pokemon.get(0).level; + int trainerPkmnCount = t.pokemon.size(); + for (int i = 1; i < trainerPkmnCount; i++) { + int levelBonus = (i == trainerPkmnCount - 1) ? 2 : 0; + if (t.pokemon.get(i).level + levelBonus > highestLevel) { + highestLevel = t.pokemon.get(i).level; + } + } + return highestLevel; + } + } + return 0; + } + + private void changeStarterWithTag(List<Trainer> currentTrainers, String tag, Pokemon starter, int abilitySlot) { + for (Trainer t : currentTrainers) { + if (t.tag != null && t.tag.equals(tag)) { + + // Bingo + TrainerPokemon bestPoke = t.pokemon.get(0); + + if (t.forceStarterPosition >= 0) { + bestPoke = t.pokemon.get(t.forceStarterPosition); + } else { + // Change the highest level pokemon, not the last. + // BUT: last gets +2 lvl priority (effectively +1) + // same as above, equal priority = earlier wins + int trainerPkmnCount = t.pokemon.size(); + for (int i = 1; i < trainerPkmnCount; i++) { + int levelBonus = (i == trainerPkmnCount - 1) ? 2 : 0; + if (t.pokemon.get(i).level + levelBonus > bestPoke.level) { + bestPoke = t.pokemon.get(i); + } + } + } + bestPoke.pokemon = starter; + setFormeForTrainerPokemon(bestPoke,starter); + bestPoke.resetMoves = true; + bestPoke.abilitySlot = abilitySlot; + } + } + + } + + // Return the max depth of pre-evolutions a Pokemon has + private int numPreEvolutions(Pokemon pk, int maxInterested) { + return numPreEvolutions(pk, 0, maxInterested); + } + + private int numPreEvolutions(Pokemon pk, int depth, int maxInterested) { + if (pk.evolutionsTo.size() == 0) { + return 0; + } else { + if (depth == maxInterested - 1) { + return 1; + } else { + int maxPreEvos = 0; + for (Evolution ev : pk.evolutionsTo) { + maxPreEvos = Math.max(maxPreEvos, numPreEvolutions(ev.from, depth + 1, maxInterested) + 1); + } + return maxPreEvos; + } + } + } + + private int numEvolutions(Pokemon pk, int maxInterested) { + return numEvolutions(pk, 0, maxInterested); + } + + private int numEvolutions(Pokemon pk, int depth, int maxInterested) { + if (pk.evolutionsFrom.size() == 0) { + return 0; + } else { + if (depth == maxInterested - 1) { + return 1; + } else { + int maxEvos = 0; + for (Evolution ev : pk.evolutionsFrom) { + maxEvos = Math.max(maxEvos, numEvolutions(ev.to, depth + 1, maxInterested) + 1); + } + return maxEvos; + } + } + } + + private Pokemon fullyEvolve(Pokemon pokemon, int trainerIndex) { + // If the fullyEvolvedRandomSeed hasn't been set yet, set it here. + if (this.fullyEvolvedRandomSeed == -1) { + this.fullyEvolvedRandomSeed = random.nextInt(GlobalConstants.LARGEST_NUMBER_OF_SPLIT_EVOS); + } + + Set<Pokemon> seenMons = new HashSet<>(); + seenMons.add(pokemon); + + while (true) { + if (pokemon.evolutionsFrom.size() == 0) { + // fully evolved + break; + } + + // check for cyclic evolutions from what we've already seen + boolean cyclic = false; + for (Evolution ev : pokemon.evolutionsFrom) { + if (seenMons.contains(ev.to)) { + // cyclic evolution detected - bail now + cyclic = true; + break; + } + } + + if (cyclic) { + break; + } + + // We want to make split evolutions deterministic, but still random on a seed-to-seed basis. + // Therefore, we take a random value (which is generated once per seed) and add it to the trainer's + // index to get a pseudorandom number that can be used to decide which split to take. + int evolutionIndex = (this.fullyEvolvedRandomSeed + trainerIndex) % pokemon.evolutionsFrom.size(); + pokemon = pokemon.evolutionsFrom.get(evolutionIndex).to; + seenMons.add(pokemon); + } + + return pokemon; + } + + private Set<Pokemon> relatedPokemon(Pokemon original) { + Set<Pokemon> results = new HashSet<>(); + results.add(original); + Queue<Pokemon> toCheck = new LinkedList<>(); + toCheck.add(original); + while (!toCheck.isEmpty()) { + Pokemon check = toCheck.poll(); + for (Evolution ev : check.evolutionsFrom) { + if (!results.contains(ev.to)) { + results.add(ev.to); + toCheck.add(ev.to); + } + } + for (Evolution ev : check.evolutionsTo) { + if (!results.contains(ev.from)) { + results.add(ev.from); + toCheck.add(ev.from); + } + } + } + return results; + } + + private Map<Type, List<Pokemon>> cachedReplacementLists; + private List<Pokemon> cachedAllList; + private List<Pokemon> bannedList = new ArrayList<>(); + private List<Pokemon> usedAsUniqueList = new ArrayList<>(); + + + private Pokemon pickTrainerPokeReplacement(Pokemon current, boolean usePowerLevels, Type type, + boolean noLegendaries, boolean wonderGuardAllowed, + boolean usePlacementHistory, boolean swapMegaEvos, + boolean abilitiesAreRandomized, boolean allowAltFormes, + boolean banIrregularAltFormes) { + List<Pokemon> pickFrom; + List<Pokemon> withoutBannedPokemon; + + if (swapMegaEvos) { + pickFrom = megaEvolutionsList + .stream() + .filter(mega -> mega.method == 1) + .map(mega -> mega.from) + .distinct() + .collect(Collectors.toList()); + } else { + pickFrom = cachedAllList; + } + + if (usePlacementHistory) { + // "Distributed" settings + double placementAverage = getPlacementAverage(); + pickFrom = pickFrom + .stream() + .filter(pk -> getPlacementHistory(pk) < placementAverage * 2) + .collect(Collectors.toList()); + if (pickFrom.isEmpty()) { + pickFrom = cachedAllList; + } + } else if (type != null && cachedReplacementLists != null) { + // "Type Themed" settings + if (!cachedReplacementLists.containsKey(type)) { + List<Pokemon> pokemonOfType = allowAltFormes ? pokemonOfTypeInclFormes(type, noLegendaries) : + pokemonOfType(type, noLegendaries); + pokemonOfType.removeAll(this.getBannedFormesForPlayerPokemon()); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + pokemonOfType.removeAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + pokemonOfType.removeAll(getIrregularFormes()); + } + cachedReplacementLists.put(type, pokemonOfType); + } + if (swapMegaEvos) { + pickFrom = cachedReplacementLists.get(type) + .stream() + .filter(pickFrom::contains) + .collect(Collectors.toList()); + if (pickFrom.isEmpty()) { + pickFrom = cachedReplacementLists.get(type); + } + } else { + pickFrom = cachedReplacementLists.get(type); + } + } + + withoutBannedPokemon = pickFrom.stream().filter(pk -> !bannedList.contains(pk)).collect(Collectors.toList()); + if (!withoutBannedPokemon.isEmpty()) { + pickFrom = withoutBannedPokemon; + } + + if (usePowerLevels) { + // start with within 10% and add 5% either direction till we find + // something + int currentBST = current.bstForPowerLevels(); + int minTarget = currentBST - currentBST / 10; + int maxTarget = currentBST + currentBST / 10; + List<Pokemon> canPick = new ArrayList<>(); + int expandRounds = 0; + while (canPick.isEmpty() || (canPick.size() < 3 && expandRounds < 2)) { + for (Pokemon pk : pickFrom) { + if (pk.bstForPowerLevels() >= minTarget + && pk.bstForPowerLevels() <= maxTarget + && (wonderGuardAllowed || (pk.ability1 != Abilities.wonderGuard + && pk.ability2 != Abilities.wonderGuard && pk.ability3 != Abilities.wonderGuard))) { + canPick.add(pk); + } + } + minTarget -= currentBST / 20; + maxTarget += currentBST / 20; + expandRounds++; + } + // If usePlacementHistory is True, then we need to do some + // extra checking to make sure the randomly chosen pokemon + // is actually below the current average placement + // if not, re-roll + + Pokemon chosenPokemon = canPick.get(this.random.nextInt(canPick.size())); + if (usePlacementHistory) { + double placementAverage = getPlacementAverage(); + List<Pokemon> filteredPickList = canPick + .stream() + .filter(pk -> getPlacementHistory(pk) < placementAverage) + .collect(Collectors.toList()); + if (filteredPickList.isEmpty()) { + filteredPickList = canPick; + } + chosenPokemon = filteredPickList.get(this.random.nextInt(filteredPickList.size())); + } + return chosenPokemon; + } else { + if (wonderGuardAllowed) { + return pickFrom.get(this.random.nextInt(pickFrom.size())); + } else { + Pokemon pk = pickFrom.get(this.random.nextInt(pickFrom.size())); + while (pk.ability1 == Abilities.wonderGuard + || pk.ability2 == Abilities.wonderGuard + || pk.ability3 == Abilities.wonderGuard) { + pk = pickFrom.get(this.random.nextInt(pickFrom.size())); + } + return pk; + } + } + } + + private Pokemon pickWildPowerLvlReplacement(List<Pokemon> pokemonPool, Pokemon current, boolean banSamePokemon, + List<Pokemon> usedUp, int bstBalanceLevel) { + // start with within 10% and add 5% either direction till we find + // something + int balancedBST = bstBalanceLevel * 10 + 250; + int currentBST = Math.min(current.bstForPowerLevels(), balancedBST); + int minTarget = currentBST - currentBST / 10; + int maxTarget = currentBST + currentBST / 10; + List<Pokemon> canPick = new ArrayList<>(); + int expandRounds = 0; + while (canPick.isEmpty() || (canPick.size() < 3 && expandRounds < 3)) { + for (Pokemon pk : pokemonPool) { + if (pk.bstForPowerLevels() >= minTarget && pk.bstForPowerLevels() <= maxTarget + && (!banSamePokemon || pk != current) && (usedUp == null || !usedUp.contains(pk)) + && !canPick.contains(pk)) { + canPick.add(pk); + } + } + minTarget -= currentBST / 20; + maxTarget += currentBST / 20; + expandRounds++; + } + return canPick.get(this.random.nextInt(canPick.size())); + } + + private void setFormeForEncounter(Encounter enc, Pokemon pk) { + boolean checkCosmetics = true; + enc.formeNumber = 0; + if (enc.pokemon.formeNumber > 0) { + enc.formeNumber = enc.pokemon.formeNumber; + enc.pokemon = enc.pokemon.baseForme; + checkCosmetics = false; + } + if (checkCosmetics && enc.pokemon.cosmeticForms > 0) { + enc.formeNumber = enc.pokemon.getCosmeticFormNumber(this.random.nextInt(enc.pokemon.cosmeticForms)); + } else if (!checkCosmetics && pk.cosmeticForms > 0) { + enc.formeNumber += pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms)); + } + } + + private Map<Integer, List<EncounterSet>> mapZonesToEncounters(List<EncounterSet> encountersForAreas) { + Map<Integer, List<EncounterSet>> zonesToEncounters = new TreeMap<>(); + for (EncounterSet encountersInArea : encountersForAreas) { + if (zonesToEncounters.containsKey(encountersInArea.offset)) { + zonesToEncounters.get(encountersInArea.offset).add(encountersInArea); + } else { + List<EncounterSet> encountersForZone = new ArrayList<>(); + encountersForZone.add(encountersInArea); + zonesToEncounters.put(encountersInArea.offset, encountersForZone); + } + } + return zonesToEncounters; + } + + public Pokemon pickEntirelyRandomPokemon(boolean includeFormes, boolean noLegendaries, EncounterSet area, List<Pokemon> banned) { + Pokemon result; + Pokemon randomNonLegendaryPokemon = includeFormes ? randomNonLegendaryPokemonInclFormes() : randomNonLegendaryPokemon(); + Pokemon randomPokemon = includeFormes ? randomPokemonInclFormes() : randomPokemon(); + result = noLegendaries ? randomNonLegendaryPokemon : randomPokemon; + while (result.actuallyCosmetic) { + randomNonLegendaryPokemon = includeFormes ? randomNonLegendaryPokemonInclFormes() : randomNonLegendaryPokemon(); + randomPokemon = includeFormes ? randomPokemonInclFormes() : randomPokemon(); + result = noLegendaries ? randomNonLegendaryPokemon : randomPokemon; + } + while (banned.contains(result) || area.bannedPokemon.contains(result)) { + randomNonLegendaryPokemon = includeFormes ? randomNonLegendaryPokemonInclFormes() : randomNonLegendaryPokemon(); + randomPokemon = includeFormes ? randomPokemonInclFormes() : randomPokemon(); + result = noLegendaries ? randomNonLegendaryPokemon : randomPokemon; + while (result.actuallyCosmetic) { + randomNonLegendaryPokemon = includeFormes ? randomNonLegendaryPokemonInclFormes() : randomNonLegendaryPokemon(); + randomPokemon = includeFormes ? randomPokemonInclFormes() : randomPokemon(); + result = noLegendaries ? randomNonLegendaryPokemon : randomPokemon; + } + } + return result; + } + + private Pokemon pickStaticPowerLvlReplacement(List<Pokemon> pokemonPool, Pokemon current, boolean banSamePokemon, + boolean limitBST) { + // start with within 10% and add 5% either direction till we find + // something + int currentBST = current.bstForPowerLevels(); + int minTarget = limitBST ? currentBST - currentBST / 5 : currentBST - currentBST / 10; + int maxTarget = limitBST ? currentBST : currentBST + currentBST / 10; + List<Pokemon> canPick = new ArrayList<>(); + int expandRounds = 0; + while (canPick.isEmpty() || (canPick.size() < 3 && expandRounds < 3)) { + for (Pokemon pk : pokemonPool) { + if (pk.bstForPowerLevels() >= minTarget && pk.bstForPowerLevels() <= maxTarget + && (!banSamePokemon || pk != current) && !canPick.contains(pk)) { + canPick.add(pk); + } + } + minTarget -= currentBST / 20; + maxTarget += currentBST / 20; + expandRounds++; + } + return canPick.get(this.random.nextInt(canPick.size())); + } + + @Override + public List<Pokemon> getAbilityDependentFormes() { + List<Pokemon> abilityDependentFormes = new ArrayList<>(); + for (int i = 0; i < mainPokemonListInclFormes.size(); i++) { + Pokemon pokemon = mainPokemonListInclFormes.get(i); + if (pokemon.baseForme != null) { + if (pokemon.baseForme.number == Species.castform) { + // All alternate Castform formes + abilityDependentFormes.add(pokemon); + } else if (pokemon.baseForme.number == Species.darmanitan && pokemon.formeNumber == 1) { + // Damanitan-Z + abilityDependentFormes.add(pokemon); + } else if (pokemon.baseForme.number == Species.aegislash) { + // Aegislash-B + abilityDependentFormes.add(pokemon); + } else if (pokemon.baseForme.number == Species.wishiwashi) { + // Wishiwashi-S + abilityDependentFormes.add(pokemon); + } + } + } + return abilityDependentFormes; + } + + @Override + public List<Pokemon> getBannedFormesForPlayerPokemon() { + List<Pokemon> bannedFormes = new ArrayList<>(); + for (int i = 0; i < mainPokemonListInclFormes.size(); i++) { + Pokemon pokemon = mainPokemonListInclFormes.get(i); + if (pokemon.baseForme != null) { + if (pokemon.baseForme.number == Species.giratina) { + // Giratina-O is banned because it reverts back to Altered Forme if + // equipped with any item that isn't the Griseous Orb. + bannedFormes.add(pokemon); + } else if (pokemon.baseForme.number == Species.shaymin) { + // Shaymin-S is banned because it reverts back to its original forme + // under a variety of circumstances, and can only be changed back + // with the Gracidea. + bannedFormes.add(pokemon); + } + } + } + return bannedFormes; + } + + @Override + public void randomizeTotemPokemon(Settings settings) { + boolean randomizeTotem = + settings.getTotemPokemonMod() == Settings.TotemPokemonMod.RANDOM || + settings.getTotemPokemonMod() == Settings.TotemPokemonMod.SIMILAR_STRENGTH; + boolean randomizeAllies = + settings.getAllyPokemonMod() == Settings.AllyPokemonMod.RANDOM || + settings.getAllyPokemonMod() == Settings.AllyPokemonMod.SIMILAR_STRENGTH; + boolean randomizeAuras = + settings.getAuraMod() == Settings.AuraMod.RANDOM || + settings.getAuraMod() == Settings.AuraMod.SAME_STRENGTH; + boolean similarStrengthTotem = settings.getTotemPokemonMod() == Settings.TotemPokemonMod.SIMILAR_STRENGTH; + boolean similarStrengthAllies = settings.getAllyPokemonMod() == Settings.AllyPokemonMod.SIMILAR_STRENGTH; + boolean similarStrengthAuras = settings.getAuraMod() == Settings.AuraMod.SAME_STRENGTH; + boolean randomizeHeldItems = settings.isRandomizeTotemHeldItems(); + int levelModifier = settings.isTotemLevelsModified() ? settings.getTotemLevelModifier() : 0; + boolean allowAltFormes = settings.isAllowTotemAltFormes(); + boolean banIrregularAltFormes = settings.isBanIrregularAltFormes(); + boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE; + + checkPokemonRestrictions(); + List<TotemPokemon> currentTotemPokemon = this.getTotemPokemon(); + List<TotemPokemon> replacements = new ArrayList<>(); + List<Pokemon> banned = this.bannedForStaticPokemon(); + if (!abilitiesAreRandomized) { + List<Pokemon> abilityDependentFormes = getAbilityDependentFormes(); + banned.addAll(abilityDependentFormes); + } + if (banIrregularAltFormes) { + banned.addAll(getIrregularFormes()); + } + List<Pokemon> listInclFormesExclCosmetics = + mainPokemonListInclFormes + .stream() + .filter(pk -> !pk.actuallyCosmetic) + .collect(Collectors.toList()); + List<Pokemon> pokemonLeft = new ArrayList<>(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics); + pokemonLeft.removeAll(banned); + for (TotemPokemon old : currentTotemPokemon) { + TotemPokemon newTotem = new TotemPokemon(); + newTotem.heldItem = old.heldItem; + if (randomizeTotem) { + Pokemon newPK; + Pokemon oldPK = old.pkmn; + if (old.forme > 0) { + oldPK = getAltFormeOfPokemon(oldPK, old.forme); + } + + if (similarStrengthTotem) { + newPK = pickStaticPowerLvlReplacement( + pokemonLeft, + oldPK, + true, + false); + } else { + newPK = pokemonLeft.remove(this.random.nextInt(pokemonLeft.size())); + } + + pokemonLeft.remove(newPK); + newTotem.pkmn = newPK; + setFormeForStaticEncounter(newTotem, newPK); + newTotem.resetMoves = true; + newTotem.level = old.level; + + if (levelModifier != 0) { + newTotem.level = Math.min(100, (int) Math.round(newTotem.level * (1 + levelModifier / 100.0))); + } + if (pokemonLeft.size() == 0) { + pokemonLeft.addAll(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics); + pokemonLeft.removeAll(banned); + } + } else { + newTotem.pkmn = old.pkmn; + newTotem.level = old.level; + if (levelModifier != 0) { + newTotem.level = Math.min(100, (int) Math.round(newTotem.level * (1 + levelModifier / 100.0))); + } + setFormeForStaticEncounter(newTotem, newTotem.pkmn); + } + + if (randomizeAllies) { + for (Integer oldAllyIndex: old.allies.keySet()) { + StaticEncounter oldAlly = old.allies.get(oldAllyIndex); + StaticEncounter newAlly = new StaticEncounter(); + Pokemon newAllyPK; + Pokemon oldAllyPK = oldAlly.pkmn; + if (oldAlly.forme > 0) { + oldAllyPK = getAltFormeOfPokemon(oldAllyPK, oldAlly.forme); + } + if (similarStrengthAllies) { + newAllyPK = pickStaticPowerLvlReplacement( + pokemonLeft, + oldAllyPK, + true, + false); + } else { + newAllyPK = pokemonLeft.remove(this.random.nextInt(pokemonLeft.size())); + } + + pokemonLeft.remove(newAllyPK); + newAlly.pkmn = newAllyPK; + setFormeForStaticEncounter(newAlly, newAllyPK); + newAlly.resetMoves = true; + newAlly.level = oldAlly.level; + if (levelModifier != 0) { + newAlly.level = Math.min(100, (int) Math.round(newAlly.level * (1 + levelModifier / 100.0))); + } + + newTotem.allies.put(oldAllyIndex,newAlly); + if (pokemonLeft.size() == 0) { + pokemonLeft.addAll(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics); + pokemonLeft.removeAll(banned); + } + } + } else { + newTotem.allies = old.allies; + for (StaticEncounter ally: newTotem.allies.values()) { + if (levelModifier != 0) { + ally.level = Math.min(100, (int) Math.round(ally.level * (1 + levelModifier / 100.0))); + setFormeForStaticEncounter(ally, ally.pkmn); + } + } + } + + if (randomizeAuras) { + if (similarStrengthAuras) { + newTotem.aura = Aura.randomAuraSimilarStrength(this.random, old.aura); + } else { + newTotem.aura = Aura.randomAura(this.random); + } + } else { + newTotem.aura = old.aura; + } + + if (randomizeHeldItems) { + if (old.heldItem != 0) { + List<Integer> consumableList = getAllConsumableHeldItems(); + newTotem.heldItem = consumableList.get(this.random.nextInt(consumableList.size())); + } + } + + replacements.add(newTotem); + } + + // Save + this.setTotemPokemon(replacements); + } + + /* Helper methods used by subclasses and/or this class */ + + void checkPokemonRestrictions() { + if (!restrictionsSet) { + setPokemonPool(null); + } + } + + protected void applyCamelCaseNames() { + List<Pokemon> pokes = getPokemon(); + for (Pokemon pkmn : pokes) { + if (pkmn == null) { + continue; + } + pkmn.name = RomFunctions.camelCase(pkmn.name); + } + + } + + private void setPlacementHistory(Pokemon newPK) { + Integer history = getPlacementHistory(newPK); + placementHistory.put(newPK, history + 1); + } + + private int getPlacementHistory(Pokemon newPK) { + return placementHistory.getOrDefault(newPK, 0); + } + + private double getPlacementAverage() { + return placementHistory.values().stream().mapToInt(e -> e).average().orElse(0); + } + + + private List<Pokemon> getBelowAveragePlacements() { + // This method will return a PK if the number of times a pokemon has been + // placed is less than average of all placed pokemon's appearances + // E.g., Charmander's been placed once, but the average for all pokemon is 2.2 + // So add to list and return + + List<Pokemon> toPlacePK = new ArrayList<>(); + List<Pokemon> placedPK = new ArrayList<>(placementHistory.keySet()); + List<Pokemon> allPK = cachedAllList; + int placedPKNum = 0; + for (Pokemon p : placedPK) { + placedPKNum += placementHistory.get(p); + } + float placedAverage = Math.round((float)placedPKNum / (float)placedPK.size()); + + + + if (placedAverage != placedAverage) { // this is checking for NaN, should only happen on first call + placedAverage = 1; + } + + // now we've got placement average, iterate all pokemon and see if they qualify to be placed + + for (Pokemon newPK : allPK) { + if (placedPK.contains(newPK)) { // if it's in the list of previously placed, then check its viability + if (placementHistory.get(newPK) <= placedAverage) { + toPlacePK.add(newPK); + } + } + else { + toPlacePK.add(newPK); // if not placed at all, automatically flag true for placing + + } + } + + return toPlacePK; + + } + + @Override + public void renderPlacementHistory() { + List<Pokemon> placedPK = new ArrayList<>(placementHistory.keySet()); + for (Pokemon p : placedPK) { + System.out.println(p.name+": "+ placementHistory.get(p)); + } + } + + ///// Item functions + private void setItemPlacementHistory(int newItem) { + Integer history = getItemPlacementHistory(newItem); + // System.out.println("Current history: " + newPK.name + " : " + history); + itemPlacementHistory.put(newItem, history + 1); + } + + private int getItemPlacementHistory(int newItem) { + List<Integer> placedItem = new ArrayList<>(itemPlacementHistory.keySet()); + if (placedItem.contains(newItem)) { + return itemPlacementHistory.get(newItem); + } + else { + return 0; + } + } + + private float getItemPlacementAverage() { + // This method will return an integer of average for itemPlacementHistory + // placed is less than average of all placed pokemon's appearances + // E.g., Charmander's been placed once, but the average for all pokemon is 2.2 + // So add to list and return + + List<Integer> placedPK = new ArrayList<>(itemPlacementHistory.keySet()); + int placedPKNum = 0; + for (Integer p : placedPK) { + placedPKNum += itemPlacementHistory.get(p); + } + return (float)placedPKNum / (float)placedPK.size(); + } + + private void reportItemHistory() { + String[] itemNames = this.getItemNames(); + List<Integer> placedItem = new ArrayList<>(itemPlacementHistory.keySet()); + for (Integer p : placedItem) { + System.out.println(itemNames[p]+": "+ itemPlacementHistory.get(p)); + } + } + + protected void log(String log) { + if (logStream != null) { + logStream.println(log); + } + } + + protected void logBlankLine() { + if (logStream != null) { + logStream.println(); + } + } + + /* Default Implementations */ + /* Used when a subclass doesn't override */ + /* + * The implication here is that these WILL be overridden by at least one + * subclass. + */ + @Override + public boolean typeInGame(Type type) { + return !type.isHackOnly && !(type == Type.FAIRY && generationOfPokemon() < 6); + } + + @Override + public String abilityName(int number) { + return ""; + } + + @Override + public List<Integer> getUselessAbilities() { + return new ArrayList<>(); + } + + @Override + public int getAbilityForTrainerPokemon(TrainerPokemon tp) { + return 0; + } + + @Override + public boolean hasTimeBasedEncounters() { + // DEFAULT: no + return false; + } + + @Override + public List<Pokemon> bannedForWildEncounters() { + return new ArrayList<>(); + } + + @Override + public List<Integer> getMovesBannedFromLevelup() { + return new ArrayList<>(); + } + + @Override + public List<Pokemon> bannedForStaticPokemon() { + return new ArrayList<>(); + } + + @Override + public boolean forceSwapStaticMegaEvos() { + return false; + } + + @Override + public int maxTrainerNameLength() { + // default: no real limit + return Integer.MAX_VALUE; + } + + @Override + public int maxSumOfTrainerNameLengths() { + // default: no real limit + return Integer.MAX_VALUE; + } + + @Override + public int maxTrainerClassNameLength() { + // default: no real limit + return Integer.MAX_VALUE; + } + + @Override + public int maxTradeNicknameLength() { + return 10; + } + + @Override + public int maxTradeOTNameLength() { + return 7; + } + + @Override + public boolean altFormesCanHaveDifferentEvolutions() { + return false; + } + + @Override + public List<Integer> getGameBreakingMoves() { + // Sonicboom & Dragon Rage + return Arrays.asList(49, 82); + } + + @Override + public List<Integer> getIllegalMoves() { + return new ArrayList<>(); + } + + @Override + public boolean isYellow() { + return false; + } + + @Override + public void writeCheckValueToROM(int value) { + // do nothing + } + + @Override + public int miscTweaksAvailable() { + // default: none + return 0; + } + + @Override + public void applyMiscTweaks(Settings settings) { + int selectedMiscTweaks = settings.getCurrentMiscTweaks(); + + int codeTweaksAvailable = miscTweaksAvailable(); + List<MiscTweak> tweaksToApply = new ArrayList<>(); + + for (MiscTweak mt : MiscTweak.allTweaks) { + if ((codeTweaksAvailable & mt.getValue()) > 0 && (selectedMiscTweaks & mt.getValue()) > 0) { + tweaksToApply.add(mt); + } + } + + // Sort so priority is respected in tweak ordering. + Collections.sort(tweaksToApply); + + // Now apply in order. + for (MiscTweak mt : tweaksToApply) { + applyMiscTweak(mt); + } + } + + @Override + public void applyMiscTweak(MiscTweak tweak) { + // default: do nothing + } + + @Override + public List<Integer> getXItems() { + return GlobalConstants.xItems; + } + + @Override + public List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, int[] pokeMoves) { + return Arrays.asList(0); + } + + @Override + public List<Integer> getAllConsumableHeldItems() { + return Arrays.asList(0); + } + + @Override + public List<Integer> getAllHeldItems() { + return Arrays.asList(0); + } + + @Override + public List<Pokemon> getBannedFormesForTrainerPokemon() { + return new ArrayList<>(); + } + + @Override + public List<PickupItem> getPickupItems() { + return new ArrayList<>(); + } + + @Override + public void setPickupItems(List<PickupItem> pickupItems) { + // do nothing + } +} diff --git a/src/com/pkrandom/romhandlers/Gen1RomHandler.java b/src/com/pkrandom/romhandlers/Gen1RomHandler.java new file mode 100755 index 0000000..69cd51e --- /dev/null +++ b/src/com/pkrandom/romhandlers/Gen1RomHandler.java @@ -0,0 +1,2918 @@ +package com.pkrandom.romhandlers;
+
+/*----------------------------------------------------------------------------*/
+/*-- Gen1RomHandler.java - randomizer handler for R/B/Y. --*/
+/*-- --*/
+/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/
+/*-- Pokemon and any associated names and the like are --*/
+/*-- trademark and (C) Nintendo 1996-2020. --*/
+/*-- --*/
+/*-- The custom code written here is licensed under the terms of the GPL: --*/
+/*-- --*/
+/*-- This program is free software: you can redistribute it and/or modify --*/
+/*-- it under the terms of the GNU General Public License as published by --*/
+/*-- the Free Software Foundation, either version 3 of the License, or --*/
+/*-- (at your option) any later version. --*/
+/*-- --*/
+/*-- This program is distributed in the hope that it will be useful, --*/
+/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/
+/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/
+/*-- GNU General Public License for more details. --*/
+/*-- --*/
+/*-- You should have received a copy of the GNU General Public License --*/
+/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/
+/*----------------------------------------------------------------------------*/
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Scanner;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.pkrandom.*;
+import com.pkrandom.constants.*;
+import com.pkrandom.exceptions.RandomizationException;
+import com.pkrandom.exceptions.RandomizerIOException;
+import com.pkrandom.pokemon.*;
+import compressors.Gen1Decmp;
+
+public class Gen1RomHandler extends AbstractGBCRomHandler {
+
+ public static class Factory extends RomHandler.Factory {
+
+ @Override
+ public Gen1RomHandler create(Random random, PrintStream logStream) {
+ return new Gen1RomHandler(random, logStream);
+ }
+
+ public boolean isLoadable(String filename) {
+ long fileLength = new File(filename).length();
+ if (fileLength > 8 * 1024 * 1024) {
+ return false;
+ }
+ byte[] loaded = loadFilePartial(filename, 0x1000);
+ // nope
+ return loaded.length != 0 && detectRomInner(loaded, (int) fileLength);
+ }
+ }
+
+ public Gen1RomHandler(Random random) {
+ super(random, null);
+ }
+
+ public Gen1RomHandler(Random random, PrintStream logStream) {
+ super(random, logStream);
+ }
+
+ // Important RBY Data Structures
+
+ private int[] pokeNumToRBYTable;
+ private int[] pokeRBYToNumTable;
+ private int[] moveNumToRomTable;
+ private int[] moveRomToNumTable;
+ private int pokedexCount;
+
+ private Type idToType(int value) {
+ if (Gen1Constants.typeTable[value] != null) {
+ return Gen1Constants.typeTable[value];
+ }
+ if (romEntry.extraTypeLookup.containsKey(value)) {
+ return romEntry.extraTypeLookup.get(value);
+ }
+ return null;
+ }
+
+ private byte typeToByte(Type type) {
+ if (type == null) {
+ return 0x00; // revert to normal
+ }
+ if (romEntry.extraTypeReverse.containsKey(type)) {
+ return romEntry.extraTypeReverse.get(type).byteValue();
+ }
+ return Gen1Constants.typeToByte(type);
+ }
+
+ private static class RomEntry {
+ private String name;
+ private String romName;
+ private int version, nonJapanese;
+ private String extraTableFile;
+ private boolean isYellow;
+ private long expectedCRC32 = -1;
+ private int crcInHeader = -1;
+ private Map<String, String> tweakFiles = new HashMap<>();
+ private List<TMTextEntry> tmTexts = new ArrayList<>();
+ private Map<String, Integer> entries = new HashMap<>();
+ private Map<String, int[]> arrayEntries = new HashMap<>();
+ private List<StaticPokemon> staticPokemon = new ArrayList<>();
+ private int[] ghostMarowakOffsets = new int[0];
+ private Map<Integer, Type> extraTypeLookup = new HashMap<>();
+ private Map<Type, Integer> extraTypeReverse = new HashMap<>();
+
+ private int getValue(String key) {
+ if (!entries.containsKey(key)) {
+ entries.put(key, 0);
+ }
+ return entries.get(key);
+ }
+ }
+
+ private static List<RomEntry> roms;
+
+ static {
+ loadROMInfo();
+ }
+
+ private static class TMTextEntry {
+ private int number;
+ private int offset;
+ private String template;
+ }
+
+ private static void loadROMInfo() {
+ roms = new ArrayList<>();
+ RomEntry current = null;
+ try {
+ Scanner sc = new Scanner(FileFunctions.openConfig("gen1_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();
+ r[0] = r[0].trim();
+ // Static Pokemon?
+ if (r[0].equals("StaticPokemon{}")) {
+ current.staticPokemon.add(parseStaticPokemon(r[1]));
+ } else if (r[0].equals("StaticPokemonGhostMarowak{}")) {
+ StaticPokemon ghostMarowak = parseStaticPokemon(r[1]);
+ current.staticPokemon.add(ghostMarowak);
+ current.ghostMarowakOffsets = ghostMarowak.speciesOffsets;
+ } else if (r[0].equals("TMText[]")) {
+ if (r[1].startsWith("[") && r[1].endsWith("]")) {
+ String[] parts = r[1].substring(1, r[1].length() - 1).split(",", 3);
+ TMTextEntry tte = new TMTextEntry();
+ tte.number = parseRIInt(parts[0]);
+ tte.offset = parseRIInt(parts[1]);
+ tte.template = parts[2];
+ current.tmTexts.add(tte);
+ }
+ } else if (r[0].equals("Game")) {
+ current.romName = r[1];
+ } else if (r[0].equals("Version")) {
+ current.version = parseRIInt(r[1]);
+ } else if (r[0].equals("NonJapanese")) {
+ current.nonJapanese = parseRIInt(r[1]);
+ } else if (r[0].equals("Type")) {
+ current.isYellow = r[1].equalsIgnoreCase("Yellow");
+ } else if (r[0].equals("ExtraTableFile")) {
+ current.extraTableFile = r[1];
+ } else if (r[0].equals("CRCInHeader")) {
+ current.crcInHeader = parseRIInt(r[1]);
+ } else if (r[0].equals("CRC32")) {
+ current.expectedCRC32 = parseRILong("0x" + r[1]);
+ } else if (r[0].endsWith("Tweak")) {
+ current.tweakFiles.put(r[0], r[1]);
+ } else if (r[0].equals("ExtraTypes")) {
+ // remove the containers
+ r[1] = r[1].substring(1, r[1].length() - 1);
+ String[] parts = r[1].split(",");
+ for (String part : parts) {
+ String[] iParts = part.split("=");
+ int typeId = Integer.parseInt(iParts[0], 16);
+ String typeName = iParts[1].trim();
+ Type theType = Type.valueOf(typeName);
+ current.extraTypeLookup.put(typeId, theType);
+ current.extraTypeReverse.put(theType, typeId);
+ }
+ } else if (r[0].equals("CopyFrom")) {
+ for (RomEntry otherEntry : roms) {
+ if (r[1].equalsIgnoreCase(otherEntry.name)) {
+ // copy from here
+ boolean cSP = (current.getValue("CopyStaticPokemon") == 1);
+ boolean cTT = (current.getValue("CopyTMText") == 1);
+ current.arrayEntries.putAll(otherEntry.arrayEntries);
+ current.entries.putAll(otherEntry.entries);
+ if (cSP) {
+ current.staticPokemon.addAll(otherEntry.staticPokemon);
+ current.ghostMarowakOffsets = otherEntry.ghostMarowakOffsets;
+ current.entries.put("StaticPokemonSupport", 1);
+ } else {
+ current.entries.put("StaticPokemonSupport", 0);
+ }
+ if (cTT) {
+ current.tmTexts.addAll(otherEntry.tmTexts);
+ }
+ current.extraTableFile = otherEntry.extraTableFile;
+ }
+ }
+ } else {
+ if (r[1].startsWith("[") && r[1].endsWith("]")) {
+ String[] offsets = r[1].substring(1, r[1].length() - 1).split(",");
+ if (offsets.length == 1 && offsets[0].trim().isEmpty()) {
+ current.arrayEntries.put(r[0], new int[0]);
+ } else {
+ int[] offs = new int[offsets.length];
+ int c = 0;
+ for (String off : offsets) {
+ offs[c++] = parseRIInt(off);
+ }
+ current.arrayEntries.put(r[0], offs);
+ }
+
+ } else {
+ int offs = parseRIInt(r[1]);
+ current.entries.put(r[0], offs);
+ }
+ }
+ }
+ }
+ }
+ sc.close();
+ } catch (FileNotFoundException e) {
+ System.err.println("File not found!");
+ }
+
+ }
+
+ private static StaticPokemon parseStaticPokemon(String staticPokemonString) {
+ StaticPokemon sp = new StaticPokemon();
+ String pattern = "[A-z]+=\\[(0x[0-9a-fA-F]+,?\\s?)+]";
+ Pattern r = Pattern.compile(pattern);
+ Matcher m = r.matcher(staticPokemonString);
+ while (m.find()) {
+ String[] segments = m.group().split("=");
+ String[] romOffsets = segments[1].substring(1, segments[1].length() - 1).split(",");
+ int[] offsets = new int [romOffsets.length];
+ for (int i = 0; i < offsets.length; i++) {
+ offsets[i] = parseRIInt(romOffsets[i]);
+ }
+ switch (segments[0]) {
+ case "Species":
+ sp.speciesOffsets = offsets;
+ break;
+ case "Level":
+ sp.levelOffsets = offsets;
+ break;
+ }
+ }
+ return sp;
+ }
+
+ private 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's data
+ private Pokemon[] pokes;
+ private List<Pokemon> pokemonList;
+ private RomEntry romEntry;
+ private Move[] moves;
+ private String[] itemNames;
+ private String[] mapNames;
+ private SubMap[] maps;
+ private boolean xAccNerfed;
+ private long actualCRC32;
+ private boolean effectivenessUpdated;
+
+ @Override
+ public boolean detectRom(byte[] rom) {
+ return detectRomInner(rom, rom.length);
+ }
+
+ public static boolean detectRomInner(byte[] rom, int romSize) {
+ // size check
+ return romSize >= GBConstants.minRomSize && romSize <= GBConstants.maxRomSize && checkRomEntry(rom) != null;
+ }
+
+ @Override
+ public void loadedRom() {
+ romEntry = checkRomEntry(this.rom);
+ pokeNumToRBYTable = new int[256];
+ pokeRBYToNumTable = new int[256];
+ moveNumToRomTable = new int[256];
+ moveRomToNumTable = new int[256];
+ maps = new SubMap[256];
+ xAccNerfed = false;
+ clearTextTables();
+ readTextTable("gameboy_jpn");
+ if (romEntry.extraTableFile != null && !romEntry.extraTableFile.equalsIgnoreCase("none")) {
+ readTextTable(romEntry.extraTableFile);
+ }
+ loadPokedexOrder();
+ loadPokemonStats();
+ pokemonList = Arrays.asList(pokes);
+ loadMoves();
+ loadItemNames();
+ preloadMaps();
+ loadMapNames();
+ actualCRC32 = FileFunctions.getCRC32(rom);
+ }
+
+ private void loadPokedexOrder() {
+ int pkmnCount = romEntry.getValue("InternalPokemonCount");
+ int orderOffset = romEntry.getValue("PokedexOrder");
+ pokedexCount = 0;
+ for (int i = 1; i <= pkmnCount; i++) {
+ int pokedexNum = rom[orderOffset + i - 1] & 0xFF;
+ pokeRBYToNumTable[i] = pokedexNum;
+ if (pokedexNum != 0 && pokeNumToRBYTable[pokedexNum] == 0) {
+ pokeNumToRBYTable[pokedexNum] = i;
+ }
+ pokedexCount = Math.max(pokedexCount, pokedexNum);
+ }
+ }
+
+ private static RomEntry checkRomEntry(byte[] rom) {
+ int version = rom[GBConstants.versionOffset] & 0xFF;
+ int nonjap = rom[GBConstants.jpFlagOffset] & 0xFF;
+ // Check for specific CRC first
+ int crcInHeader = ((rom[GBConstants.crcOffset] & 0xFF) << 8) | (rom[GBConstants.crcOffset + 1] & 0xFF);
+ for (RomEntry re : roms) {
+ if (romSig(rom, re.romName) && re.version == version && re.nonJapanese == nonjap
+ && re.crcInHeader == crcInHeader) {
+ return re;
+ }
+ }
+ // Now check for non-specific-CRC entries
+ for (RomEntry re : roms) {
+ if (romSig(rom, re.romName) && re.version == version && re.nonJapanese == nonjap && re.crcInHeader == -1) {
+ return re;
+ }
+ }
+ // Not found
+ return null;
+ }
+
+ @Override
+ public void savingRom() {
+ savePokemonStats();
+ saveMoves();
+ }
+
+ private String[] readMoveNames() {
+ int moveCount = romEntry.getValue("MoveCount");
+ int offset = romEntry.getValue("MoveNamesOffset");
+ String[] moveNames = new String[moveCount + 1];
+ for (int i = 1; i <= moveCount; i++) {
+ moveNames[i] = readVariableLengthString(offset, false);
+ offset += lengthOfStringAt(offset, false) + 1;
+ }
+ return moveNames;
+ }
+
+ private void loadMoves() {
+ String[] moveNames = readMoveNames();
+ int moveCount = romEntry.getValue("MoveCount");
+ int movesOffset = romEntry.getValue("MoveDataOffset");
+ // check real move count
+ int trueMoveCount = 0;
+ for (int i = 1; i <= moveCount; i++) {
+ // temp hack for Brown
+ if (rom[movesOffset + (i - 1) * 6] != 0 && !moveNames[i].equals("Nothing")) {
+ trueMoveCount++;
+ }
+ }
+ moves = new Move[trueMoveCount + 1];
+ int trueMoveIndex = 0;
+
+ for (int i = 1; i <= moveCount; i++) {
+ int anim = rom[movesOffset + (i - 1) * 6] & 0xFF;
+ // another temp hack for brown
+ if (anim > 0 && !moveNames[i].equals("Nothing")) {
+ trueMoveIndex++;
+ moveNumToRomTable[trueMoveIndex] = i;
+ moveRomToNumTable[i] = trueMoveIndex;
+ moves[trueMoveIndex] = new Move();
+ moves[trueMoveIndex].name = moveNames[i];
+ moves[trueMoveIndex].internalId = i;
+ moves[trueMoveIndex].number = trueMoveIndex;
+ moves[trueMoveIndex].effectIndex = rom[movesOffset + (i - 1) * 6 + 1] & 0xFF;
+ moves[trueMoveIndex].hitratio = ((rom[movesOffset + (i - 1) * 6 + 4] & 0xFF)) / 255.0 * 100;
+ moves[trueMoveIndex].power = rom[movesOffset + (i - 1) * 6 + 2] & 0xFF;
+ moves[trueMoveIndex].pp = rom[movesOffset + (i - 1) * 6 + 5] & 0xFF;
+ moves[trueMoveIndex].type = idToType(rom[movesOffset + (i - 1) * 6 + 3] & 0xFF);
+ moves[trueMoveIndex].category = GBConstants.physicalTypes.contains(moves[trueMoveIndex].type) ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL;
+ if (moves[trueMoveIndex].power == 0 && !GlobalConstants.noPowerNonStatusMoves.contains(trueMoveIndex)) {
+ moves[trueMoveIndex].category = MoveCategory.STATUS;
+ }
+
+ if (moves[trueMoveIndex].name.equals("Swift")) {
+ perfectAccuracy = (int)moves[trueMoveIndex].hitratio;
+ }
+
+ if (GlobalConstants.normalMultihitMoves.contains(i)) {
+ moves[trueMoveIndex].hitCount = 3;
+ } else if (GlobalConstants.doubleHitMoves.contains(i)) {
+ moves[trueMoveIndex].hitCount = 2;
+ }
+
+ loadStatChangesFromEffect(moves[trueMoveIndex]);
+ loadStatusFromEffect(moves[trueMoveIndex]);
+ loadMiscMoveInfoFromEffect(moves[trueMoveIndex]);
+ }
+ }
+ }
+
+ private void loadStatChangesFromEffect(Move move) {
+ switch (move.effectIndex) {
+ case Gen1Constants.noDamageAtkPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen1Constants.noDamageDefPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen1Constants.noDamageSpecialPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen1Constants.noDamageEvasionPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.EVASION;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen1Constants.noDamageAtkMinusOneEffect:
+ case Gen1Constants.damageAtkMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen1Constants.noDamageDefMinusOneEffect:
+ case Gen1Constants.damageDefMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen1Constants.noDamageSpeMinusOneEffect:
+ case Gen1Constants.damageSpeMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPEED;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen1Constants.noDamageAccuracyMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.ACCURACY;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen1Constants.noDamageAtkPlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen1Constants.noDamageDefPlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen1Constants.noDamageSpePlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPEED;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen1Constants.noDamageSpecialPlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen1Constants.noDamageDefMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen1Constants.damageSpecialMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL;
+ move.statChanges[0].stages = -1;
+ break;
+ default:
+ // Move does not have a stat-changing effect
+ return;
+ }
+
+ switch (move.effectIndex) {
+ case Gen1Constants.noDamageAtkPlusOneEffect:
+ case Gen1Constants.noDamageDefPlusOneEffect:
+ case Gen1Constants.noDamageSpecialPlusOneEffect:
+ case Gen1Constants.noDamageEvasionPlusOneEffect:
+ case Gen1Constants.noDamageAtkMinusOneEffect:
+ case Gen1Constants.noDamageDefMinusOneEffect:
+ case Gen1Constants.noDamageSpeMinusOneEffect:
+ case Gen1Constants.noDamageAccuracyMinusOneEffect:
+ case Gen1Constants.noDamageAtkPlusTwoEffect:
+ case Gen1Constants.noDamageDefPlusTwoEffect:
+ case Gen1Constants.noDamageSpePlusTwoEffect:
+ case Gen1Constants.noDamageSpecialPlusTwoEffect:
+ case Gen1Constants.noDamageDefMinusTwoEffect:
+ if (move.statChanges[0].stages < 0) {
+ move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET;
+ } else {
+ move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER;
+ }
+ break;
+
+ case Gen1Constants.damageAtkMinusOneEffect:
+ case Gen1Constants.damageDefMinusOneEffect:
+ case Gen1Constants.damageSpeMinusOneEffect:
+ case Gen1Constants.damageSpecialMinusOneEffect:
+ move.statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET;
+ break;
+ }
+
+ if (move.statChangeMoveType == StatChangeMoveType.DAMAGE_TARGET) {
+ for (int i = 0; i < move.statChanges.length; i++) {
+ if (move.statChanges[i].type != StatChangeType.NONE) {
+ move.statChanges[i].percentChance = 85 / 256.0;
+ }
+ }
+ }
+ }
+
+ private void loadStatusFromEffect(Move move) {
+ switch (move.effectIndex) {
+ case Gen1Constants.noDamageSleepEffect:
+ case Gen1Constants.noDamageConfusionEffect:
+ case Gen1Constants.noDamagePoisonEffect:
+ case Gen1Constants.noDamageParalyzeEffect:
+ move.statusMoveType = StatusMoveType.NO_DAMAGE;
+ break;
+
+ case Gen1Constants.damagePoison20PercentEffect:
+ case Gen1Constants.damageBurn10PercentEffect:
+ case Gen1Constants.damageFreeze10PercentEffect:
+ case Gen1Constants.damageParalyze10PercentEffect:
+ case Gen1Constants.damagePoison40PercentEffect:
+ case Gen1Constants.damageBurn30PercentEffect:
+ case Gen1Constants.damageFreeze30PercentEffect:
+ case Gen1Constants.damageParalyze30PercentEffect:
+ case Gen1Constants.damageConfusionEffect:
+ case Gen1Constants.twineedleEffect:
+ move.statusMoveType = StatusMoveType.DAMAGE;
+ break;
+
+ default:
+ // Move does not have a status effect
+ return;
+ }
+
+ switch (move.effectIndex) {
+ case Gen1Constants.noDamageSleepEffect:
+ move.statusType = StatusType.SLEEP;
+ break;
+ case Gen1Constants.damagePoison20PercentEffect:
+ case Gen1Constants.damagePoison40PercentEffect:
+ case Gen1Constants.noDamagePoisonEffect:
+ case Gen1Constants.twineedleEffect:
+ move.statusType = StatusType.POISON;
+ if (move.number == Moves.toxic) {
+ move.statusType = StatusType.TOXIC_POISON;
+ }
+ break;
+ case Gen1Constants.damageBurn10PercentEffect:
+ case Gen1Constants.damageBurn30PercentEffect:
+ move.statusType = StatusType.BURN;
+ break;
+ case Gen1Constants.damageFreeze10PercentEffect:
+ case Gen1Constants.damageFreeze30PercentEffect:
+ move.statusType = StatusType.FREEZE;
+ break;
+ case Gen1Constants.damageParalyze10PercentEffect:
+ case Gen1Constants.damageParalyze30PercentEffect:
+ case Gen1Constants.noDamageParalyzeEffect:
+ move.statusType = StatusType.PARALYZE;
+ break;
+ case Gen1Constants.noDamageConfusionEffect:
+ case Gen1Constants.damageConfusionEffect:
+ move.statusType = StatusType.CONFUSION;
+ break;
+ }
+
+ if (move.statusMoveType == StatusMoveType.DAMAGE) {
+ switch (move.effectIndex) {
+ case Gen1Constants.damageBurn10PercentEffect:
+ case Gen1Constants.damageFreeze10PercentEffect:
+ case Gen1Constants.damageParalyze10PercentEffect:
+ case Gen1Constants.damageConfusionEffect:
+ move.statusPercentChance = 10.0;
+ break;
+ case Gen1Constants.damagePoison20PercentEffect:
+ case Gen1Constants.twineedleEffect:
+ move.statusPercentChance = 20.0;
+ break;
+ case Gen1Constants.damageBurn30PercentEffect:
+ case Gen1Constants.damageFreeze30PercentEffect:
+ case Gen1Constants.damageParalyze30PercentEffect:
+ move.statusPercentChance = 30.0;
+ break;
+ case Gen1Constants.damagePoison40PercentEffect:
+ move.statusPercentChance = 40.0;
+ break;
+ }
+ }
+ }
+
+ private void loadMiscMoveInfoFromEffect(Move move) {
+ switch (move.effectIndex) {
+ case Gen1Constants.flinch10PercentEffect:
+ move.flinchPercentChance = 10.0;
+ break;
+
+ case Gen1Constants.flinch30PercentEffect:
+ move.flinchPercentChance = 30.0;
+ break;
+
+ case Gen1Constants.damageAbsorbEffect:
+ case Gen1Constants.dreamEaterEffect:
+ move.absorbPercent = 50;
+ break;
+
+ case Gen1Constants.damageRecoilEffect:
+ move.recoilPercent = 25;
+ break;
+
+ case Gen1Constants.chargeEffect:
+ case Gen1Constants.flyEffect:
+ move.isChargeMove = true;
+ break;
+
+ case Gen1Constants.hyperBeamEffect:
+ move.isRechargeMove = true;
+ break;
+ }
+
+ if (Gen1Constants.increasedCritMoves.contains(move.number)) {
+ move.criticalChance = CriticalChance.INCREASED;
+ }
+ }
+
+ private void saveMoves() {
+ int movesOffset = romEntry.getValue("MoveDataOffset");
+ for (Move m : moves) {
+ if (m != null) {
+ int i = m.internalId;
+ rom[movesOffset + (i - 1) * 6 + 1] = (byte) m.effectIndex;
+ rom[movesOffset + (i - 1) * 6 + 2] = (byte) m.power;
+ rom[movesOffset + (i - 1) * 6 + 3] = typeToByte(m.type);
+ int hitratio = (int) Math.round(m.hitratio * 2.55);
+ if (hitratio < 0) {
+ hitratio = 0;
+ }
+ if (hitratio > 255) {
+ hitratio = 255;
+ }
+ rom[movesOffset + (i - 1) * 6 + 4] = (byte) hitratio;
+ rom[movesOffset + (i - 1) * 6 + 5] = (byte) m.pp;
+ }
+ }
+ }
+
+ public List<Move> getMoves() {
+ return Arrays.asList(moves);
+ }
+
+ private void loadPokemonStats() {
+ pokes = new Gen1Pokemon[pokedexCount + 1];
+ // Fetch our names
+ String[] pokeNames = readPokemonNames();
+ // Get base stats
+ int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset");
+ for (int i = 1; i <= pokedexCount; i++) {
+ pokes[i] = new Gen1Pokemon();
+ pokes[i].number = i;
+ if (i != Species.mew || romEntry.isYellow) {
+ loadBasicPokeStats(pokes[i], pokeStatsOffset + (i - 1) * Gen1Constants.baseStatsEntrySize);
+ }
+ // Name?
+ pokes[i].name = pokeNames[pokeNumToRBYTable[i]];
+ }
+
+ // Mew override for R/B
+ if (!romEntry.isYellow) {
+ loadBasicPokeStats(pokes[Species.mew], romEntry.getValue("MewStatsOffset"));
+ }
+
+ // Evolutions
+ populateEvolutions();
+
+ }
+
+ private void savePokemonStats() {
+ // Write pokemon names
+ int offs = romEntry.getValue("PokemonNamesOffset");
+ int nameLength = romEntry.getValue("PokemonNamesLength");
+ for (int i = 1; i <= pokedexCount; i++) {
+ int rbynum = pokeNumToRBYTable[i];
+ int stringOffset = offs + (rbynum - 1) * nameLength;
+ writeFixedLengthString(pokes[i].name, stringOffset, nameLength);
+ }
+ // Write pokemon stats
+ int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset");
+ for (int i = 1; i <= pokedexCount; i++) {
+ if (i == Species.mew) {
+ continue;
+ }
+ saveBasicPokeStats(pokes[i], pokeStatsOffset + (i - 1) * Gen1Constants.baseStatsEntrySize);
+ }
+ // Write MEW
+ int mewOffset = romEntry.isYellow ? pokeStatsOffset + (Species.mew - 1)
+ * Gen1Constants.baseStatsEntrySize : romEntry.getValue("MewStatsOffset");
+ saveBasicPokeStats(pokes[Species.mew], mewOffset);
+
+ // Write evolutions
+ writeEvosAndMovesLearnt(true, null);
+ }
+
+ private void loadBasicPokeStats(Pokemon pkmn, int offset) {
+ pkmn.hp = rom[offset + Gen1Constants.bsHPOffset] & 0xFF;
+ pkmn.attack = rom[offset + Gen1Constants.bsAttackOffset] & 0xFF;
+ pkmn.defense = rom[offset + Gen1Constants.bsDefenseOffset] & 0xFF;
+ pkmn.speed = rom[offset + Gen1Constants.bsSpeedOffset] & 0xFF;
+ pkmn.special = rom[offset + Gen1Constants.bsSpecialOffset] & 0xFF;
+ // Type
+ pkmn.primaryType = idToType(rom[offset + Gen1Constants.bsPrimaryTypeOffset] & 0xFF);
+ pkmn.secondaryType = idToType(rom[offset + Gen1Constants.bsSecondaryTypeOffset] & 0xFF);
+ // Only one type?
+ if (pkmn.secondaryType == pkmn.primaryType) {
+ pkmn.secondaryType = null;
+ }
+
+ pkmn.catchRate = rom[offset + Gen1Constants.bsCatchRateOffset] & 0xFF;
+ pkmn.expYield = rom[offset + Gen1Constants.bsExpYieldOffset] & 0xFF;
+ pkmn.growthCurve = ExpCurve.fromByte(rom[offset + Gen1Constants.bsGrowthCurveOffset]);
+ pkmn.frontSpritePointer = readWord(offset + Gen1Constants.bsFrontSpriteOffset);
+
+ pkmn.guaranteedHeldItem = -1;
+ pkmn.commonHeldItem = -1;
+ pkmn.rareHeldItem = -1;
+ pkmn.darkGrassHeldItem = -1;
+ }
+
+ private void saveBasicPokeStats(Pokemon pkmn, int offset) {
+ rom[offset + Gen1Constants.bsHPOffset] = (byte) pkmn.hp;
+ rom[offset + Gen1Constants.bsAttackOffset] = (byte) pkmn.attack;
+ rom[offset + Gen1Constants.bsDefenseOffset] = (byte) pkmn.defense;
+ rom[offset + Gen1Constants.bsSpeedOffset] = (byte) pkmn.speed;
+ rom[offset + Gen1Constants.bsSpecialOffset] = (byte) pkmn.special;
+ rom[offset + Gen1Constants.bsPrimaryTypeOffset] = typeToByte(pkmn.primaryType);
+ if (pkmn.secondaryType == null) {
+ rom[offset + Gen1Constants.bsSecondaryTypeOffset] = rom[offset + Gen1Constants.bsPrimaryTypeOffset];
+ } else {
+ rom[offset + Gen1Constants.bsSecondaryTypeOffset] = typeToByte(pkmn.secondaryType);
+ }
+ rom[offset + Gen1Constants.bsCatchRateOffset] = (byte) pkmn.catchRate;
+ rom[offset + Gen1Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte();
+ rom[offset + Gen1Constants.bsExpYieldOffset] = (byte) pkmn.expYield;
+ }
+
+ private String[] readPokemonNames() {
+ int offs = romEntry.getValue("PokemonNamesOffset");
+ int nameLength = romEntry.getValue("PokemonNamesLength");
+ int pkmnCount = romEntry.getValue("InternalPokemonCount");
+ String[] names = new String[pkmnCount + 1];
+ for (int i = 1; i <= pkmnCount; i++) {
+ names[i] = readFixedLengthString(offs + (i - 1) * nameLength, nameLength);
+ }
+ return names;
+ }
+
+ @Override
+ public List<Pokemon> getStarters() {
+ // Get the starters
+ List<Pokemon> starters = new ArrayList<>();
+ starters.add(pokes[pokeRBYToNumTable[rom[romEntry.arrayEntries.get("StarterOffsets1")[0]] & 0xFF]]);
+ starters.add(pokes[pokeRBYToNumTable[rom[romEntry.arrayEntries.get("StarterOffsets2")[0]] & 0xFF]]);
+ if (!romEntry.isYellow) {
+ starters.add(pokes[pokeRBYToNumTable[rom[romEntry.arrayEntries.get("StarterOffsets3")[0]] & 0xFF]]);
+ }
+ return starters;
+ }
+
+ @Override
+ public boolean setStarters(List<Pokemon> newStarters) {
+ // Amount?
+ int starterAmount = 2;
+ if (!romEntry.isYellow) {
+ starterAmount = 3;
+ }
+
+ // Basic checks
+ if (newStarters.size() != starterAmount) {
+ return false;
+ }
+
+ // Patch starter bytes
+ for (int i = 0; i < starterAmount; i++) {
+ byte starter = (byte) pokeNumToRBYTable[newStarters.get(i).number];
+ int[] offsets = romEntry.arrayEntries.get("StarterOffsets" + (i + 1));
+ for (int offset : offsets) {
+ rom[offset] = starter;
+ }
+ }
+
+ // Special stuff for non-Yellow only
+
+ if (!romEntry.isYellow) {
+
+ // Starter text
+ if (romEntry.getValue("CanChangeStarterText") > 0) {
+ int[] starterTextOffsets = romEntry.arrayEntries.get("StarterTextOffsets");
+ for (int i = 0; i < 3 && i < starterTextOffsets.length; i++) {
+ writeVariableLengthString(String.format("So! You want\\n%s?\\e", newStarters.get(i).name),
+ starterTextOffsets[i], true);
+ }
+ }
+
+ // Patch starter pokedex routine?
+ // Can only do in 1M roms because of size concerns
+ if (romEntry.getValue("PatchPokedex") > 0) {
+
+ // Starter pokedex required RAM values
+ // RAM offset => value
+ // Allows for multiple starters in the same RAM byte
+ Map<Integer, Integer> onValues = new TreeMap<>();
+ for (int i = 0; i < 3; i++) {
+ int pkDexNum = newStarters.get(i).number;
+ int ramOffset = (pkDexNum - 1) / 8 + romEntry.getValue("PokedexRamOffset");
+ int bitShift = (pkDexNum - 1) % 8;
+ int writeValue = 1 << bitShift;
+ if (onValues.containsKey(ramOffset)) {
+ onValues.put(ramOffset, onValues.get(ramOffset) | writeValue);
+ } else {
+ onValues.put(ramOffset, writeValue);
+ }
+ }
+
+ // Starter pokedex offset/pointer calculations
+
+ int pkDexOnOffset = romEntry.getValue("StarterPokedexOnOffset");
+ int pkDexOffOffset = romEntry.getValue("StarterPokedexOffOffset");
+
+ int sizeForOnRoutine = 5 * onValues.size() + 3;
+ int writeOnRoutineTo = romEntry.getValue("StarterPokedexBranchOffset");
+ int writeOffRoutineTo = writeOnRoutineTo + sizeForOnRoutine;
+ int offsetForOnRoutine = makeGBPointer(writeOnRoutineTo);
+ int offsetForOffRoutine = makeGBPointer(writeOffRoutineTo);
+ int retOnOffset = makeGBPointer(pkDexOnOffset + 5);
+ int retOffOffset = makeGBPointer(pkDexOffOffset + 4);
+
+ // Starter pokedex
+ // Branch to our new routine(s)
+
+ // Turn bytes on
+ rom[pkDexOnOffset] = GBConstants.gbZ80Jump;
+ writeWord(pkDexOnOffset + 1, offsetForOnRoutine);
+ rom[pkDexOnOffset + 3] = GBConstants.gbZ80Nop;
+ rom[pkDexOnOffset + 4] = GBConstants.gbZ80Nop;
+
+ // Turn bytes off
+ rom[pkDexOffOffset] = GBConstants.gbZ80Jump;
+ writeWord(pkDexOffOffset + 1, offsetForOffRoutine);
+ rom[pkDexOffOffset + 3] = GBConstants.gbZ80Nop;
+
+ // Put together the two scripts
+ rom[writeOffRoutineTo] = GBConstants.gbZ80XorA;
+ int turnOnOffset = writeOnRoutineTo;
+ int turnOffOffset = writeOffRoutineTo + 1;
+ for (int ramOffset : onValues.keySet()) {
+ int onValue = onValues.get(ramOffset);
+ // Turn on code
+ rom[turnOnOffset++] = GBConstants.gbZ80LdA;
+ rom[turnOnOffset++] = (byte) onValue;
+ // Turn on code for ram writing
+ rom[turnOnOffset++] = GBConstants.gbZ80LdAToFar;
+ rom[turnOnOffset++] = (byte) (ramOffset % 0x100);
+ rom[turnOnOffset++] = (byte) (ramOffset / 0x100);
+ // Turn off code for ram writing
+ rom[turnOffOffset++] = GBConstants.gbZ80LdAToFar;
+ rom[turnOffOffset++] = (byte) (ramOffset % 0x100);
+ rom[turnOffOffset++] = (byte) (ramOffset / 0x100);
+ }
+ // Jump back
+ rom[turnOnOffset++] = GBConstants.gbZ80Jump;
+ writeWord(turnOnOffset, retOnOffset);
+
+ rom[turnOffOffset++] = GBConstants.gbZ80Jump;
+ writeWord(turnOffOffset, retOffOffset);
+ }
+
+ }
+
+ // If we're changing the player's starter for Yellow, then the player can't get the
+ // Bulbasaur gift unless they randomly stumble into a Pikachu somewhere else. This is
+ // because you need a certain amount of Pikachu happiness to acquire this gift, and
+ // happiness only accumulates if you have a Pikachu. Instead, just patch out this check.
+ if (romEntry.entries.containsKey("PikachuHappinessCheckOffset") && newStarters.get(0).number != Species.pikachu) {
+ int offset = romEntry.getValue("PikachuHappinessCheckOffset");
+
+ // The code looks like this:
+ // ld a, [wPikachuHappiness]
+ // cp 147
+ // jr c, .asm_1cfb3 <- this is where "offset" is
+ // Write two nops to patch out the jump
+ rom[offset] = GBConstants.gbZ80Nop;
+ rom[offset + 1] = GBConstants.gbZ80Nop;
+ }
+
+ return true;
+
+ }
+
+ @Override
+ public boolean hasStarterAltFormes() {
+ return false;
+ }
+
+ @Override
+ public int starterCount() {
+ return isYellow() ? 2 : 3;
+ }
+
+ @Override
+ public Map<Integer, StatChange> getUpdatedPokemonStats(int generation) {
+ Map<Integer,StatChange> map = GlobalConstants.getStatChanges(generation);
+ switch(generation) {
+ case 6:
+ map.put(12,new StatChange(Stat.SPECIAL.val,90));
+ map.put(36,new StatChange(Stat.SPECIAL.val,95));
+ map.put(45,new StatChange(Stat.SPECIAL.val,110));
+ break;
+ default:
+ break;
+ }
+ return map;
+ }
+
+ @Override
+ public boolean supportsStarterHeldItems() {
+ // No held items in Gen 1
+ return false;
+ }
+
+ @Override
+ public List<Integer> getStarterHeldItems() {
+ // do nothing
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setStarterHeldItems(List<Integer> items) {
+ // do nothing
+ }
+
+ @Override
+ public List<Integer> getEvolutionItems() {
+ return null;
+ }
+
+ @Override
+ public List<EncounterSet> getEncounters(boolean useTimeOfDay) {
+ List<EncounterSet> encounters = new ArrayList<>();
+
+ Pokemon ghostMarowak = pokes[Species.marowak];
+ if (canChangeStaticPokemon()) {
+ ghostMarowak = pokes[pokeRBYToNumTable[rom[romEntry.ghostMarowakOffsets[0]] & 0xFF]];
+ }
+
+ // grass & water
+ List<Integer> usedOffsets = new ArrayList<>();
+ int tableOffset = romEntry.getValue("WildPokemonTableOffset");
+ int tableBank = bankOf(tableOffset);
+ int mapID = -1;
+
+ while (readWord(tableOffset) != Gen1Constants.encounterTableEnd) {
+ mapID++;
+ int offset = calculateOffset(tableBank, readWord(tableOffset));
+ int rootOffset = offset;
+ if (!usedOffsets.contains(offset)) {
+ usedOffsets.add(offset);
+ // grass and water are exactly the same
+ for (int a = 0; a < 2; a++) {
+ int rate = rom[offset++] & 0xFF;
+ if (rate > 0) {
+ // there is data here
+ EncounterSet thisSet = new EncounterSet();
+ thisSet.rate = rate;
+ thisSet.offset = rootOffset;
+ thisSet.displayName = (a == 1 ? "Surfing" : "Grass/Cave") + " on " + mapNames[mapID];
+ if (mapID >= Gen1Constants.towerMapsStartIndex && mapID <= Gen1Constants.towerMapsEndIndex) {
+ thisSet.bannedPokemon.add(ghostMarowak);
+ }
+ for (int slot = 0; slot < Gen1Constants.encounterTableSize; slot++) {
+ Encounter enc = new Encounter();
+ enc.level = rom[offset] & 0xFF;
+ enc.pokemon = pokes[pokeRBYToNumTable[rom[offset + 1] & 0xFF]];
+ thisSet.encounters.add(enc);
+ offset += 2;
+ }
+ encounters.add(thisSet);
+ }
+ }
+ } else {
+ for (EncounterSet es : encounters) {
+ if (es.offset == offset) {
+ es.displayName += ", " + mapNames[mapID];
+ }
+ }
+ }
+ tableOffset += 2;
+ }
+
+ // old rod
+ int oldRodOffset = romEntry.getValue("OldRodOffset");
+ EncounterSet oldRodSet = new EncounterSet();
+ oldRodSet.displayName = "Old Rod Fishing";
+ Encounter oldRodEnc = new Encounter();
+ oldRodEnc.level = rom[oldRodOffset + 2] & 0xFF;
+ oldRodEnc.pokemon = pokes[pokeRBYToNumTable[rom[oldRodOffset + 1] & 0xFF]];
+ oldRodSet.encounters.add(oldRodEnc);
+ oldRodSet.bannedPokemon.add(ghostMarowak);
+ encounters.add(oldRodSet);
+
+ // good rod
+ int goodRodOffset = romEntry.getValue("GoodRodOffset");
+ EncounterSet goodRodSet = new EncounterSet();
+ goodRodSet.displayName = "Good Rod Fishing";
+ for (int grSlot = 0; grSlot < 2; grSlot++) {
+ Encounter enc = new Encounter();
+ enc.level = rom[goodRodOffset + grSlot * 2] & 0xFF;
+ enc.pokemon = pokes[pokeRBYToNumTable[rom[goodRodOffset + grSlot * 2 + 1] & 0xFF]];
+ goodRodSet.encounters.add(enc);
+ }
+ goodRodSet.bannedPokemon.add(ghostMarowak);
+ encounters.add(goodRodSet);
+
+ // super rod
+ if (romEntry.isYellow) {
+ int superRodOffset = romEntry.getValue("SuperRodTableOffset");
+ while ((rom[superRodOffset] & 0xFF) != 0xFF) {
+ int map = rom[superRodOffset++] & 0xFF;
+ EncounterSet thisSet = new EncounterSet();
+ thisSet.displayName = "Super Rod Fishing on " + mapNames[map];
+ for (int encN = 0; encN < Gen1Constants.yellowSuperRodTableSize; encN++) {
+ Encounter enc = new Encounter();
+ enc.level = rom[superRodOffset + 1] & 0xFF;
+ enc.pokemon = pokes[pokeRBYToNumTable[rom[superRodOffset] & 0xFF]];
+ thisSet.encounters.add(enc);
+ superRodOffset += 2;
+ }
+ thisSet.bannedPokemon.add(ghostMarowak);
+ encounters.add(thisSet);
+ }
+ } else {
+ // red/blue
+ int superRodOffset = romEntry.getValue("SuperRodTableOffset");
+ int superRodBank = bankOf(superRodOffset);
+ List<Integer> usedSROffsets = new ArrayList<>();
+ while ((rom[superRodOffset] & 0xFF) != 0xFF) {
+ int map = rom[superRodOffset++] & 0xFF;
+ int setOffset = calculateOffset(superRodBank, readWord(superRodOffset));
+ superRodOffset += 2;
+ if (!usedSROffsets.contains(setOffset)) {
+ usedSROffsets.add(setOffset);
+ EncounterSet thisSet = new EncounterSet();
+ thisSet.displayName = "Super Rod Fishing on " + mapNames[map];
+ thisSet.offset = setOffset;
+ int pokesInSet = rom[setOffset++] & 0xFF;
+ for (int encN = 0; encN < pokesInSet; encN++) {
+ Encounter enc = new Encounter();
+ enc.level = rom[setOffset] & 0xFF;
+ enc.pokemon = pokes[pokeRBYToNumTable[rom[setOffset + 1] & 0xFF]];
+ thisSet.encounters.add(enc);
+ setOffset += 2;
+ }
+ thisSet.bannedPokemon.add(ghostMarowak);
+ encounters.add(thisSet);
+ } else {
+ for (EncounterSet es : encounters) {
+ if (es.offset == setOffset) {
+ es.displayName += ", " + mapNames[map];
+ }
+ }
+ }
+ }
+ }
+
+ return encounters;
+ }
+
+ @Override
+ public void setEncounters(boolean useTimeOfDay, List<EncounterSet> encounters) {
+ Iterator<EncounterSet> encsetit = encounters.iterator();
+
+ // grass & water
+ List<Integer> usedOffsets = new ArrayList<>();
+ int tableOffset = romEntry.getValue("WildPokemonTableOffset");
+ int tableBank = bankOf(tableOffset);
+
+ while (readWord(tableOffset) != Gen1Constants.encounterTableEnd) {
+ int offset = calculateOffset(tableBank, readWord(tableOffset));
+ if (!usedOffsets.contains(offset)) {
+ usedOffsets.add(offset);
+ // grass and water are exactly the same
+ for (int a = 0; a < 2; a++) {
+ int rate = rom[offset++] & 0xFF;
+ if (rate > 0) {
+ // there is data here
+ EncounterSet thisSet = encsetit.next();
+ for (int slot = 0; slot < Gen1Constants.encounterTableSize; slot++) {
+ Encounter enc = thisSet.encounters.get(slot);
+ rom[offset] = (byte) enc.level;
+ rom[offset + 1] = (byte) pokeNumToRBYTable[enc.pokemon.number];
+ offset += 2;
+ }
+ }
+ }
+ }
+ tableOffset += 2;
+ }
+
+ // old rod
+ int oldRodOffset = romEntry.getValue("OldRodOffset");
+ EncounterSet oldRodSet = encsetit.next();
+ Encounter oldRodEnc = oldRodSet.encounters.get(0);
+ rom[oldRodOffset + 2] = (byte) oldRodEnc.level;
+ rom[oldRodOffset + 1] = (byte) pokeNumToRBYTable[oldRodEnc.pokemon.number];
+
+ // good rod
+ int goodRodOffset = romEntry.getValue("GoodRodOffset");
+ EncounterSet goodRodSet = encsetit.next();
+ for (int grSlot = 0; grSlot < 2; grSlot++) {
+ Encounter enc = goodRodSet.encounters.get(grSlot);
+ rom[goodRodOffset + grSlot * 2] = (byte) enc.level;
+ rom[goodRodOffset + grSlot * 2 + 1] = (byte) pokeNumToRBYTable[enc.pokemon.number];
+ }
+
+ // super rod
+ if (romEntry.isYellow) {
+ int superRodOffset = romEntry.getValue("SuperRodTableOffset");
+ while ((rom[superRodOffset] & 0xFF) != 0xFF) {
+ superRodOffset++;
+ EncounterSet thisSet = encsetit.next();
+ for (int encN = 0; encN < Gen1Constants.yellowSuperRodTableSize; encN++) {
+ Encounter enc = thisSet.encounters.get(encN);
+ rom[superRodOffset + 1] = (byte) enc.level;
+ rom[superRodOffset] = (byte) pokeNumToRBYTable[enc.pokemon.number];
+ superRodOffset += 2;
+ }
+ }
+ } else {
+ // red/blue
+ int superRodOffset = romEntry.getValue("SuperRodTableOffset");
+ int superRodBank = bankOf(superRodOffset);
+ List<Integer> usedSROffsets = new ArrayList<>();
+ while ((rom[superRodOffset] & 0xFF) != 0xFF) {
+ superRodOffset++;
+ int setOffset = calculateOffset(superRodBank, readWord(superRodOffset));
+ superRodOffset += 2;
+ if (!usedSROffsets.contains(setOffset)) {
+ usedSROffsets.add(setOffset);
+ int pokesInSet = rom[setOffset++] & 0xFF;
+ EncounterSet thisSet = encsetit.next();
+ for (int encN = 0; encN < pokesInSet; encN++) {
+ Encounter enc = thisSet.encounters.get(encN);
+ rom[setOffset] = (byte) enc.level;
+ rom[setOffset + 1] = (byte) pokeNumToRBYTable[enc.pokemon.number];
+ setOffset += 2;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean hasWildAltFormes() {
+ return false;
+ }
+
+ @Override
+ public List<Pokemon> getPokemon() {
+ return pokemonList;
+ }
+
+ @Override
+ public List<Pokemon> getPokemonInclFormes() {
+ return pokemonList;
+ }
+
+ @Override
+ public List<Pokemon> getAltFormes() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<MegaEvolution> getMegaEvolutions() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public Pokemon getAltFormeOfPokemon(Pokemon pk, int forme) {
+ return pk;
+ }
+
+ @Override
+ public List<Pokemon> getIrregularFormes() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public boolean hasFunctionalFormes() {
+ return false;
+ }
+
+ public List<Trainer> getTrainers() {
+ int traineroffset = romEntry.getValue("TrainerDataTableOffset");
+ int traineramount = Gen1Constants.trainerClassCount;
+ int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts");
+
+ int[] pointers = new int[traineramount + 1];
+ for (int i = 1; i <= traineramount; i++) {
+ int tPointer = readWord(traineroffset + (i - 1) * 2);
+ pointers[i] = calculateOffset(bankOf(traineroffset), tPointer);
+ }
+
+ List<String> tcnames = getTrainerClassesForText();
+
+ List<Trainer> allTrainers = new ArrayList<>();
+ int index = 0;
+ for (int i = 1; i <= traineramount; i++) {
+ int offs = pointers[i];
+ int limit = trainerclasslimits[i];
+ String tcname = tcnames.get(i - 1);
+ for (int trnum = 0; trnum < limit; trnum++) {
+ index++;
+ Trainer tr = new Trainer();
+ tr.offset = offs;
+ tr.index = index;
+ tr.trainerclass = i;
+ tr.fullDisplayName = tcname;
+ int dataType = rom[offs] & 0xFF;
+ if (dataType == 0xFF) {
+ // "Special" trainer
+ tr.poketype = 1;
+ offs++;
+ while (rom[offs] != 0x0) {
+ TrainerPokemon tpk = new TrainerPokemon();
+ tpk.level = rom[offs] & 0xFF;
+ tpk.pokemon = pokes[pokeRBYToNumTable[rom[offs + 1] & 0xFF]];
+ tr.pokemon.add(tpk);
+ offs += 2;
+ }
+ } else {
+ tr.poketype = 0;
+ offs++;
+ while (rom[offs] != 0x0) {
+ TrainerPokemon tpk = new TrainerPokemon();
+ tpk.level = dataType;
+ tpk.pokemon = pokes[pokeRBYToNumTable[rom[offs] & 0xFF]];
+ tr.pokemon.add(tpk);
+ offs++;
+ }
+ }
+ offs++;
+ allTrainers.add(tr);
+ }
+ }
+ Gen1Constants.tagTrainersUniversal(allTrainers);
+ if (romEntry.isYellow) {
+ Gen1Constants.tagTrainersYellow(allTrainers);
+ } else {
+ Gen1Constants.tagTrainersRB(allTrainers);
+ }
+ return allTrainers;
+ }
+
+ @Override
+ public List<Integer> getMainPlaythroughTrainers() {
+ return new ArrayList<>(); // Not implemented
+ }
+
+ @Override
+ public List<Integer> getEliteFourTrainers(boolean isChallengeMode) {
+ return new ArrayList<>();
+ }
+
+ public void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode) {
+ int traineroffset = romEntry.getValue("TrainerDataTableOffset");
+ int traineramount = Gen1Constants.trainerClassCount;
+ int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts");
+
+ int[] pointers = new int[traineramount + 1];
+ for (int i = 1; i <= traineramount; i++) {
+ int tPointer = readWord(traineroffset + (i - 1) * 2);
+ pointers[i] = calculateOffset(bankOf(traineroffset), tPointer);
+ }
+
+ Iterator<Trainer> allTrainers = trainerData.iterator();
+ for (int i = 1; i <= traineramount; i++) {
+ int offs = pointers[i];
+ int limit = trainerclasslimits[i];
+ for (int trnum = 0; trnum < limit; trnum++) {
+ Trainer tr = allTrainers.next();
+ if (tr.trainerclass != i) {
+ System.err.println("Trainer mismatch: " + tr.name);
+ }
+ Iterator<TrainerPokemon> tPokes = tr.pokemon.iterator();
+ // Write their pokemon based on poketype
+ if (tr.poketype == 0) {
+ // Regular trainer
+ int fixedLevel = tr.pokemon.get(0).level;
+ rom[offs] = (byte) fixedLevel;
+ offs++;
+ while (tPokes.hasNext()) {
+ TrainerPokemon tpk = tPokes.next();
+ rom[offs] = (byte) pokeNumToRBYTable[tpk.pokemon.number];
+ offs++;
+ }
+ } else {
+ // Special trainer
+ rom[offs] = (byte) 0xFF;
+ offs++;
+ while (tPokes.hasNext()) {
+ TrainerPokemon tpk = tPokes.next();
+ rom[offs] = (byte) tpk.level;
+ rom[offs + 1] = (byte) pokeNumToRBYTable[tpk.pokemon.number];
+ offs += 2;
+ }
+ }
+ rom[offs] = 0;
+ offs++;
+ }
+ }
+
+ // Custom Moves AI Table
+ // Zero it out entirely.
+ rom[romEntry.getValue("ExtraTrainerMovesTableOffset")] = (byte) 0xFF;
+
+ // Champion Rival overrides in Red/Blue
+ if (!isYellow()) {
+ // hacky relative offset (very likely to work but maybe not always)
+ int champRivalJump = romEntry.getValue("GymLeaderMovesTableOffset")
+ - Gen1Constants.champRivalOffsetFromGymLeaderMoves;
+ // nop out this jump
+ rom[champRivalJump] = GBConstants.gbZ80Nop;
+ rom[champRivalJump + 1] = GBConstants.gbZ80Nop;
+ }
+
+ }
+
+ @Override
+ public boolean hasRivalFinalBattle() {
+ return true;
+ }
+
+ @Override
+ public boolean isYellow() {
+ return romEntry.isYellow;
+ }
+
+ @Override
+ public boolean typeInGame(Type type) {
+ if (!type.isHackOnly && (type != Type.DARK && type != Type.STEEL && type != Type.FAIRY)) {
+ return true;
+ }
+ return romEntry.extraTypeReverse.containsKey(type);
+ }
+
+ @Override
+ public List<Integer> getMovesBannedFromLevelup() {
+ return Gen1Constants.bannedLevelupMoves;
+ }
+
+ private void updateTypeEffectiveness() {
+ List<TypeRelationship> typeEffectivenessTable = readTypeEffectivenessTable();
+ log("--Updating Type Effectiveness--");
+ for (TypeRelationship relationship : typeEffectivenessTable) {
+ // Change Poison 2x against bug (should be neutral) to Ice 0.5x against Fire (is currently neutral)
+ if (relationship.attacker == Type.POISON && relationship.defender == Type.BUG) {
+ relationship.attacker = Type.ICE;
+ relationship.defender = Type.FIRE;
+ relationship.effectiveness = Effectiveness.HALF;
+ log("Replaced: Poison super effective vs Bug => Ice not very effective vs Fire");
+ }
+
+ // Change Bug 2x against Poison to Bug 0.5x against Poison
+ else if (relationship.attacker == Type.BUG && relationship.defender == Type.POISON) {
+ relationship.effectiveness = Effectiveness.HALF;
+ log("Changed: Bug super effective vs Poison => Bug not very effective vs Poison");
+ }
+
+ // Change Ghost 0x against Psychic to Ghost 2x against Psychic
+ else if (relationship.attacker == Type.GHOST && relationship.defender == Type.PSYCHIC) {
+ relationship.effectiveness = Effectiveness.DOUBLE;
+ log("Changed: Psychic immune to Ghost => Ghost super effective vs Psychic");
+ }
+ }
+ logBlankLine();
+ writeTypeEffectivenessTable(typeEffectivenessTable);
+ effectivenessUpdated = true;
+ }
+
+ private List<TypeRelationship> readTypeEffectivenessTable() {
+ List<TypeRelationship> typeEffectivenessTable = new ArrayList<>();
+ int currentOffset = romEntry.getValue("TypeEffectivenessOffset");
+ int attackingType = rom[currentOffset];
+ while (attackingType != (byte) 0xFF) {
+ int defendingType = rom[currentOffset + 1];
+ int effectivenessInternal = rom[currentOffset + 2];
+ Type attacking = Gen1Constants.typeTable[attackingType];
+ Type defending = Gen1Constants.typeTable[defendingType];
+ Effectiveness effectiveness = null;
+ switch (effectivenessInternal) {
+ case 20:
+ effectiveness = Effectiveness.DOUBLE;
+ break;
+ case 10:
+ effectiveness = Effectiveness.NEUTRAL;
+ break;
+ case 5:
+ effectiveness = Effectiveness.HALF;
+ break;
+ case 0:
+ effectiveness = Effectiveness.ZERO;
+ break;
+ }
+ if (effectiveness != null) {
+ TypeRelationship relationship = new TypeRelationship(attacking, defending, effectiveness);
+ typeEffectivenessTable.add(relationship);
+ }
+ currentOffset += 3;
+ attackingType = rom[currentOffset];
+ }
+ return typeEffectivenessTable;
+ }
+
+ private void writeTypeEffectivenessTable(List<TypeRelationship> typeEffectivenessTable) {
+ int currentOffset = romEntry.getValue("TypeEffectivenessOffset");
+ for (TypeRelationship relationship : typeEffectivenessTable) {
+ rom[currentOffset] = Gen1Constants.typeToByte(relationship.attacker);
+ rom[currentOffset + 1] = Gen1Constants.typeToByte(relationship.defender);
+ byte effectivenessInternal = 0;
+ switch (relationship.effectiveness) {
+ case DOUBLE:
+ effectivenessInternal = 20;
+ break;
+ case NEUTRAL:
+ effectivenessInternal = 10;
+ break;
+ case HALF:
+ effectivenessInternal = 5;
+ break;
+ case ZERO:
+ effectivenessInternal = 0;
+ break;
+ }
+ rom[currentOffset + 2] = effectivenessInternal;
+ currentOffset += 3;
+ }
+ }
+
+ @Override
+ public Map<Integer, List<MoveLearnt>> getMovesLearnt() {
+ Map<Integer, List<MoveLearnt>> movesets = new TreeMap<>();
+ int pointersOffset = romEntry.getValue("PokemonMovesetsTableOffset");
+ int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset");
+ int pkmnCount = romEntry.getValue("InternalPokemonCount");
+ for (int i = 1; i <= pkmnCount; i++) {
+ int pointer = readWord(pointersOffset + (i - 1) * 2);
+ int realPointer = calculateOffset(bankOf(pointersOffset), pointer);
+ if (pokeRBYToNumTable[i] != 0) {
+ Pokemon pkmn = pokes[pokeRBYToNumTable[i]];
+ int statsOffset;
+ if (pokeRBYToNumTable[i] == Species.mew && !romEntry.isYellow) {
+ // Mewww
+ statsOffset = romEntry.getValue("MewStatsOffset");
+ } else {
+ statsOffset = (pokeRBYToNumTable[i] - 1) * 0x1C + pokeStatsOffset;
+ }
+ List<MoveLearnt> ourMoves = new ArrayList<>();
+ for (int delta = Gen1Constants.bsLevel1MovesOffset; delta < Gen1Constants.bsLevel1MovesOffset + 4; delta++) {
+ if (rom[statsOffset + delta] != 0x00) {
+ MoveLearnt learnt = new MoveLearnt();
+ learnt.level = 1;
+ learnt.move = moveRomToNumTable[rom[statsOffset + delta] & 0xFF];
+ ourMoves.add(learnt);
+ }
+ }
+ // Skip over evolution data
+ while (rom[realPointer] != 0) {
+ if (rom[realPointer] == 1) {
+ realPointer += 3;
+ } else if (rom[realPointer] == 2) {
+ realPointer += 4;
+ } else if (rom[realPointer] == 3) {
+ realPointer += 3;
+ }
+ }
+ realPointer++;
+ while (rom[realPointer] != 0) {
+ MoveLearnt learnt = new MoveLearnt();
+ learnt.level = rom[realPointer] & 0xFF;
+ learnt.move = moveRomToNumTable[rom[realPointer + 1] & 0xFF];
+ ourMoves.add(learnt);
+ realPointer += 2;
+ }
+ movesets.put(pkmn.number, ourMoves);
+ }
+ }
+ return movesets;
+ }
+
+ @Override
+ public void setMovesLearnt(Map<Integer, List<MoveLearnt>> movesets) {
+ // new method for moves learnt
+ writeEvosAndMovesLearnt(false, movesets);
+ }
+
+ @Override
+ public Map<Integer, List<Integer>> getEggMoves() {
+ // Gen 1 does not have egg moves
+ return new TreeMap<>();
+ }
+
+ @Override
+ public void setEggMoves(Map<Integer, List<Integer>> eggMoves) {
+ // Gen 1 does not have egg moves
+ }
+
+ private static class StaticPokemon {
+ protected int[] speciesOffsets;
+ protected int[] levelOffsets;
+
+ public StaticPokemon() {
+ this.speciesOffsets = new int[0];
+ this.levelOffsets = new int[0];
+ }
+
+ public Pokemon getPokemon(Gen1RomHandler rh) {
+ return rh.pokes[rh.pokeRBYToNumTable[rh.rom[speciesOffsets[0]] & 0xFF]];
+ }
+
+ public void setPokemon(Gen1RomHandler rh, Pokemon pkmn) {
+ for (int offset : speciesOffsets) {
+ rh.rom[offset] = (byte) rh.pokeNumToRBYTable[pkmn.number];
+ }
+ }
+
+ public int getLevel(byte[] rom, int i) {
+ if (levelOffsets.length <= i) {
+ return 1;
+ }
+ return rom[levelOffsets[i]];
+ }
+
+ public void setLevel(byte[] rom, int level, int i) {
+ rom[levelOffsets[i]] = (byte) level;
+ }
+ }
+
+ @Override
+ public List<StaticEncounter> getStaticPokemon() {
+ List<StaticEncounter> statics = new ArrayList<>();
+ if (romEntry.getValue("StaticPokemonSupport") > 0) {
+ for (StaticPokemon sp : romEntry.staticPokemon) {
+ StaticEncounter se = new StaticEncounter();
+ se.pkmn = sp.getPokemon(this);
+ se.level = sp.getLevel(rom, 0);
+ statics.add(se);
+ }
+ }
+ return statics;
+ }
+
+ @Override
+ public boolean setStaticPokemon(List<StaticEncounter> staticPokemon) {
+ if (romEntry.getValue("StaticPokemonSupport") == 0) {
+ return false;
+ }
+ for (int i = 0; i < romEntry.staticPokemon.size(); i++) {
+ StaticEncounter se = staticPokemon.get(i);
+ StaticPokemon sp = romEntry.staticPokemon.get(i);
+ sp.setPokemon(this, se.pkmn);
+ sp.setLevel(rom, se.level, 0);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean canChangeStaticPokemon() {
+ return (romEntry.getValue("StaticPokemonSupport") > 0);
+ }
+
+ @Override
+ public boolean hasStaticAltFormes() {
+ return false;
+ }
+
+ @Override
+ public boolean hasMainGameLegendaries() {
+ return false;
+ }
+
+ @Override
+ public List<Integer> getMainGameLegendaries() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getSpecialMusicStatics() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void applyCorrectStaticMusic(Map<Integer, Integer> specialMusicStaticChanges) {
+
+ }
+
+ @Override
+ public boolean hasStaticMusicFix() {
+ return false;
+ }
+
+ @Override
+ public List<TotemPokemon> getTotemPokemon() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setTotemPokemon(List<TotemPokemon> totemPokemon) {
+
+ }
+
+ @Override
+ public List<Integer> getTMMoves() {
+ List<Integer> tms = new ArrayList<>();
+ int offset = romEntry.getValue("TMMovesOffset");
+ for (int i = 1; i <= Gen1Constants.tmCount; i++) {
+ tms.add(moveRomToNumTable[rom[offset + (i - 1)] & 0xFF]);
+ }
+ return tms;
+ }
+
+ @Override
+ public List<Integer> getHMMoves() {
+ List<Integer> hms = new ArrayList<>();
+ int offset = romEntry.getValue("TMMovesOffset");
+ for (int i = 1; i <= Gen1Constants.hmCount; i++) {
+ hms.add(moveRomToNumTable[rom[offset + Gen1Constants.tmCount + (i - 1)] & 0xFF]);
+ }
+ return hms;
+ }
+
+ @Override
+ public void setTMMoves(List<Integer> moveIndexes) {
+ int offset = romEntry.getValue("TMMovesOffset");
+ for (int i = 1; i <= Gen1Constants.tmCount; i++) {
+ rom[offset + (i - 1)] = (byte) moveNumToRomTable[moveIndexes.get(i - 1)];
+ }
+
+ // Gym Leader TM Moves (RB only)
+ if (!romEntry.isYellow) {
+ int[] tms = Gen1Constants.gymLeaderTMs;
+ int glMovesOffset = romEntry.getValue("GymLeaderMovesTableOffset");
+ for (int i = 0; i < tms.length; i++) {
+ // Set the special move used by gym (i+1) to
+ // the move we just wrote to TM tms[i]
+ rom[glMovesOffset + i * 2] = (byte) moveNumToRomTable[moveIndexes.get(tms[i] - 1)];
+ }
+ }
+
+ // TM Text
+ String[] moveNames = readMoveNames();
+ for (TMTextEntry tte : romEntry.tmTexts) {
+ String moveName = moveNames[moveNumToRomTable[moveIndexes.get(tte.number - 1)]];
+ String text = tte.template.replace("%m", moveName);
+ writeVariableLengthString(text, tte.offset, true);
+ }
+ }
+
+ @Override
+ public int getTMCount() {
+ return Gen1Constants.tmCount;
+ }
+
+ @Override
+ public int getHMCount() {
+ return Gen1Constants.hmCount;
+ }
+
+ @Override
+ public Map<Pokemon, boolean[]> getTMHMCompatibility() {
+ Map<Pokemon, boolean[]> compat = new TreeMap<>();
+ int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset");
+ for (int i = 1; i <= pokedexCount; i++) {
+ int baseStatsOffset = (romEntry.isYellow || i != Species.mew) ? (pokeStatsOffset + (i - 1)
+ * Gen1Constants.baseStatsEntrySize) : romEntry.getValue("MewStatsOffset");
+ Pokemon pkmn = pokes[i];
+ boolean[] flags = new boolean[Gen1Constants.tmCount + Gen1Constants.hmCount + 1];
+ for (int j = 0; j < 7; j++) {
+ readByteIntoFlags(flags, j * 8 + 1, baseStatsOffset + Gen1Constants.bsTMHMCompatOffset + j);
+ }
+ compat.put(pkmn, flags);
+ }
+ return compat;
+ }
+
+ @Override
+ public void setTMHMCompatibility(Map<Pokemon, boolean[]> compatData) {
+ int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset");
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ int baseStatsOffset = (romEntry.isYellow || pkmn.number != Species.mew) ? (pokeStatsOffset + (pkmn.number - 1)
+ * Gen1Constants.baseStatsEntrySize)
+ : romEntry.getValue("MewStatsOffset");
+ for (int j = 0; j < 7; j++) {
+ rom[baseStatsOffset + Gen1Constants.bsTMHMCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasMoveTutors() {
+ return false;
+ }
+
+ @Override
+ public List<Integer> getMoveTutorMoves() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setMoveTutorMoves(List<Integer> moves) {
+ // Do nothing
+ }
+
+ @Override
+ public Map<Pokemon, boolean[]> getMoveTutorCompatibility() {
+ return new TreeMap<>();
+ }
+
+ @Override
+ public void setMoveTutorCompatibility(Map<Pokemon, boolean[]> compatData) {
+ // Do nothing
+ }
+
+ @Override
+ public String getROMName() {
+ return "Pokemon " + romEntry.name;
+ }
+
+ @Override
+ public String getROMCode() {
+ return romEntry.romName + " (" + romEntry.version + "/" + romEntry.nonJapanese + ")";
+ }
+
+ @Override
+ public String getSupportLevel() {
+ return (romEntry.getValue("StaticPokemonSupport") > 0) ? "Complete" : "No Static Pokemon";
+ }
+
+ private static int find(byte[] haystack, String hexString) {
+ if (hexString.length() % 2 != 0) {
+ return -3; // error
+ }
+ byte[] searchFor = new byte[hexString.length() / 2];
+ for (int i = 0; i < searchFor.length; i++) {
+ searchFor[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16);
+ }
+ List<Integer> found = RomFunctions.search(haystack, searchFor);
+ if (found.size() == 0) {
+ return -1; // not found
+ } else if (found.size() > 1) {
+ return -2; // not unique
+ } else {
+ return found.get(0);
+ }
+ }
+
+ private void populateEvolutions() {
+ for (Pokemon pkmn : pokes) {
+ if (pkmn != null) {
+ pkmn.evolutionsFrom.clear();
+ pkmn.evolutionsTo.clear();
+ }
+ }
+
+ int pointersOffset = romEntry.getValue("PokemonMovesetsTableOffset");
+
+ int pkmnCount = romEntry.getValue("InternalPokemonCount");
+ for (int i = 1; i <= pkmnCount; i++) {
+ int pointer = readWord(pointersOffset + (i - 1) * 2);
+ int realPointer = calculateOffset(bankOf(pointersOffset), pointer);
+ if (pokeRBYToNumTable[i] != 0) {
+ int thisPoke = pokeRBYToNumTable[i];
+ Pokemon pkmn = pokes[thisPoke];
+ while (rom[realPointer] != 0) {
+ int method = rom[realPointer];
+ EvolutionType type = EvolutionType.fromIndex(1, method);
+ int otherPoke = pokeRBYToNumTable[rom[realPointer + 2 + (type == EvolutionType.STONE ? 1 : 0)] & 0xFF];
+ int extraInfo = rom[realPointer + 1] & 0xFF;
+ Evolution evo = new Evolution(pkmn, pokes[otherPoke], true, type, extraInfo);
+ if (!pkmn.evolutionsFrom.contains(evo)) {
+ pkmn.evolutionsFrom.add(evo);
+ if (pokes[otherPoke] != null) {
+ pokes[otherPoke].evolutionsTo.add(evo);
+ }
+ }
+ realPointer += (type == EvolutionType.STONE ? 4 : 3);
+ }
+ // split evos don't carry stats
+ if (pkmn.evolutionsFrom.size() > 1) {
+ for (Evolution e : pkmn.evolutionsFrom) {
+ e.carryStats = false;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void removeImpossibleEvolutions(Settings settings) {
+ // Gen 1: only regular trade evos
+ // change them all to evolve at level 37
+ for (Pokemon pkmn : pokes) {
+ if (pkmn != null) {
+ for (Evolution evo : pkmn.evolutionsFrom) {
+ if (evo.type == EvolutionType.TRADE) {
+ // change
+ evo.type = EvolutionType.LEVEL;
+ evo.extraInfo = 37;
+ addEvoUpdateLevel(impossibleEvolutionUpdates,evo);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void makeEvolutionsEasier(Settings settings) {
+ // No such thing
+ }
+
+ @Override
+ public void removeTimeBasedEvolutions() {
+ // No such thing
+ }
+
+ @Override
+ public boolean hasShopRandomization() {
+ return false;
+ }
+
+ @Override
+ public Map<Integer, Shop> getShopItems() {
+ return null; // Not implemented
+ }
+
+ @Override
+ public void setShopItems(Map<Integer, Shop> shopItems) {
+ // Not implemented
+ }
+
+ @Override
+ public void setShopPrices() {
+ // Not implemented
+ }
+
+ private List<String> getTrainerClassesForText() {
+ int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets");
+ List<String> tcNames = new ArrayList<>();
+ int offset = offsets[offsets.length - 1];
+ for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) {
+ String name = readVariableLengthString(offset, false);
+ offset += lengthOfStringAt(offset, false) + 1;
+ tcNames.add(name);
+ }
+ return tcNames;
+ }
+
+ @Override
+ public boolean canChangeTrainerText() {
+ return romEntry.getValue("CanChangeTrainerText") > 0;
+ }
+
+ @Override
+ public List<Integer> getDoublesTrainerClasses() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<String> getTrainerNames() {
+ int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets");
+ List<String> trainerNames = new ArrayList<>();
+ int offset = offsets[offsets.length - 1];
+ for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) {
+ String name = readVariableLengthString(offset, false);
+ offset += lengthOfStringAt(offset, false) + 1;
+ if (Gen1Constants.singularTrainers.contains(j)) {
+ trainerNames.add(name);
+ }
+ }
+ return trainerNames;
+ }
+
+ @Override
+ public void setTrainerNames(List<String> trainerNames) {
+ if (romEntry.getValue("CanChangeTrainerText") > 0) {
+ int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets");
+ Iterator<String> trainerNamesI = trainerNames.iterator();
+ int offset = offsets[offsets.length - 1];
+ for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) {
+ int oldLength = lengthOfStringAt(offset, false) + 1;
+ if (Gen1Constants.singularTrainers.contains(j)) {
+ String newName = trainerNamesI.next();
+ writeFixedLengthString(newName, offset, oldLength);
+ }
+ offset += oldLength;
+ }
+ }
+ }
+
+ @Override
+ public TrainerNameMode trainerNameMode() {
+ return TrainerNameMode.SAME_LENGTH;
+ }
+
+ @Override
+ public List<Integer> getTCNameLengthsByTrainer() {
+ // not needed
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<String> getTrainerClassNames() {
+ int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets");
+ List<String> trainerClassNames = new ArrayList<>();
+ if (offsets.length == 2) {
+ for (int i = 0; i < offsets.length; i++) {
+ int offset = offsets[i];
+ for (int j = 0; j < Gen1Constants.tclassesCounts[i]; j++) {
+ String name = readVariableLengthString(offset, false);
+ offset += lengthOfStringAt(offset, false) + 1;
+ if (i == 0 || !Gen1Constants.singularTrainers.contains(j)) {
+ trainerClassNames.add(name);
+ }
+ }
+ }
+ } else {
+ int offset = offsets[0];
+ for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) {
+ String name = readVariableLengthString(offset, false);
+ offset += lengthOfStringAt(offset, false) + 1;
+ if (!Gen1Constants.singularTrainers.contains(j)) {
+ trainerClassNames.add(name);
+ }
+ }
+ }
+ return trainerClassNames;
+ }
+
+ @Override
+ public void setTrainerClassNames(List<String> trainerClassNames) {
+ if (romEntry.getValue("CanChangeTrainerText") > 0) {
+ int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets");
+ Iterator<String> tcNamesIter = trainerClassNames.iterator();
+ if (offsets.length == 2) {
+ for (int i = 0; i < offsets.length; i++) {
+ int offset = offsets[i];
+ for (int j = 0; j < Gen1Constants.tclassesCounts[i]; j++) {
+ int oldLength = lengthOfStringAt(offset, false) + 1;
+ if (i == 0 || !Gen1Constants.singularTrainers.contains(j)) {
+ String newName = tcNamesIter.next();
+ writeFixedLengthString(newName, offset, oldLength);
+ }
+ offset += oldLength;
+ }
+ }
+ } else {
+ int offset = offsets[0];
+ for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) {
+ int oldLength = lengthOfStringAt(offset, false) + 1;
+ if (!Gen1Constants.singularTrainers.contains(j)) {
+ String newName = tcNamesIter.next();
+ writeFixedLengthString(newName, offset, oldLength);
+ }
+ offset += oldLength;
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public boolean fixedTrainerClassNamesLength() {
+ return true;
+ }
+
+ @Override
+ public String getDefaultExtension() {
+ return "gbc";
+ }
+
+ @Override
+ public int abilitiesPerPokemon() {
+ return 0;
+ }
+
+ @Override
+ public int highestAbilityIndex() {
+ return 0;
+ }
+
+ @Override
+ public Map<Integer, List<Integer>> getAbilityVariations() {
+ return new HashMap<>();
+ }
+
+ @Override
+ public boolean hasMegaEvolutions() {
+ return false;
+ }
+
+ @Override
+ public int internalStringLength(String string) {
+ return translateString(string).length;
+ }
+
+ @Override
+ public int miscTweaksAvailable() {
+ int available = MiscTweak.LOWER_CASE_POKEMON_NAMES.getValue();
+ available |= MiscTweak.UPDATE_TYPE_EFFECTIVENESS.getValue();
+
+ if (romEntry.tweakFiles.get("BWXPTweak") != null) {
+ available |= MiscTweak.BW_EXP_PATCH.getValue();
+ }
+ if (romEntry.tweakFiles.get("XAccNerfTweak") != null) {
+ available |= MiscTweak.NERF_X_ACCURACY.getValue();
+ }
+ if (romEntry.tweakFiles.get("CritRateTweak") != null) {
+ available |= MiscTweak.FIX_CRIT_RATE.getValue();
+ }
+ if (romEntry.getValue("TextDelayFunctionOffset") != 0) {
+ available |= MiscTweak.FASTEST_TEXT.getValue();
+ }
+ if (romEntry.getValue("PCPotionOffset") != 0) {
+ available |= MiscTweak.RANDOMIZE_PC_POTION.getValue();
+ }
+ if (romEntry.getValue("PikachuEvoJumpOffset") != 0) {
+ available |= MiscTweak.ALLOW_PIKACHU_EVOLUTION.getValue();
+ }
+ if (romEntry.getValue("CatchingTutorialMonOffset") != 0) {
+ available |= MiscTweak.RANDOMIZE_CATCHING_TUTORIAL.getValue();
+ }
+
+ return available;
+ }
+
+ @Override
+ public void applyMiscTweak(MiscTweak tweak) {
+ if (tweak == MiscTweak.BW_EXP_PATCH) {
+ applyBWEXPPatch();
+ } else if (tweak == MiscTweak.NERF_X_ACCURACY) {
+ applyXAccNerfPatch();
+ } else if (tweak == MiscTweak.FIX_CRIT_RATE) {
+ applyCritRatePatch();
+ } else if (tweak == MiscTweak.FASTEST_TEXT) {
+ applyFastestTextPatch();
+ } else if (tweak == MiscTweak.RANDOMIZE_PC_POTION) {
+ randomizePCPotion();
+ } else if (tweak == MiscTweak.ALLOW_PIKACHU_EVOLUTION) {
+ applyPikachuEvoPatch();
+ } else if (tweak == MiscTweak.LOWER_CASE_POKEMON_NAMES) {
+ applyCamelCaseNames();
+ } else if (tweak == MiscTweak.UPDATE_TYPE_EFFECTIVENESS) {
+ updateTypeEffectiveness();
+ } else if (tweak == MiscTweak.RANDOMIZE_CATCHING_TUTORIAL) {
+ randomizeCatchingTutorial();
+ }
+ }
+
+ @Override
+ public boolean isEffectivenessUpdated() {
+ return effectivenessUpdated;
+ }
+
+ private void applyBWEXPPatch() {
+ genericIPSPatch("BWXPTweak");
+ }
+
+ private void applyXAccNerfPatch() {
+ xAccNerfed = genericIPSPatch("XAccNerfTweak");
+ }
+
+ private void applyCritRatePatch() {
+ genericIPSPatch("CritRateTweak");
+ }
+
+ private void applyFastestTextPatch() {
+ if (romEntry.getValue("TextDelayFunctionOffset") != 0) {
+ rom[romEntry.getValue("TextDelayFunctionOffset")] = GBConstants.gbZ80Ret;
+ }
+ }
+
+ private void randomizePCPotion() {
+ if (romEntry.getValue("PCPotionOffset") != 0) {
+ rom[romEntry.getValue("PCPotionOffset")] = (byte) this.getNonBadItems().randomNonTM(this.random);
+ }
+ }
+
+ private void applyPikachuEvoPatch() {
+ if (romEntry.getValue("PikachuEvoJumpOffset") != 0) {
+ rom[romEntry.getValue("PikachuEvoJumpOffset")] = GBConstants.gbZ80JumpRelative;
+ }
+ }
+
+ private void randomizeCatchingTutorial() {
+ if (romEntry.getValue("CatchingTutorialMonOffset") != 0) {
+ rom[romEntry.getValue("CatchingTutorialMonOffset")] = (byte) pokeNumToRBYTable[this.randomPokemon().number];
+ }
+ }
+
+ @Override
+ public void enableGuaranteedPokemonCatching() {
+ int offset = find(rom, Gen1Constants.guaranteedCatchPrefix);
+ if (offset > 0) {
+ offset += Gen1Constants.guaranteedCatchPrefix.length() / 2; // because it was a prefix
+
+ // The game ensures that the Master Ball always catches a Pokemon by running the following code:
+ // ; Get the item ID.
+ // ld hl, wcf91
+ // ld a, [hl]
+ //
+ // ; The Master Ball always succeeds.
+ // cp MASTER_BALL
+ // jp z, .captured
+ // By making the jump here unconditional, we can ensure that catching always succeeds no
+ // matter the ball type. We check that the original condition is present just for safety.
+ if (rom[offset] == (byte)0xCA) {
+ rom[offset] = (byte)0xC3;
+ }
+ }
+ }
+
+ private boolean genericIPSPatch(String ctName) {
+ String patchName = romEntry.tweakFiles.get(ctName);
+ if (patchName == null) {
+ return false;
+ }
+
+ try {
+ FileFunctions.applyPatch(rom, patchName);
+ return true;
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public List<Integer> getGameBreakingMoves() {
+ // Sonicboom & drage & OHKO moves
+ // 160 add spore
+ // also remove OHKO if xacc nerfed
+ if (xAccNerfed) {
+ return Gen1Constants.bannedMovesWithXAccBanned;
+ } else {
+ return Gen1Constants.bannedMovesWithoutXAccBanned;
+ }
+ }
+
+ @Override
+ public List<Integer> getFieldMoves() {
+ // cut, fly, surf, strength, flash,
+ // dig, teleport (NOT softboiled)
+ return Gen1Constants.fieldMoves;
+ }
+
+ @Override
+ public List<Integer> getEarlyRequiredHMMoves() {
+ // just cut
+ return Gen1Constants.earlyRequiredHMs;
+ }
+
+ @Override
+ public void randomizeIntroPokemon() {
+ // First off, intro Pokemon
+ // 160 add yellow intro random
+ int introPokemon = pokeNumToRBYTable[this.randomPokemon().number];
+ rom[romEntry.getValue("IntroPokemonOffset")] = (byte) introPokemon;
+ rom[romEntry.getValue("IntroCryOffset")] = (byte) introPokemon;
+
+ }
+
+ @Override
+ public ItemList getAllowedItems() {
+ return Gen1Constants.allowedItems;
+ }
+
+ @Override
+ public ItemList getNonBadItems() {
+ // Gen 1 has no bad items Kappa
+ return Gen1Constants.allowedItems;
+ }
+
+ @Override
+ public List<Integer> getUniqueNoSellItems() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getRegularShopItems() {
+ return null; // Not implemented
+ }
+
+ @Override
+ public List<Integer> getOPShopItems() {
+ return null; // Not implemented
+ }
+
+ private void loadItemNames() {
+ itemNames = new String[256];
+ itemNames[0] = "glitch";
+ // trying to emulate pretty much what the game does here
+ // normal items
+ int origOffset = romEntry.getValue("ItemNamesOffset");
+ int itemNameOffset = origOffset;
+ for (int index = 1; index <= 0x100; index++) {
+ if (itemNameOffset / GBConstants.bankSize > origOffset / GBConstants.bankSize) {
+ // the game would continue making its merry way into VRAM here,
+ // but we don't have VRAM to simulate.
+ // just give up.
+ break;
+ }
+ int startOfText = itemNameOffset;
+ while ((rom[itemNameOffset] & 0xFF) != GBConstants.stringTerminator) {
+ itemNameOffset++;
+ }
+ itemNameOffset++;
+ itemNames[index % 256] = readFixedLengthString(startOfText, 20);
+ }
+ // hms override
+ for (int index = Gen1Constants.hmsStartIndex; index < Gen1Constants.tmsStartIndex; index++) {
+ itemNames[index] = String.format("HM%02d", index - Gen1Constants.hmsStartIndex + 1);
+ }
+ // tms override
+ for (int index = Gen1Constants.tmsStartIndex; index < 0x100; index++) {
+ itemNames[index] = String.format("TM%02d", index - Gen1Constants.tmsStartIndex + 1);
+ }
+ }
+
+ @Override
+ public String[] getItemNames() {
+ return itemNames;
+ }
+
+ private static class SubMap {
+ private int id;
+ private int addr;
+ private int bank;
+ private MapHeader header;
+ private Connection[] cons;
+ private int n_cons;
+ private int obj_addr;
+ private List<Integer> itemOffsets;
+ }
+
+ private static class MapHeader {
+ private int tileset_id; // u8
+ private int map_h, map_w; // u8
+ private int map_ptr, text_ptr, script_ptr; // u16
+ private int connect_byte; // u8
+ // 10 bytes
+ }
+
+ private static class Connection {
+ private int index; // u8
+ private int connected_map; // u16
+ private int current_map; // u16
+ private int bigness; // u8
+ private int map_width; // u8
+ private int y_align; // u8
+ private int x_align; // u8
+ private int window; // u16
+ // 11 bytes
+ }
+
+ private void preloadMaps() {
+ int mapBanks = romEntry.getValue("MapBanks");
+ int mapAddresses = romEntry.getValue("MapAddresses");
+
+ preloadMap(mapBanks, mapAddresses, 0);
+ }
+
+ private void preloadMap(int mapBanks, int mapAddresses, int mapID) {
+
+ if (maps[mapID] != null || mapID == 0xED || mapID == 0xFF) {
+ return;
+ }
+
+ SubMap map = new SubMap();
+ maps[mapID] = map;
+
+ map.id = mapID;
+ map.addr = calculateOffset(rom[mapBanks + mapID] & 0xFF, readWord(mapAddresses + mapID * 2));
+ map.bank = bankOf(map.addr);
+
+ map.header = new MapHeader();
+ map.header.tileset_id = rom[map.addr] & 0xFF;
+ map.header.map_h = rom[map.addr + 1] & 0xFF;
+ map.header.map_w = rom[map.addr + 2] & 0xFF;
+ map.header.map_ptr = calculateOffset(map.bank, readWord(map.addr + 3));
+ map.header.text_ptr = calculateOffset(map.bank, readWord(map.addr + 5));
+ map.header.script_ptr = calculateOffset(map.bank, readWord(map.addr + 7));
+ map.header.connect_byte = rom[map.addr + 9] & 0xFF;
+
+ int cb = map.header.connect_byte;
+ map.n_cons = ((cb & 8) >> 3) + ((cb & 4) >> 2) + ((cb & 2) >> 1) + (cb & 1);
+
+ int cons_offset = map.addr + 10;
+
+ map.cons = new Connection[map.n_cons];
+ for (int i = 0; i < map.n_cons; i++) {
+ int tcon_offs = cons_offset + i * 11;
+ Connection con = new Connection();
+ con.index = rom[tcon_offs] & 0xFF;
+ con.connected_map = readWord(tcon_offs + 1);
+ con.current_map = readWord(tcon_offs + 3);
+ con.bigness = rom[tcon_offs + 5] & 0xFF;
+ con.map_width = rom[tcon_offs + 6] & 0xFF;
+ con.y_align = rom[tcon_offs + 7] & 0xFF;
+ con.x_align = rom[tcon_offs + 8] & 0xFF;
+ con.window = readWord(tcon_offs + 9);
+ map.cons[i] = con;
+ preloadMap(mapBanks, mapAddresses, con.index);
+ }
+ map.obj_addr = calculateOffset(map.bank, readWord(cons_offset + map.n_cons * 11));
+
+ // Read objects
+ // +0 is the border tile (ignore)
+ // +1 is warp count
+
+ int n_warps = rom[map.obj_addr + 1] & 0xFF;
+ int offs = map.obj_addr + 2;
+ for (int i = 0; i < n_warps; i++) {
+ // track this warp
+ int to_map = rom[offs + 3] & 0xFF;
+ preloadMap(mapBanks, mapAddresses, to_map);
+ offs += 4;
+ }
+
+ // Now we're pointing to sign count
+ int n_signs = rom[offs++] & 0xFF;
+ offs += n_signs * 3;
+
+ // Finally, entities, which contain the items
+ map.itemOffsets = new ArrayList<>();
+ int n_entities = rom[offs++] & 0xFF;
+ for (int i = 0; i < n_entities; i++) {
+ // Read text ID
+ int tid = rom[offs + 5] & 0xFF;
+ if ((tid & (1 << 6)) > 0) {
+ // trainer
+ offs += 8;
+ } else if ((tid & (1 << 7)) > 0 && (rom[offs + 6] != 0x00)) {
+ // item
+ map.itemOffsets.add(offs + 6);
+ offs += 7;
+ } else {
+ // generic
+ offs += 6;
+ }
+ }
+ }
+
+ private void loadMapNames() {
+ mapNames = new String[256];
+ int mapNameTableOffset = romEntry.getValue("MapNameTableOffset");
+ int mapNameBank = bankOf(mapNameTableOffset);
+ // external names
+ List<Integer> usedExternal = new ArrayList<>();
+ for (int i = 0; i < 0x25; i++) {
+ int externalOffset = calculateOffset(mapNameBank, readWord(mapNameTableOffset + 1));
+ usedExternal.add(externalOffset);
+ mapNames[i] = readVariableLengthString(externalOffset, false);
+ mapNameTableOffset += 3;
+ }
+
+ // internal names
+ int lastMaxMap = 0x25;
+ Map<Integer, Integer> previousMapCounts = new HashMap<>();
+ while ((rom[mapNameTableOffset] & 0xFF) != 0xFF) {
+ int maxMap = rom[mapNameTableOffset] & 0xFF;
+ int nameOffset = calculateOffset(mapNameBank, readWord(mapNameTableOffset + 2));
+ String actualName = readVariableLengthString(nameOffset, false).trim();
+ if (usedExternal.contains(nameOffset)) {
+ for (int i = lastMaxMap; i < maxMap; i++) {
+ if (maps[i] != null) {
+ mapNames[i] = actualName + " (Building)";
+ }
+ }
+ } else {
+ int mapCount = 0;
+ if (previousMapCounts.containsKey(nameOffset)) {
+ mapCount = previousMapCounts.get(nameOffset);
+ }
+ for (int i = lastMaxMap; i < maxMap; i++) {
+ if (maps[i] != null) {
+ mapCount++;
+ mapNames[i] = actualName + " (" + mapCount + ")";
+ }
+ }
+ previousMapCounts.put(nameOffset, mapCount);
+ }
+ lastMaxMap = maxMap;
+ mapNameTableOffset += 4;
+ }
+ }
+
+ private List<Integer> getItemOffsets() {
+
+ List<Integer> itemOffs = new ArrayList<>();
+
+ for (SubMap map : maps) {
+ if (map != null) {
+ itemOffs.addAll(map.itemOffsets);
+ }
+ }
+
+ int hiRoutine = romEntry.getValue("HiddenItemRoutine");
+ int spclTable = romEntry.getValue("SpecialMapPointerTable");
+ int spclBank = bankOf(spclTable);
+
+ if (!isYellow()) {
+
+ int lOffs = romEntry.getValue("SpecialMapList");
+ int idx = 0;
+
+ while ((rom[lOffs] & 0xFF) != 0xFF) {
+
+ int spclOffset = calculateOffset(spclBank, readWord(spclTable + idx));
+
+ while ((rom[spclOffset] & 0xFF) != 0xFF) {
+ if (calculateOffset(rom[spclOffset + 3] & 0xFF, readWord(spclOffset + 4)) == hiRoutine) {
+ itemOffs.add(spclOffset + 2);
+ }
+ spclOffset += 6;
+ }
+ lOffs++;
+ idx += 2;
+ }
+ } else {
+
+ int lOffs = spclTable;
+
+ while ((rom[lOffs] & 0xFF) != 0xFF) {
+
+ int spclOffset = calculateOffset(spclBank, readWord(lOffs + 1));
+
+ while ((rom[spclOffset] & 0xFF) != 0xFF) {
+ if (calculateOffset(rom[spclOffset + 3] & 0xFF, readWord(spclOffset + 4)) == hiRoutine) {
+ itemOffs.add(spclOffset + 2);
+ }
+ spclOffset += 6;
+ }
+ lOffs += 3;
+ }
+ }
+
+ return itemOffs;
+ }
+
+ @Override
+ public List<Integer> getRequiredFieldTMs() {
+ return Gen1Constants.requiredFieldTMs;
+ }
+
+ @Override
+ public List<Integer> getCurrentFieldTMs() {
+ List<Integer> itemOffsets = getItemOffsets();
+ List<Integer> fieldTMs = new ArrayList<>();
+
+ for (int offset : itemOffsets) {
+ int itemHere = rom[offset] & 0xFF;
+ if (Gen1Constants.allowedItems.isTM(itemHere)) {
+ fieldTMs.add(itemHere - Gen1Constants.tmsStartIndex + 1); // TM
+ // offset
+ }
+ }
+ return fieldTMs;
+ }
+
+ @Override
+ public void setFieldTMs(List<Integer> fieldTMs) {
+ List<Integer> itemOffsets = getItemOffsets();
+ Iterator<Integer> iterTMs = fieldTMs.iterator();
+
+ for (int offset : itemOffsets) {
+ int itemHere = rom[offset] & 0xFF;
+ if (Gen1Constants.allowedItems.isTM(itemHere)) {
+ // Replace this with a TM from the list
+ rom[offset] = (byte) (iterTMs.next() + Gen1Constants.tmsStartIndex - 1);
+ }
+ }
+ }
+
+ @Override
+ public List<Integer> getRegularFieldItems() {
+ List<Integer> itemOffsets = getItemOffsets();
+ List<Integer> fieldItems = new ArrayList<>();
+
+ for (int offset : itemOffsets) {
+ int itemHere = rom[offset] & 0xFF;
+ if (Gen1Constants.allowedItems.isAllowed(itemHere) && !(Gen1Constants.allowedItems.isTM(itemHere))) {
+ fieldItems.add(itemHere);
+ }
+ }
+ return fieldItems;
+ }
+
+ @Override
+ public void setRegularFieldItems(List<Integer> items) {
+ List<Integer> itemOffsets = getItemOffsets();
+ Iterator<Integer> iterItems = items.iterator();
+
+ for (int offset : itemOffsets) {
+ int itemHere = rom[offset] & 0xFF;
+ if (Gen1Constants.allowedItems.isAllowed(itemHere) && !(Gen1Constants.allowedItems.isTM(itemHere))) {
+ // Replace it
+ rom[offset] = (byte) (iterItems.next().intValue());
+ }
+ }
+
+ }
+
+ @Override
+ public List<IngameTrade> getIngameTrades() {
+ List<IngameTrade> trades = new ArrayList<>();
+
+ // info
+ int tableOffset = romEntry.getValue("TradeTableOffset");
+ int tableSize = romEntry.getValue("TradeTableSize");
+ int nicknameLength = romEntry.getValue("TradeNameLength");
+ int[] unused = romEntry.arrayEntries.get("TradesUnused");
+ int unusedOffset = 0;
+ int entryLength = nicknameLength + 3;
+
+ for (int entry = 0; entry < tableSize; entry++) {
+ if (unusedOffset < unused.length && unused[unusedOffset] == entry) {
+ unusedOffset++;
+ continue;
+ }
+ IngameTrade trade = new IngameTrade();
+ int entryOffset = tableOffset + entry * entryLength;
+ trade.requestedPokemon = pokes[pokeRBYToNumTable[rom[entryOffset] & 0xFF]];
+ trade.givenPokemon = pokes[pokeRBYToNumTable[rom[entryOffset + 1] & 0xFF]];
+ trade.nickname = readString(entryOffset + 3, nicknameLength, false);
+ trades.add(trade);
+ }
+
+ return trades;
+ }
+
+ @Override
+ public void setIngameTrades(List<IngameTrade> trades) {
+
+ // info
+ int tableOffset = romEntry.getValue("TradeTableOffset");
+ int tableSize = romEntry.getValue("TradeTableSize");
+ int nicknameLength = romEntry.getValue("TradeNameLength");
+ int[] unused = romEntry.arrayEntries.get("TradesUnused");
+ int unusedOffset = 0;
+ int entryLength = nicknameLength + 3;
+ int tradeOffset = 0;
+
+ for (int entry = 0; entry < tableSize; entry++) {
+ if (unusedOffset < unused.length && unused[unusedOffset] == entry) {
+ unusedOffset++;
+ continue;
+ }
+ IngameTrade trade = trades.get(tradeOffset++);
+ int entryOffset = tableOffset + entry * entryLength;
+ rom[entryOffset] = (byte) pokeNumToRBYTable[trade.requestedPokemon.number];
+ rom[entryOffset + 1] = (byte) pokeNumToRBYTable[trade.givenPokemon.number];
+ if (romEntry.getValue("CanChangeTrainerText") > 0) {
+ writeFixedLengthString(trade.nickname, entryOffset + 3, nicknameLength);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasDVs() {
+ return true;
+ }
+
+ @Override
+ public int generationOfPokemon() {
+ return 1;
+ }
+
+ @Override
+ public void removeEvosForPokemonPool() {
+ // gen1 doesn't have this functionality anyway
+ }
+
+ @Override
+ public boolean supportsFourStartingMoves() {
+ return true;
+ }
+
+ private void writeEvosAndMovesLearnt(boolean writeEvos, Map<Integer, List<MoveLearnt>> movesets) {
+ // we assume a few things here:
+ // 1) evos & moves learnt are stored directly after their pointer table
+ // 2) PokemonMovesetsExtraSpaceOffset is in the same bank, and
+ // points to the start of the free space at the end of the bank
+ // (if set to 0, disabled from being used)
+ // 3) PokemonMovesetsDataSize is from the start of actual data to
+ // the start of engine/battle/e_2.asm in pokered (aka code we can't
+ // overwrite)
+ // it appears that in yellow, this code is moved
+ // so we can write the evos/movesets in one continuous block
+ // until the end of the bank.
+ // so for yellow, extraspace is disabled.
+ // specify null to either argument to copy old values
+ int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset");
+ int movesEvosStart = romEntry.getValue("PokemonMovesetsTableOffset");
+ int movesEvosBank = bankOf(movesEvosStart);
+ int pkmnCount = romEntry.getValue("InternalPokemonCount");
+ byte[] pointerTable = new byte[pkmnCount * 2];
+ int mainDataBlockSize = romEntry.getValue("PokemonMovesetsDataSize");
+ int mainDataBlockOffset = movesEvosStart + pointerTable.length;
+ byte[] mainDataBlock = new byte[mainDataBlockSize];
+ int offsetInMainData = 0;
+ int extraSpaceOffset = romEntry.getValue("PokemonMovesetsExtraSpaceOffset");
+ int extraSpaceBank = bankOf(extraSpaceOffset);
+ boolean extraSpaceEnabled = false;
+ byte[] extraDataBlock = null;
+ int offsetInExtraData = 0;
+ int extraSpaceSize = 0;
+ if (movesEvosBank == extraSpaceBank && extraSpaceOffset != 0) {
+ extraSpaceEnabled = true;
+ int startOfNextBank = ((extraSpaceOffset / GBConstants.bankSize) + 1) * GBConstants.bankSize;
+ extraSpaceSize = startOfNextBank - extraSpaceOffset;
+ extraDataBlock = new byte[extraSpaceSize];
+ }
+ int nullEntryPointer = -1;
+
+ for (int i = 1; i <= pkmnCount; i++) {
+ byte[] writeData = null;
+ int oldDataOffset = calculateOffset(movesEvosBank, readWord(movesEvosStart + (i - 1) * 2));
+ boolean setNullEntryPointerHere = false;
+ if (pokeRBYToNumTable[i] == 0) {
+ // null entry
+ if (nullEntryPointer == -1) {
+ // make the null entry
+ writeData = new byte[] { 0, 0 };
+ setNullEntryPointerHere = true;
+ } else {
+ writeWord(pointerTable, (i - 1) * 2, nullEntryPointer);
+ }
+ } else {
+ int pokeNum = pokeRBYToNumTable[i];
+ Pokemon pkmn = pokes[pokeNum];
+ ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
+ // Evolutions
+ if (!writeEvos) {
+ // copy old
+ int evoOffset = oldDataOffset;
+ while (rom[evoOffset] != 0x00) {
+ int method = rom[evoOffset] & 0xFF;
+ int limiter = (method == 2) ? 4 : 3;
+ for (int b = 0; b < limiter; b++) {
+ dataStream.write(rom[evoOffset++] & 0xFF);
+ }
+ }
+ } else {
+ for (Evolution evo : pkmn.evolutionsFrom) {
+ // write evos for this poke
+ dataStream.write(evo.type.toIndex(1));
+ if (evo.type == EvolutionType.LEVEL) {
+ dataStream.write(evo.extraInfo); // min lvl
+ } else if (evo.type == EvolutionType.STONE) {
+ dataStream.write(evo.extraInfo); // stone item
+ dataStream.write(1); // minimum level
+ } else if (evo.type == EvolutionType.TRADE) {
+ dataStream.write(1); // minimum level
+ }
+ int pokeIndexTo = pokeNumToRBYTable[evo.to.number];
+ dataStream.write(pokeIndexTo); // species
+ }
+ }
+ // write terminator for evos
+ dataStream.write(0);
+
+ // Movesets
+ if (movesets == null) {
+ // copy old
+ int movesOffset = oldDataOffset;
+ // move past evos
+ while (rom[movesOffset] != 0x00) {
+ int method = rom[movesOffset] & 0xFF;
+ movesOffset += (method == 2) ? 4 : 3;
+ }
+ movesOffset++;
+ // copy moves
+ while (rom[movesOffset] != 0x00) {
+ dataStream.write(rom[movesOffset++] & 0xFF);
+ dataStream.write(rom[movesOffset++] & 0xFF);
+ }
+ } else {
+ List<MoveLearnt> ourMoves = movesets.get(pkmn.number);
+ int statsOffset;
+ if (pokeNum == Species.mew && !romEntry.isYellow) {
+ // Mewww
+ statsOffset = romEntry.getValue("MewStatsOffset");
+ } else {
+ statsOffset = (pokeNum - 1) * Gen1Constants.baseStatsEntrySize + pokeStatsOffset;
+ }
+ int movenum = 0;
+ while (movenum < 4 && ourMoves.size() > movenum && ourMoves.get(movenum).level == 1) {
+ rom[statsOffset + Gen1Constants.bsLevel1MovesOffset + movenum] = (byte) moveNumToRomTable[ourMoves
+ .get(movenum).move];
+ movenum++;
+ }
+ // Write out the rest of zeroes
+ for (int mn = movenum; mn < 4; mn++) {
+ rom[statsOffset + Gen1Constants.bsLevel1MovesOffset + mn] = 0;
+ }
+ // Add the non level 1 moves to the data stream
+ while (movenum < ourMoves.size()) {
+ dataStream.write(ourMoves.get(movenum).level);
+ dataStream.write(moveNumToRomTable[ourMoves.get(movenum).move]);
+ movenum++;
+ }
+ }
+ // terminator
+ dataStream.write(0);
+
+ // done, set writeData
+ writeData = dataStream.toByteArray();
+ try {
+ dataStream.close();
+ } catch (IOException e) {
+ }
+ }
+
+ // write data and set pointer?
+ if (writeData != null) {
+ int lengthToFit = writeData.length;
+ int pointerToWrite;
+ // compression of leading & trailing 0s:
+ // every entry ends in a 0 (end of move list).
+ // if a block already has data in it, and the data
+ // we want to write starts with a 0 (no evolutions)
+ // we can compress it into the end of the last entry
+ // this saves a decent amount of space overall.
+ if ((offsetInMainData + lengthToFit <= mainDataBlockSize)
+ || (writeData[0] == 0 && offsetInMainData > 0 && offsetInMainData + lengthToFit == mainDataBlockSize + 1)) {
+ // place in main storage
+ if (writeData[0] == 0 && offsetInMainData > 0) {
+ int writtenDataOffset = mainDataBlockOffset + offsetInMainData - 1;
+ pointerToWrite = makeGBPointer(writtenDataOffset);
+ System.arraycopy(writeData, 1, mainDataBlock, offsetInMainData, lengthToFit - 1);
+ offsetInMainData += lengthToFit - 1;
+ } else {
+ int writtenDataOffset = mainDataBlockOffset + offsetInMainData;
+ pointerToWrite = makeGBPointer(writtenDataOffset);
+ System.arraycopy(writeData, 0, mainDataBlock, offsetInMainData, lengthToFit);
+ offsetInMainData += lengthToFit;
+ }
+ } else if (extraSpaceEnabled
+ && ((offsetInExtraData + lengthToFit <= extraSpaceSize) || (writeData[0] == 0
+ && offsetInExtraData > 0 && offsetInExtraData + lengthToFit == extraSpaceSize + 1))) {
+ // place in extra space
+ if (writeData[0] == 0 && offsetInExtraData > 0) {
+ int writtenDataOffset = extraSpaceOffset + offsetInExtraData - 1;
+ pointerToWrite = makeGBPointer(writtenDataOffset);
+ System.arraycopy(writeData, 1, extraDataBlock, offsetInExtraData, lengthToFit - 1);
+ offsetInExtraData += lengthToFit - 1;
+ } else {
+ int writtenDataOffset = extraSpaceOffset + offsetInExtraData;
+ pointerToWrite = makeGBPointer(writtenDataOffset);
+ System.arraycopy(writeData, 0, extraDataBlock, offsetInExtraData, lengthToFit);
+ offsetInExtraData += lengthToFit;
+ }
+ } else {
+ // this should never happen, but if not, uh oh
+ throw new RandomizationException("Unable to save moves/evolutions, out of space");
+ }
+ if (pointerToWrite >= 0) {
+ writeWord(pointerTable, (i - 1) * 2, pointerToWrite);
+ if (setNullEntryPointerHere) {
+ nullEntryPointer = pointerToWrite;
+ }
+ }
+ }
+ }
+
+ // Done, write final results to ROM
+ System.arraycopy(pointerTable, 0, rom, movesEvosStart, pointerTable.length);
+ System.arraycopy(mainDataBlock, 0, rom, mainDataBlockOffset, mainDataBlock.length);
+ if (extraSpaceEnabled) {
+ System.arraycopy(extraDataBlock, 0, rom, extraSpaceOffset, extraDataBlock.length);
+ }
+ }
+
+ @Override
+ public boolean isRomValid() {
+ return romEntry.expectedCRC32 == actualCRC32;
+ }
+
+ @Override
+ public BufferedImage getMascotImage() {
+ Pokemon mascot = randomPokemon();
+ int idx = pokeNumToRBYTable[mascot.number];
+ int fsBank;
+ // define (by index number) the bank that a pokemon's image is in
+ // using pokered code
+ if (mascot.number == Species.mew && !romEntry.isYellow) {
+ fsBank = 1;
+ } else if (idx < 0x1F) {
+ fsBank = 0x9;
+ } else if (idx < 0x4A) {
+ fsBank = 0xA;
+ } else if (idx < 0x74 || idx == 0x74 && mascot.frontSpritePointer > 0x7000) {
+ fsBank = 0xB;
+ } else if (idx < 0x99 || idx == 0x99 && mascot.frontSpritePointer > 0x7000) {
+ fsBank = 0xC;
+ } else {
+ fsBank = 0xD;
+ }
+
+ int fsOffset = calculateOffset(fsBank, mascot.frontSpritePointer);
+ Gen1Decmp mscSprite = new Gen1Decmp(rom, fsOffset);
+ mscSprite.decompress();
+ mscSprite.transpose();
+ int w = mscSprite.getWidth();
+ int h = mscSprite.getHeight();
+
+ // Palette?
+ int[] palette;
+ if (romEntry.getValue("MonPaletteIndicesOffset") > 0 && romEntry.getValue("SGBPalettesOffset") > 0) {
+ int palIndex = rom[romEntry.getValue("MonPaletteIndicesOffset") + mascot.number] & 0xFF;
+ int palOffset = romEntry.getValue("SGBPalettesOffset") + palIndex * 8;
+ if (romEntry.isYellow && romEntry.nonJapanese == 1) {
+ // Non-japanese Yellow can use GBC palettes instead.
+ // Stored directly after regular SGB palettes.
+ palOffset += 320;
+ }
+ palette = new int[4];
+ for (int i = 0; i < 4; i++) {
+ palette[i] = GFXFunctions.conv16BitColorToARGB(readWord(palOffset + i * 2));
+ }
+ } else {
+ palette = new int[] { 0xFFFFFFFF, 0xFFAAAAAA, 0xFF666666, 0xFF000000 };
+ }
+
+ byte[] data = mscSprite.getFlattenedData();
+
+ BufferedImage bim = GFXFunctions.drawTiledImage(data, palette, w, h, 8);
+ GFXFunctions.pseudoTransparency(bim, palette[0]);
+
+ return bim;
+ }
+
+}
diff --git a/src/com/pkrandom/romhandlers/Gen2RomHandler.java b/src/com/pkrandom/romhandlers/Gen2RomHandler.java new file mode 100755 index 0000000..2cf4a77 --- /dev/null +++ b/src/com/pkrandom/romhandlers/Gen2RomHandler.java @@ -0,0 +1,2999 @@ +package com.pkrandom.romhandlers;
+
+/*----------------------------------------------------------------------------*/
+/*-- Gen2RomHandler.java - randomizer handler for G/S/C. --*/
+/*-- --*/
+/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/
+/*-- Pokemon and any associated names and the like are --*/
+/*-- trademark and (C) Nintendo 1996-2020. --*/
+/*-- --*/
+/*-- The custom code written here is licensed under the terms of the GPL: --*/
+/*-- --*/
+/*-- This program is free software: you can redistribute it and/or modify --*/
+/*-- it under the terms of the GNU General Public License as published by --*/
+/*-- the Free Software Foundation, either version 3 of the License, or --*/
+/*-- (at your option) any later version. --*/
+/*-- --*/
+/*-- This program is distributed in the hope that it will be useful, --*/
+/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/
+/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/
+/*-- GNU General Public License for more details. --*/
+/*-- --*/
+/*-- You should have received a copy of the GNU General Public License --*/
+/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/
+/*----------------------------------------------------------------------------*/
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.pkrandom.*;
+import com.pkrandom.constants.*;
+import com.pkrandom.exceptions.RandomizerIOException;
+import com.pkrandom.pokemon.*;
+import compressors.Gen2Decmp;
+
+public class Gen2RomHandler extends AbstractGBCRomHandler {
+
+ public static class Factory extends RomHandler.Factory {
+
+ @Override
+ public Gen2RomHandler create(Random random, PrintStream logStream) {
+ return new Gen2RomHandler(random, logStream);
+ }
+
+ public boolean isLoadable(String filename) {
+ long fileLength = new File(filename).length();
+ if (fileLength > 8 * 1024 * 1024) {
+ return false;
+ }
+ byte[] loaded = loadFilePartial(filename, 0x1000);
+ // nope
+ return loaded.length != 0 && detectRomInner(loaded, (int) fileLength);
+ }
+ }
+
+ public Gen2RomHandler(Random random) {
+ super(random, null);
+ }
+
+ public Gen2RomHandler(Random random, PrintStream logStream) {
+ super(random, logStream);
+ }
+
+ private static class RomEntry {
+ private String name;
+ private String romCode;
+ private int version, nonJapanese;
+ private String extraTableFile;
+ private boolean isCrystal;
+ private long expectedCRC32 = -1;
+ private int crcInHeader = -1;
+ private Map<String, String> codeTweaks = new HashMap<>();
+ private List<TMTextEntry> tmTexts = new ArrayList<>();
+ private Map<String, Integer> entries = new HashMap<>();
+ private Map<String, int[]> arrayEntries = new HashMap<>();
+ private Map<String, String> strings = new HashMap<>();
+ private List<StaticPokemon> staticPokemon = new ArrayList<>();
+
+ private int getValue(String key) {
+ if (!entries.containsKey(key)) {
+ entries.put(key, 0);
+ }
+ return entries.get(key);
+ }
+
+ private String getString(String key) {
+ if (!strings.containsKey(key)) {
+ strings.put(key, "");
+ }
+ return strings.get(key);
+ }
+ }
+
+ private static class TMTextEntry {
+ private int number;
+ private int offset;
+ private String template;
+ }
+
+ private static List<RomEntry> roms;
+
+ static {
+ loadROMInfo();
+ }
+
+ private static void loadROMInfo() {
+ roms = new ArrayList<>();
+ RomEntry current = null;
+ try {
+ Scanner sc = new Scanner(FileFunctions.openConfig("gen2_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();
+ r[0] = r[0].trim();
+ if (r[0].equals("StaticPokemon{}")) {
+ current.staticPokemon.add(parseStaticPokemon(r[1], false));
+ } else if (r[0].equals("StaticPokemonGameCorner{}")) {
+ current.staticPokemon.add(parseStaticPokemon(r[1], true));
+ } else if (r[0].equals("TMText[]")) {
+ if (r[1].startsWith("[") && r[1].endsWith("]")) {
+ String[] parts = r[1].substring(1, r[1].length() - 1).split(",", 3);
+ TMTextEntry tte = new TMTextEntry();
+ tte.number = parseRIInt(parts[0]);
+ tte.offset = parseRIInt(parts[1]);
+ tte.template = parts[2];
+ current.tmTexts.add(tte);
+ }
+ } else if (r[0].equals("Game")) {
+ current.romCode = r[1];
+ } else if (r[0].equals("Version")) {
+ current.version = parseRIInt(r[1]);
+ } else if (r[0].equals("NonJapanese")) {
+ current.nonJapanese = parseRIInt(r[1]);
+ } else if (r[0].equals("Type")) {
+ current.isCrystal = r[1].equalsIgnoreCase("Crystal");
+ } else if (r[0].equals("ExtraTableFile")) {
+ current.extraTableFile = r[1];
+ } else if (r[0].equals("CRCInHeader")) {
+ current.crcInHeader = parseRIInt(r[1]);
+ } else if (r[0].equals("CRC32")) {
+ current.expectedCRC32 = parseRILong("0x" + r[1]);
+ } else if (r[0].endsWith("Tweak")) {
+ current.codeTweaks.put(r[0], r[1]);
+ } else if (r[0].equals("CopyFrom")) {
+ for (RomEntry otherEntry : roms) {
+ if (r[1].equalsIgnoreCase(otherEntry.name)) {
+ // copy from here
+ boolean cSP = (current.getValue("CopyStaticPokemon") == 1);
+ boolean cTT = (current.getValue("CopyTMText") == 1);
+ current.arrayEntries.putAll(otherEntry.arrayEntries);
+ current.entries.putAll(otherEntry.entries);
+ current.strings.putAll(otherEntry.strings);
+ if (cSP) {
+ current.staticPokemon.addAll(otherEntry.staticPokemon);
+ current.entries.put("StaticPokemonSupport", 1);
+ } else {
+ current.entries.put("StaticPokemonSupport", 0);
+ current.entries.remove("StaticPokemonOddEggOffset");
+ current.entries.remove("StaticPokemonOddEggDataSize");
+ }
+ if (cTT) {
+ current.tmTexts.addAll(otherEntry.tmTexts);
+ }
+ current.extraTableFile = otherEntry.extraTableFile;
+ }
+ }
+ } else if (r[0].endsWith("Locator") || r[0].endsWith("Prefix")) {
+ current.strings.put(r[0], r[1]);
+ } else {
+ if (r[1].startsWith("[") && r[1].endsWith("]")) {
+ String[] offsets = r[1].substring(1, r[1].length() - 1).split(",");
+ if (offsets.length == 1 && offsets[0].trim().isEmpty()) {
+ current.arrayEntries.put(r[0], new int[0]);
+ } else {
+ int[] offs = new int[offsets.length];
+ int c = 0;
+ for (String off : offsets) {
+ offs[c++] = parseRIInt(off);
+ }
+ current.arrayEntries.put(r[0], offs);
+ }
+ } else {
+ int offs = parseRIInt(r[1]);
+ current.entries.put(r[0], offs);
+ }
+ }
+ }
+ }
+ }
+ sc.close();
+ } catch (FileNotFoundException e) {
+ System.err.println("File not found!");
+ }
+
+ }
+
+ private static StaticPokemon parseStaticPokemon(String staticPokemonString, boolean isGameCorner) {
+ StaticPokemon sp;
+ if (isGameCorner) {
+ sp = new StaticPokemonGameCorner();
+ } else {
+ sp = new StaticPokemon();
+ }
+ String pattern = "[A-z]+=\\[(0x[0-9a-fA-F]+,?\\s?)+]";
+ Pattern r = Pattern.compile(pattern);
+ Matcher m = r.matcher(staticPokemonString);
+ while (m.find()) {
+ String[] segments = m.group().split("=");
+ String[] romOffsets = segments[1].substring(1, segments[1].length() - 1).split(",");
+ int[] offsets = new int [romOffsets.length];
+ for (int i = 0; i < offsets.length; i++) {
+ offsets[i] = parseRIInt(romOffsets[i]);
+ }
+ switch (segments[0]) {
+ case "Species":
+ sp.speciesOffsets = offsets;
+ break;
+ case "Level":
+ sp.levelOffsets = offsets;
+ break;
+ }
+ }
+ return sp;
+ }
+
+ private 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's data
+ private Pokemon[] pokes;
+ private List<Pokemon> pokemonList;
+ private RomEntry romEntry;
+ private Move[] moves;
+ private boolean havePatchedFleeing;
+ private String[] itemNames;
+ private List<Integer> itemOffs;
+ private String[][] mapNames;
+ private String[] landmarkNames;
+ private boolean isVietCrystal;
+ private ItemList allowedItems, nonBadItems;
+ private long actualCRC32;
+ private boolean effectivenessUpdated;
+
+ @Override
+ public boolean detectRom(byte[] rom) {
+ return detectRomInner(rom, rom.length);
+ }
+
+ private static boolean detectRomInner(byte[] rom, int romSize) {
+ // size check
+ return romSize >= GBConstants.minRomSize && romSize <= GBConstants.maxRomSize && checkRomEntry(rom) != null;
+ }
+
+ @Override
+ public void loadedRom() {
+ romEntry = checkRomEntry(this.rom);
+ clearTextTables();
+ readTextTable("gameboy_jpn");
+ if (romEntry.extraTableFile != null && !romEntry.extraTableFile.equalsIgnoreCase("none")) {
+ readTextTable(romEntry.extraTableFile);
+ }
+ // VietCrystal override
+ if (romEntry.name.equals("Crystal (J)")
+ && rom[Gen2Constants.vietCrystalCheckOffset] == Gen2Constants.vietCrystalCheckValue) {
+ readTextTable("vietcrystal");
+ isVietCrystal = true;
+ } else {
+ isVietCrystal = false;
+ }
+ havePatchedFleeing = false;
+ loadPokemonStats();
+ pokemonList = Arrays.asList(pokes);
+ loadMoves();
+ loadLandmarkNames();
+ preprocessMaps();
+ loadItemNames();
+ allowedItems = Gen2Constants.allowedItems.copy();
+ nonBadItems = Gen2Constants.nonBadItems.copy();
+ actualCRC32 = FileFunctions.getCRC32(rom);
+ // VietCrystal: exclude Burn Heal, Calcium, TwistedSpoon, and Elixir
+ // crashes your game if used, glitches out your inventory if carried
+ if (isVietCrystal) {
+ allowedItems.banSingles(Gen2Items.burnHeal, Gen2Items.calcium, Gen2Items.elixer, Gen2Items.twistedSpoon);
+ }
+ }
+
+ private static RomEntry checkRomEntry(byte[] rom) {
+ int version = rom[GBConstants.versionOffset] & 0xFF;
+ int nonjap = rom[GBConstants.jpFlagOffset] & 0xFF;
+ // Check for specific CRC first
+ int crcInHeader = ((rom[GBConstants.crcOffset] & 0xFF) << 8) | (rom[GBConstants.crcOffset + 1] & 0xFF);
+ for (RomEntry re : roms) {
+ if (romCode(rom, re.romCode) && re.version == version && re.nonJapanese == nonjap
+ && re.crcInHeader == crcInHeader) {
+ return re;
+ }
+ }
+ // Now check for non-specific-CRC entries
+ for (RomEntry re : roms) {
+ if (romCode(rom, re.romCode) && re.version == version && re.nonJapanese == nonjap && re.crcInHeader == -1) {
+ return re;
+ }
+ }
+ // Not found
+ return null;
+ }
+
+ @Override
+ public void savingRom() {
+ savePokemonStats();
+ saveMoves();
+ }
+
+ private void loadPokemonStats() {
+ pokes = new Pokemon[Gen2Constants.pokemonCount + 1];
+ // Fetch our names
+ String[] pokeNames = readPokemonNames();
+ int offs = romEntry.getValue("PokemonStatsOffset");
+ // Get base stats
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ pokes[i] = new Pokemon();
+ pokes[i].number = i;
+ loadBasicPokeStats(pokes[i], offs + (i - 1) * Gen2Constants.baseStatsEntrySize);
+ // Name?
+ pokes[i].name = pokeNames[i];
+ }
+
+ // Get evolutions
+ populateEvolutions();
+
+ }
+
+ private void savePokemonStats() {
+ // Write pokemon names
+ int offs = romEntry.getValue("PokemonNamesOffset");
+ int len = romEntry.getValue("PokemonNamesLength");
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ int stringOffset = offs + (i - 1) * len;
+ writeFixedLengthString(pokes[i].name, stringOffset, len);
+ }
+ // Write pokemon stats
+ int offs2 = romEntry.getValue("PokemonStatsOffset");
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ saveBasicPokeStats(pokes[i], offs2 + (i - 1) * Gen2Constants.baseStatsEntrySize);
+ }
+ // Write evolutions
+ writeEvosAndMovesLearnt(true, null);
+ }
+
+ private String[] readMoveNames() {
+ int offset = romEntry.getValue("MoveNamesOffset");
+ String[] moveNames = new String[Gen2Constants.moveCount + 1];
+ for (int i = 1; i <= Gen2Constants.moveCount; i++) {
+ moveNames[i] = readVariableLengthString(offset, false);
+ offset += lengthOfStringAt(offset, false) + 1;
+ }
+ return moveNames;
+ }
+
+ private void loadMoves() {
+ moves = new Move[Gen2Constants.moveCount + 1];
+ String[] moveNames = readMoveNames();
+ int offs = romEntry.getValue("MoveDataOffset");
+ for (int i = 1; i <= Gen2Constants.moveCount; i++) {
+ moves[i] = new Move();
+ moves[i].name = moveNames[i];
+ moves[i].number = i;
+ moves[i].internalId = i;
+ moves[i].effectIndex = rom[offs + (i - 1) * 7 + 1] & 0xFF;
+ moves[i].hitratio = ((rom[offs + (i - 1) * 7 + 4] & 0xFF)) / 255.0 * 100;
+ moves[i].power = rom[offs + (i - 1) * 7 + 2] & 0xFF;
+ moves[i].pp = rom[offs + (i - 1) * 7 + 5] & 0xFF;
+ moves[i].type = Gen2Constants.typeTable[rom[offs + (i - 1) * 7 + 3]];
+ moves[i].category = GBConstants.physicalTypes.contains(moves[i].type) ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL;
+ if (moves[i].power == 0 && !GlobalConstants.noPowerNonStatusMoves.contains(i)) {
+ moves[i].category = MoveCategory.STATUS;
+ }
+
+ if (i == Moves.swift) {
+ perfectAccuracy = (int)moves[i].hitratio;
+ }
+
+ if (GlobalConstants.normalMultihitMoves.contains(i)) {
+ moves[i].hitCount = 3;
+ } else if (GlobalConstants.doubleHitMoves.contains(i)) {
+ moves[i].hitCount = 2;
+ } else if (i == Moves.tripleKick) {
+ moves[i].hitCount = 2.71; // this assumes the first hit lands
+ }
+
+ // Values taken from effect_priorities.asm from the Gen 2 disassemblies.
+ if (moves[i].effectIndex == Gen2Constants.priorityHitEffectIndex) {
+ moves[i].priority = 2;
+ } else if (moves[i].effectIndex == Gen2Constants.protectEffectIndex ||
+ moves[i].effectIndex == Gen2Constants.endureEffectIndex) {
+ moves[i].priority = 3;
+ } else if (moves[i].effectIndex == Gen2Constants.forceSwitchEffectIndex ||
+ moves[i].effectIndex == Gen2Constants.counterEffectIndex ||
+ moves[i].effectIndex == Gen2Constants.mirrorCoatEffectIndex) {
+ moves[i].priority = 0;
+ } else {
+ moves[i].priority = 1;
+ }
+
+ double secondaryEffectChance = ((rom[offs + (i - 1) * 7 + 6] & 0xFF)) / 255.0 * 100;
+ loadStatChangesFromEffect(moves[i], secondaryEffectChance);
+ loadStatusFromEffect(moves[i], secondaryEffectChance);
+ loadMiscMoveInfoFromEffect(moves[i], secondaryEffectChance);
+ }
+ }
+
+ private void loadStatChangesFromEffect(Move move, double secondaryEffectChance) {
+ switch (move.effectIndex) {
+ case Gen2Constants.noDamageAtkPlusOneEffect:
+ case Gen2Constants.damageUserAtkPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen2Constants.noDamageDefPlusOneEffect:
+ case Gen2Constants.damageUserDefPlusOneEffect:
+ case Gen2Constants.defenseCurlEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen2Constants.noDamageSpAtkPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen2Constants.noDamageEvasionPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.EVASION;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen2Constants.noDamageAtkMinusOneEffect:
+ case Gen2Constants.damageAtkMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen2Constants.noDamageDefMinusOneEffect:
+ case Gen2Constants.damageDefMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen2Constants.noDamageSpeMinusOneEffect:
+ case Gen2Constants.damageSpeMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPEED;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen2Constants.noDamageAccuracyMinusOneEffect:
+ case Gen2Constants.damageAccuracyMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.ACCURACY;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen2Constants.noDamageEvasionMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.EVASION;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen2Constants.noDamageAtkPlusTwoEffect:
+ case Gen2Constants.swaggerEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen2Constants.noDamageDefPlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen2Constants.noDamageSpePlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPEED;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen2Constants.noDamageSpDefPlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen2Constants.noDamageAtkMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen2Constants.noDamageDefMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen2Constants.noDamageSpeMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPEED;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen2Constants.noDamageSpDefMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen2Constants.damageSpDefMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen2Constants.damageUserAllPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.ALL;
+ move.statChanges[0].stages = 1;
+ break;
+ default:
+ // Move does not have a stat-changing effect
+ return;
+ }
+
+ switch (move.effectIndex) {
+ case Gen2Constants.noDamageAtkPlusOneEffect:
+ case Gen2Constants.noDamageDefPlusOneEffect:
+ case Gen2Constants.noDamageSpAtkPlusOneEffect:
+ case Gen2Constants.noDamageEvasionPlusOneEffect:
+ case Gen2Constants.noDamageAtkMinusOneEffect:
+ case Gen2Constants.noDamageDefMinusOneEffect:
+ case Gen2Constants.noDamageSpeMinusOneEffect:
+ case Gen2Constants.noDamageAccuracyMinusOneEffect:
+ case Gen2Constants.noDamageEvasionMinusOneEffect:
+ case Gen2Constants.noDamageAtkPlusTwoEffect:
+ case Gen2Constants.noDamageDefPlusTwoEffect:
+ case Gen2Constants.noDamageSpePlusTwoEffect:
+ case Gen2Constants.noDamageSpDefPlusTwoEffect:
+ case Gen2Constants.noDamageAtkMinusTwoEffect:
+ case Gen2Constants.noDamageDefMinusTwoEffect:
+ case Gen2Constants.noDamageSpeMinusTwoEffect:
+ case Gen2Constants.noDamageSpDefMinusTwoEffect:
+ case Gen2Constants.swaggerEffect:
+ case Gen2Constants.defenseCurlEffect:
+ if (move.statChanges[0].stages < 0 || move.effectIndex == Gen2Constants.swaggerEffect) {
+ move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET;
+ } else {
+ move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER;
+ }
+ break;
+
+ case Gen2Constants.damageAtkMinusOneEffect:
+ case Gen2Constants.damageDefMinusOneEffect:
+ case Gen2Constants.damageSpeMinusOneEffect:
+ case Gen2Constants.damageSpDefMinusOneEffect:
+ case Gen2Constants.damageAccuracyMinusOneEffect:
+ move.statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET;
+ break;
+
+ case Gen2Constants.damageUserDefPlusOneEffect:
+ case Gen2Constants.damageUserAtkPlusOneEffect:
+ case Gen2Constants.damageUserAllPlusOneEffect:
+ move.statChangeMoveType = StatChangeMoveType.DAMAGE_USER;
+ break;
+ }
+
+ if (move.statChangeMoveType == StatChangeMoveType.DAMAGE_TARGET || move.statChangeMoveType == StatChangeMoveType.DAMAGE_USER) {
+ for (int i = 0; i < move.statChanges.length; i++) {
+ if (move.statChanges[i].type != StatChangeType.NONE) {
+ move.statChanges[i].percentChance = secondaryEffectChance;
+ if (move.statChanges[i].percentChance == 0.0) {
+ move.statChanges[i].percentChance = 100.0;
+ }
+ }
+ }
+ }
+ }
+
+ private void loadStatusFromEffect(Move move, double secondaryEffectChance) {
+ switch (move.effectIndex) {
+ case Gen2Constants.noDamageSleepEffect:
+ case Gen2Constants.toxicEffect:
+ case Gen2Constants.noDamageConfusionEffect:
+ case Gen2Constants.noDamagePoisonEffect:
+ case Gen2Constants.noDamageParalyzeEffect:
+ case Gen2Constants.swaggerEffect:
+ move.statusMoveType = StatusMoveType.NO_DAMAGE;
+ break;
+
+ case Gen2Constants.damagePoisonEffect:
+ case Gen2Constants.damageBurnEffect:
+ case Gen2Constants.damageFreezeEffect:
+ case Gen2Constants.damageParalyzeEffect:
+ case Gen2Constants.damageConfusionEffect:
+ case Gen2Constants.twineedleEffect:
+ case Gen2Constants.damageBurnAndThawUserEffect:
+ case Gen2Constants.thunderEffect:
+ move.statusMoveType = StatusMoveType.DAMAGE;
+ break;
+
+ default:
+ // Move does not have a status effect
+ return;
+ }
+
+ switch (move.effectIndex) {
+ case Gen2Constants.noDamageSleepEffect:
+ move.statusType = StatusType.SLEEP;
+ break;
+ case Gen2Constants.damagePoisonEffect:
+ case Gen2Constants.noDamagePoisonEffect:
+ case Gen2Constants.twineedleEffect:
+ move.statusType = StatusType.POISON;
+ break;
+ case Gen2Constants.damageBurnEffect:
+ case Gen2Constants.damageBurnAndThawUserEffect:
+ move.statusType = StatusType.BURN;
+ break;
+ case Gen2Constants.damageFreezeEffect:
+ move.statusType = StatusType.FREEZE;
+ break;
+ case Gen2Constants.damageParalyzeEffect:
+ case Gen2Constants.noDamageParalyzeEffect:
+ case Gen2Constants.thunderEffect:
+ move.statusType = StatusType.PARALYZE;
+ break;
+ case Gen2Constants.toxicEffect:
+ move.statusType = StatusType.TOXIC_POISON;
+ break;
+ case Gen2Constants.noDamageConfusionEffect:
+ case Gen2Constants.damageConfusionEffect:
+ case Gen2Constants.swaggerEffect:
+ move.statusType = StatusType.CONFUSION;
+ break;
+ }
+
+ if (move.statusMoveType == StatusMoveType.DAMAGE) {
+ move.statusPercentChance = secondaryEffectChance;
+ if (move.statusPercentChance == 0.0) {
+ move.statusPercentChance = 100.0;
+ }
+ }
+ }
+
+ private void loadMiscMoveInfoFromEffect(Move move, double secondaryEffectChance) {
+ switch (move.effectIndex) {
+ case Gen2Constants.flinchEffect:
+ case Gen2Constants.snoreEffect:
+ case Gen2Constants.twisterEffect:
+ case Gen2Constants.stompEffect:
+ move.flinchPercentChance = secondaryEffectChance;
+ break;
+
+ case Gen2Constants.damageAbsorbEffect:
+ case Gen2Constants.dreamEaterEffect:
+ move.absorbPercent = 50;
+ break;
+
+ case Gen2Constants.damageRecoilEffect:
+ move.recoilPercent = 25;
+ break;
+
+ case Gen2Constants.flailAndReversalEffect:
+ case Gen2Constants.futureSightEffect:
+ move.criticalChance = CriticalChance.NONE;
+ break;
+
+ case Gen2Constants.bindingEffect:
+ case Gen2Constants.trappingEffect:
+ move.isTrapMove = true;
+ break;
+
+ case Gen2Constants.razorWindEffect:
+ case Gen2Constants.skyAttackEffect:
+ case Gen2Constants.skullBashEffect:
+ case Gen2Constants.solarbeamEffect:
+ case Gen2Constants.semiInvulnerableEffect:
+ move.isChargeMove = true;
+ break;
+
+ case Gen2Constants.hyperBeamEffect:
+ move.isRechargeMove = true;
+ break;
+ }
+
+ if (Gen2Constants.increasedCritMoves.contains(move.number)) {
+ move.criticalChance = CriticalChance.INCREASED;
+ }
+ }
+
+ private void saveMoves() {
+ int offs = romEntry.getValue("MoveDataOffset");
+ for (int i = 1; i <= 251; i++) {
+ rom[offs + (i - 1) * 7 + 1] = (byte) moves[i].effectIndex;
+ rom[offs + (i - 1) * 7 + 2] = (byte) moves[i].power;
+ rom[offs + (i - 1) * 7 + 3] = Gen2Constants.typeToByte(moves[i].type);
+ int hitratio = (int) Math.round(moves[i].hitratio * 2.55);
+ if (hitratio < 0) {
+ hitratio = 0;
+ }
+ if (hitratio > 255) {
+ hitratio = 255;
+ }
+ rom[offs + (i - 1) * 7 + 4] = (byte) hitratio;
+ rom[offs + (i - 1) * 7 + 5] = (byte) moves[i].pp;
+ }
+ }
+
+ public List<Move> getMoves() {
+ return Arrays.asList(moves);
+ }
+
+ private void loadBasicPokeStats(Pokemon pkmn, int offset) {
+ pkmn.hp = rom[offset + Gen2Constants.bsHPOffset] & 0xFF;
+ pkmn.attack = rom[offset + Gen2Constants.bsAttackOffset] & 0xFF;
+ pkmn.defense = rom[offset + Gen2Constants.bsDefenseOffset] & 0xFF;
+ pkmn.speed = rom[offset + Gen2Constants.bsSpeedOffset] & 0xFF;
+ pkmn.spatk = rom[offset + Gen2Constants.bsSpAtkOffset] & 0xFF;
+ pkmn.spdef = rom[offset + Gen2Constants.bsSpDefOffset] & 0xFF;
+ // Type
+ pkmn.primaryType = Gen2Constants.typeTable[rom[offset + Gen2Constants.bsPrimaryTypeOffset] & 0xFF];
+ pkmn.secondaryType = Gen2Constants.typeTable[rom[offset + Gen2Constants.bsSecondaryTypeOffset] & 0xFF];
+ // Only one type?
+ if (pkmn.secondaryType == pkmn.primaryType) {
+ pkmn.secondaryType = null;
+ }
+ pkmn.catchRate = rom[offset + Gen2Constants.bsCatchRateOffset] & 0xFF;
+ pkmn.guaranteedHeldItem = -1;
+ pkmn.commonHeldItem = rom[offset + Gen2Constants.bsCommonHeldItemOffset] & 0xFF;
+ pkmn.rareHeldItem = rom[offset + Gen2Constants.bsRareHeldItemOffset] & 0xFF;
+ pkmn.darkGrassHeldItem = -1;
+ pkmn.growthCurve = ExpCurve.fromByte(rom[offset + Gen2Constants.bsGrowthCurveOffset]);
+ pkmn.picDimensions = rom[offset + Gen2Constants.bsPicDimensionsOffset] & 0xFF;
+
+ }
+
+ private void saveBasicPokeStats(Pokemon pkmn, int offset) {
+ rom[offset + Gen2Constants.bsHPOffset] = (byte) pkmn.hp;
+ rom[offset + Gen2Constants.bsAttackOffset] = (byte) pkmn.attack;
+ rom[offset + Gen2Constants.bsDefenseOffset] = (byte) pkmn.defense;
+ rom[offset + Gen2Constants.bsSpeedOffset] = (byte) pkmn.speed;
+ rom[offset + Gen2Constants.bsSpAtkOffset] = (byte) pkmn.spatk;
+ rom[offset + Gen2Constants.bsSpDefOffset] = (byte) pkmn.spdef;
+ rom[offset + Gen2Constants.bsPrimaryTypeOffset] = Gen2Constants.typeToByte(pkmn.primaryType);
+ if (pkmn.secondaryType == null) {
+ rom[offset + Gen2Constants.bsSecondaryTypeOffset] = rom[offset + Gen2Constants.bsPrimaryTypeOffset];
+ } else {
+ rom[offset + Gen2Constants.bsSecondaryTypeOffset] = Gen2Constants.typeToByte(pkmn.secondaryType);
+ }
+ rom[offset + Gen2Constants.bsCatchRateOffset] = (byte) pkmn.catchRate;
+
+ rom[offset + Gen2Constants.bsCommonHeldItemOffset] = (byte) pkmn.commonHeldItem;
+ rom[offset + Gen2Constants.bsRareHeldItemOffset] = (byte) pkmn.rareHeldItem;
+ rom[offset + Gen2Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte();
+ }
+
+ private String[] readPokemonNames() {
+ int offs = romEntry.getValue("PokemonNamesOffset");
+ int len = romEntry.getValue("PokemonNamesLength");
+ String[] names = new String[Gen2Constants.pokemonCount + 1];
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ names[i] = readFixedLengthString(offs + (i - 1) * len, len);
+ }
+ return names;
+ }
+
+ @Override
+ public List<Pokemon> getStarters() {
+ // Get the starters
+ List<Pokemon> starters = new ArrayList<>();
+ starters.add(pokes[rom[romEntry.arrayEntries.get("StarterOffsets1")[0]] & 0xFF]);
+ starters.add(pokes[rom[romEntry.arrayEntries.get("StarterOffsets2")[0]] & 0xFF]);
+ starters.add(pokes[rom[romEntry.arrayEntries.get("StarterOffsets3")[0]] & 0xFF]);
+ return starters;
+ }
+
+ @Override
+ public boolean setStarters(List<Pokemon> newStarters) {
+ if (newStarters.size() != 3) {
+ return false;
+ }
+
+ // Actually write
+
+ for (int i = 0; i < 3; i++) {
+ byte starter = (byte) newStarters.get(i).number;
+ int[] offsets = romEntry.arrayEntries.get("StarterOffsets" + (i + 1));
+ for (int offset : offsets) {
+ rom[offset] = starter;
+ }
+ }
+
+ // Attempt to replace text
+ if (romEntry.getValue("CanChangeStarterText") > 0) {
+ int[] starterTextOffsets = romEntry.arrayEntries.get("StarterTextOffsets");
+ for (int i = 0; i < 3 && i < starterTextOffsets.length; i++) {
+ writeVariableLengthString(String.format("%s?\\e", newStarters.get(i).name), starterTextOffsets[i], true);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean hasStarterAltFormes() {
+ return false;
+ }
+
+ @Override
+ public int starterCount() {
+ return 3;
+ }
+
+ @Override
+ public Map<Integer, StatChange> getUpdatedPokemonStats(int generation) {
+ return GlobalConstants.getStatChanges(generation);
+ }
+
+ @Override
+ public boolean supportsStarterHeldItems() {
+ return true;
+ }
+
+ @Override
+ public List<Integer> getStarterHeldItems() {
+ List<Integer> sHeldItems = new ArrayList<>();
+ int[] shiOffsets = romEntry.arrayEntries.get("StarterHeldItems");
+ for (int offset : shiOffsets) {
+ sHeldItems.add(rom[offset] & 0xFF);
+ }
+ return sHeldItems;
+ }
+
+ @Override
+ public void setStarterHeldItems(List<Integer> items) {
+ int[] shiOffsets = romEntry.arrayEntries.get("StarterHeldItems");
+ if (items.size() != shiOffsets.length) {
+ return;
+ }
+ Iterator<Integer> sHeldItems = items.iterator();
+ for (int offset : shiOffsets) {
+ rom[offset] = sHeldItems.next().byteValue();
+ }
+ }
+
+ @Override
+ public List<EncounterSet> getEncounters(boolean useTimeOfDay) {
+ int offset = romEntry.getValue("WildPokemonOffset");
+ List<EncounterSet> areas = new ArrayList<>();
+ offset = readLandEncounters(offset, areas, useTimeOfDay); // Johto
+ offset = readSeaEncounters(offset, areas); // Johto
+ offset = readLandEncounters(offset, areas, useTimeOfDay); // Kanto
+ offset = readSeaEncounters(offset, areas); // Kanto
+ offset = readLandEncounters(offset, areas, useTimeOfDay); // Specials
+ offset = readSeaEncounters(offset, areas); // Specials
+
+ // Fishing Data
+ offset = romEntry.getValue("FishingWildsOffset");
+ int rootOffset = offset;
+ for (int k = 0; k < Gen2Constants.fishingGroupCount; k++) {
+ EncounterSet es = new EncounterSet();
+ es.displayName = "Fishing Group " + (k + 1);
+ for (int i = 0; i < Gen2Constants.pokesPerFishingGroup; i++) {
+ offset++;
+ int pokeNum = rom[offset++] & 0xFF;
+ int level = rom[offset++] & 0xFF;
+ if (pokeNum == 0) {
+ if (!useTimeOfDay) {
+ // read the encounter they put here for DAY
+ int specialOffset = rootOffset + Gen2Constants.fishingGroupEntryLength
+ * Gen2Constants.pokesPerFishingGroup * Gen2Constants.fishingGroupCount + level * 4 + 2;
+ Encounter enc = new Encounter();
+ enc.pokemon = pokes[rom[specialOffset] & 0xFF];
+ enc.level = rom[specialOffset + 1] & 0xFF;
+ es.encounters.add(enc);
+ }
+ // else will be handled by code below
+ } else {
+ Encounter enc = new Encounter();
+ enc.pokemon = pokes[pokeNum];
+ enc.level = level;
+ es.encounters.add(enc);
+ }
+ }
+ areas.add(es);
+ }
+ if (useTimeOfDay) {
+ for (int k = 0; k < Gen2Constants.timeSpecificFishingGroupCount; k++) {
+ EncounterSet es = new EncounterSet();
+ es.displayName = "Time-Specific Fishing " + (k + 1);
+ for (int i = 0; i < Gen2Constants.pokesPerTSFishingGroup; i++) {
+ int pokeNum = rom[offset++] & 0xFF;
+ int level = rom[offset++] & 0xFF;
+ Encounter enc = new Encounter();
+ enc.pokemon = pokes[pokeNum];
+ enc.level = level;
+ es.encounters.add(enc);
+ }
+ areas.add(es);
+ }
+ }
+
+ // Headbutt Data
+ offset = romEntry.getValue("HeadbuttWildsOffset");
+ int limit = romEntry.getValue("HeadbuttTableSize");
+ for (int i = 0; i < limit; i++) {
+ EncounterSet es = new EncounterSet();
+ es.displayName = "Headbutt Trees Set " + (i + 1);
+ while ((rom[offset] & 0xFF) != 0xFF) {
+ offset++;
+ int pokeNum = rom[offset++] & 0xFF;
+ int level = rom[offset++] & 0xFF;
+ Encounter enc = new Encounter();
+ enc.pokemon = pokes[pokeNum];
+ enc.level = level;
+ es.encounters.add(enc);
+ }
+ offset++;
+ areas.add(es);
+ }
+
+ // Bug Catching Contest Data
+ offset = romEntry.getValue("BCCWildsOffset");
+ EncounterSet bccES = new EncounterSet();
+ bccES.displayName = "Bug Catching Contest";
+ while ((rom[offset] & 0xFF) != 0xFF) {
+ offset++;
+ Encounter enc = new Encounter();
+ enc.pokemon = pokes[rom[offset++] & 0xFF];
+ enc.level = rom[offset++] & 0xFF;
+ enc.maxLevel = rom[offset++] & 0xFF;
+ bccES.encounters.add(enc);
+ }
+ // Unown is banned for Bug Catching Contest (5/8/2016)
+ bccES.bannedPokemon.add(pokes[Species.unown]);
+ areas.add(bccES);
+
+ return areas;
+ }
+
+ private int readLandEncounters(int offset, List<EncounterSet> areas, boolean useTimeOfDay) {
+ String[] todNames = new String[] { "Morning", "Day", "Night" };
+ while ((rom[offset] & 0xFF) != 0xFF) {
+ int mapBank = rom[offset] & 0xFF;
+ int mapNumber = rom[offset + 1] & 0xFF;
+ String mapName = mapNames[mapBank][mapNumber];
+ if (useTimeOfDay) {
+ for (int i = 0; i < 3; i++) {
+ EncounterSet encset = new EncounterSet();
+ encset.rate = rom[offset + 2 + i] & 0xFF;
+ encset.displayName = mapName + " Grass/Cave (" + todNames[i] + ")";
+ for (int j = 0; j < Gen2Constants.landEncounterSlots; j++) {
+ Encounter enc = new Encounter();
+ enc.level = rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2)] & 0xFF;
+ enc.maxLevel = 0;
+ enc.pokemon = pokes[rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2) + 1] & 0xFF];
+ encset.encounters.add(enc);
+ }
+ areas.add(encset);
+ }
+ } else {
+ // Use Day only
+ EncounterSet encset = new EncounterSet();
+ encset.rate = rom[offset + 3] & 0xFF;
+ encset.displayName = mapName + " Grass/Cave";
+ for (int j = 0; j < Gen2Constants.landEncounterSlots; j++) {
+ Encounter enc = new Encounter();
+ enc.level = rom[offset + 5 + Gen2Constants.landEncounterSlots * 2 + (j * 2)] & 0xFF;
+ enc.maxLevel = 0;
+ enc.pokemon = pokes[rom[offset + 5 + Gen2Constants.landEncounterSlots * 2 + (j * 2) + 1] & 0xFF];
+ encset.encounters.add(enc);
+ }
+ areas.add(encset);
+ }
+ offset += 5 + 6 * Gen2Constants.landEncounterSlots;
+ }
+ return offset + 1;
+ }
+
+ private int readSeaEncounters(int offset, List<EncounterSet> areas) {
+ while ((rom[offset] & 0xFF) != 0xFF) {
+ int mapBank = rom[offset] & 0xFF;
+ int mapNumber = rom[offset + 1] & 0xFF;
+ String mapName = mapNames[mapBank][mapNumber];
+ EncounterSet encset = new EncounterSet();
+ encset.rate = rom[offset + 2] & 0xFF;
+ encset.displayName = mapName + " Surfing";
+ for (int j = 0; j < Gen2Constants.seaEncounterSlots; j++) {
+ Encounter enc = new Encounter();
+ enc.level = rom[offset + 3 + (j * 2)] & 0xFF;
+ enc.maxLevel = 0;
+ enc.pokemon = pokes[rom[offset + 3 + (j * 2) + 1] & 0xFF];
+ encset.encounters.add(enc);
+ }
+ areas.add(encset);
+ offset += 3 + Gen2Constants.seaEncounterSlots * 2;
+ }
+ return offset + 1;
+ }
+
+ @Override
+ public void setEncounters(boolean useTimeOfDay, List<EncounterSet> encounters) {
+ if (!havePatchedFleeing) {
+ patchFleeing();
+ }
+ int offset = romEntry.getValue("WildPokemonOffset");
+ Iterator<EncounterSet> areas = encounters.iterator();
+ offset = writeLandEncounters(offset, areas, useTimeOfDay); // Johto
+ offset = writeSeaEncounters(offset, areas); // Johto
+ offset = writeLandEncounters(offset, areas, useTimeOfDay); // Kanto
+ offset = writeSeaEncounters(offset, areas); // Kanto
+ offset = writeLandEncounters(offset, areas, useTimeOfDay); // Specials
+ offset = writeSeaEncounters(offset, areas); // Specials
+
+ // Fishing Data
+ offset = romEntry.getValue("FishingWildsOffset");
+ for (int k = 0; k < Gen2Constants.fishingGroupCount; k++) {
+ EncounterSet es = areas.next();
+ Iterator<Encounter> encs = es.encounters.iterator();
+ for (int i = 0; i < Gen2Constants.pokesPerFishingGroup; i++) {
+ offset++;
+ if (rom[offset] == 0) {
+ if (!useTimeOfDay) {
+ // overwrite with a static encounter
+ Encounter enc = encs.next();
+ rom[offset++] = (byte) enc.pokemon.number;
+ rom[offset++] = (byte) enc.level;
+ } else {
+ // else handle below
+ offset += 2;
+ }
+ } else {
+ Encounter enc = encs.next();
+ rom[offset++] = (byte) enc.pokemon.number;
+ rom[offset++] = (byte) enc.level;
+ }
+ }
+ }
+ if (useTimeOfDay) {
+ for (int k = 0; k < Gen2Constants.timeSpecificFishingGroupCount; k++) {
+ EncounterSet es = areas.next();
+ Iterator<Encounter> encs = es.encounters.iterator();
+ for (int i = 0; i < Gen2Constants.pokesPerTSFishingGroup; i++) {
+ Encounter enc = encs.next();
+ rom[offset++] = (byte) enc.pokemon.number;
+ rom[offset++] = (byte) enc.level;
+ }
+ }
+ }
+
+ // Headbutt Data
+ offset = romEntry.getValue("HeadbuttWildsOffset");
+ int limit = romEntry.getValue("HeadbuttTableSize");
+ for (int i = 0; i < limit; i++) {
+ EncounterSet es = areas.next();
+ Iterator<Encounter> encs = es.encounters.iterator();
+ while ((rom[offset] & 0xFF) != 0xFF) {
+ Encounter enc = encs.next();
+ offset++;
+ rom[offset++] = (byte) enc.pokemon.number;
+ rom[offset++] = (byte) enc.level;
+ }
+ offset++;
+ }
+
+ // Bug Catching Contest Data
+ offset = romEntry.getValue("BCCWildsOffset");
+ EncounterSet bccES = areas.next();
+ Iterator<Encounter> bccEncs = bccES.encounters.iterator();
+ while ((rom[offset] & 0xFF) != 0xFF) {
+ offset++;
+ Encounter enc = bccEncs.next();
+ rom[offset++] = (byte) enc.pokemon.number;
+ rom[offset++] = (byte) enc.level;
+ rom[offset++] = (byte) enc.maxLevel;
+ }
+
+ }
+
+ private int writeLandEncounters(int offset, Iterator<EncounterSet> areas, boolean useTimeOfDay) {
+ while ((rom[offset] & 0xFF) != 0xFF) {
+ if (useTimeOfDay) {
+ for (int i = 0; i < 3; i++) {
+ EncounterSet encset = areas.next();
+ Iterator<Encounter> encountersHere = encset.encounters.iterator();
+ for (int j = 0; j < Gen2Constants.landEncounterSlots; j++) {
+ Encounter enc = encountersHere.next();
+ rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2)] = (byte) enc.level;
+ rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2) + 1] = (byte) enc.pokemon.number;
+ }
+ }
+ } else {
+ // Write the set to all 3 equally
+ EncounterSet encset = areas.next();
+ for (int i = 0; i < 3; i++) {
+ Iterator<Encounter> encountersHere = encset.encounters.iterator();
+ for (int j = 0; j < Gen2Constants.landEncounterSlots; j++) {
+ Encounter enc = encountersHere.next();
+ rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2)] = (byte) enc.level;
+ rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2) + 1] = (byte) enc.pokemon.number;
+ }
+ }
+ }
+ offset += 5 + 6 * Gen2Constants.landEncounterSlots;
+ }
+ return offset + 1;
+ }
+
+ private int writeSeaEncounters(int offset, Iterator<EncounterSet> areas) {
+ while ((rom[offset] & 0xFF) != 0xFF) {
+ EncounterSet encset = areas.next();
+ Iterator<Encounter> encountersHere = encset.encounters.iterator();
+ for (int j = 0; j < Gen2Constants.seaEncounterSlots; j++) {
+ Encounter enc = encountersHere.next();
+ rom[offset + 3 + (j * 2)] = (byte) enc.level;
+ rom[offset + 3 + (j * 2) + 1] = (byte) enc.pokemon.number;
+ }
+ offset += 3 + Gen2Constants.seaEncounterSlots * 2;
+ }
+ return offset + 1;
+ }
+
+ @Override
+ public List<Trainer> getTrainers() {
+ int traineroffset = romEntry.getValue("TrainerDataTableOffset");
+ int traineramount = romEntry.getValue("TrainerClassAmount");
+ int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts");
+
+ int[] pointers = new int[traineramount];
+ for (int i = 0; i < traineramount; i++) {
+ int pointer = readWord(traineroffset + i * 2);
+ pointers[i] = calculateOffset(bankOf(traineroffset), pointer);
+ }
+
+ List<String> tcnames = this.getTrainerClassNames();
+
+ List<Trainer> allTrainers = new ArrayList<>();
+ int index = 0;
+ for (int i = 0; i < traineramount; i++) {
+ int offs = pointers[i];
+ int limit = trainerclasslimits[i];
+ for (int trnum = 0; trnum < limit; trnum++) {
+ index++;
+ Trainer tr = new Trainer();
+ tr.offset = offs;
+ tr.index = index;
+ tr.trainerclass = i;
+ String name = readVariableLengthString(offs, false);
+ tr.name = name;
+ tr.fullDisplayName = tcnames.get(i) + " " + name;
+ offs += lengthOfStringAt(offs, false) + 1;
+ int dataType = rom[offs] & 0xFF;
+ tr.poketype = dataType;
+ offs++;
+ while ((rom[offs] & 0xFF) != 0xFF) {
+ TrainerPokemon tp = new TrainerPokemon();
+ tp.level = rom[offs] & 0xFF;
+ tp.pokemon = pokes[rom[offs + 1] & 0xFF];
+ offs += 2;
+ if ((dataType & 2) == 2) {
+ tp.heldItem = rom[offs] & 0xFF;
+ offs++;
+ }
+ if ((dataType & 1) == 1) {
+ for (int move = 0; move < 4; move++) {
+ tp.moves[move] = rom[offs + move] & 0xFF;
+ }
+ offs += 4;
+ }
+ tr.pokemon.add(tp);
+ }
+ allTrainers.add(tr);
+ offs++;
+ }
+ }
+
+ Gen2Constants.universalTrainerTags(allTrainers);
+ if (romEntry.isCrystal) {
+ Gen2Constants.crystalTags(allTrainers);
+ } else {
+ Gen2Constants.goldSilverTags(allTrainers);
+ }
+
+ return allTrainers;
+ }
+
+ @Override
+ public List<Integer> getMainPlaythroughTrainers() {
+ return new ArrayList<>(); // Not implemented
+ }
+
+ @Override
+ public List<Integer> getEliteFourTrainers(boolean isChallengeMode) {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode) {
+ int traineroffset = romEntry.getValue("TrainerDataTableOffset");
+ int traineramount = romEntry.getValue("TrainerClassAmount");
+ int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts");
+
+ int[] pointers = new int[traineramount];
+ for (int i = 0; i < traineramount; i++) {
+ int pointer = readWord(traineroffset + i * 2);
+ pointers[i] = calculateOffset(bankOf(traineroffset), pointer);
+ }
+
+ // Get current movesets in case we need to reset them for certain
+ // trainer mons.
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+
+ Iterator<Trainer> allTrainers = trainerData.iterator();
+ for (int i = 0; i < traineramount; i++) {
+ int offs = pointers[i];
+ int limit = trainerclasslimits[i];
+ for (int trnum = 0; trnum < limit; trnum++) {
+ Trainer tr = allTrainers.next();
+ if (tr.trainerclass != i) {
+ System.err.println("Trainer mismatch: " + tr.name);
+ }
+ // Write their name
+ int trnamelen = internalStringLength(tr.name);
+ writeFixedLengthString(tr.name, offs, trnamelen + 1);
+ offs += trnamelen + 1;
+ // Write out new trainer data
+ rom[offs++] = (byte) tr.poketype;
+ Iterator<TrainerPokemon> tPokes = tr.pokemon.iterator();
+ for (int tpnum = 0; tpnum < tr.pokemon.size(); tpnum++) {
+ TrainerPokemon tp = tPokes.next();
+ rom[offs] = (byte) tp.level;
+ rom[offs + 1] = (byte) tp.pokemon.number;
+ offs += 2;
+ if (tr.pokemonHaveItems()) {
+ rom[offs] = (byte) tp.heldItem;
+ offs++;
+ }
+ if (tr.pokemonHaveCustomMoves()) {
+ if (tp.resetMoves) {
+ int[] pokeMoves = RomFunctions.getMovesAtLevel(tp.pokemon.number, movesets, tp.level);
+ for (int m = 0; m < 4; m++) {
+ rom[offs + m] = (byte) pokeMoves[m];
+ }
+ } else {
+ rom[offs] = (byte) tp.moves[0];
+ rom[offs + 1] = (byte) tp.moves[1];
+ rom[offs + 2] = (byte) tp.moves[2];
+ rom[offs + 3] = (byte) tp.moves[3];
+ }
+ offs += 4;
+ }
+ }
+ rom[offs] = (byte) 0xFF;
+ offs++;
+ }
+ }
+
+ }
+
+ @Override
+ public List<Pokemon> getPokemon() {
+ return pokemonList;
+ }
+
+ @Override
+ public List<Pokemon> getPokemonInclFormes() {
+ return pokemonList;
+ }
+
+ @Override
+ public List<Pokemon> getAltFormes() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<MegaEvolution> getMegaEvolutions() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public Pokemon getAltFormeOfPokemon(Pokemon pk, int forme) {
+ return pk;
+ }
+
+ @Override
+ public List<Pokemon> getIrregularFormes() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public boolean hasFunctionalFormes() {
+ return false;
+ }
+
+ @Override
+ public Map<Integer, List<MoveLearnt>> getMovesLearnt() {
+ Map<Integer, List<MoveLearnt>> movesets = new TreeMap<>();
+ int pointersOffset = romEntry.getValue("PokemonMovesetsTableOffset");
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ int pointer = readWord(pointersOffset + (i - 1) * 2);
+ int realPointer = calculateOffset(bankOf(pointersOffset), pointer);
+ Pokemon pkmn = pokes[i];
+ // Skip over evolution data
+ while (rom[realPointer] != 0) {
+ if (rom[realPointer] == 5) {
+ realPointer += 4;
+ } else {
+ realPointer += 3;
+ }
+ }
+ List<MoveLearnt> ourMoves = new ArrayList<>();
+ realPointer++;
+ while (rom[realPointer] != 0) {
+ MoveLearnt learnt = new MoveLearnt();
+ learnt.level = rom[realPointer] & 0xFF;
+ learnt.move = rom[realPointer + 1] & 0xFF;
+ ourMoves.add(learnt);
+ realPointer += 2;
+ }
+ movesets.put(pkmn.number, ourMoves);
+ }
+ return movesets;
+ }
+
+ @Override
+ public void setMovesLearnt(Map<Integer, List<MoveLearnt>> movesets) {
+ writeEvosAndMovesLearnt(false, movesets);
+ }
+
+ @Override
+ public List<Integer> getMovesBannedFromLevelup() {
+ return Gen2Constants.bannedLevelupMoves;
+ }
+
+ @Override
+ public Map<Integer, List<Integer>> getEggMoves() {
+ Map<Integer, List<Integer>> eggMoves = new TreeMap<>();
+ int pointersOffset = romEntry.getValue("EggMovesTableOffset");
+ int baseOffset = (pointersOffset / 0x1000) * 0x1000;
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ int eggMovePointer = FileFunctions.read2ByteInt(rom, pointersOffset + ((i - 1) * 2));
+ int eggMoveOffset = baseOffset + (eggMovePointer % 0x1000);
+ List<Integer> moves = new ArrayList<>();
+ int val = rom[eggMoveOffset] & 0xFF;
+ while (val != 0xFF) {
+ moves.add(val);
+ eggMoveOffset++;
+ val = rom[eggMoveOffset] & 0xFF;
+ }
+ if (moves.size() > 0) {
+ eggMoves.put(i, moves);
+ }
+ }
+ return eggMoves;
+ }
+
+ @Override
+ public void setEggMoves(Map<Integer, List<Integer>> eggMoves) {
+ int pointersOffset = romEntry.getValue("EggMovesTableOffset");
+ int baseOffset = (pointersOffset / 0x1000) * 0x1000;
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ int eggMovePointer = FileFunctions.read2ByteInt(rom, pointersOffset + ((i - 1) * 2));
+ int eggMoveOffset = baseOffset + (eggMovePointer % 0x1000);
+ if (eggMoves.containsKey(i)) {
+ List<Integer> moves = eggMoves.get(i);
+ for (int move: moves) {
+ rom[eggMoveOffset] = (byte) move;
+ eggMoveOffset++;
+ }
+ }
+ }
+ }
+
+ private static class StaticPokemon {
+ protected int[] speciesOffsets;
+ protected int[] levelOffsets;
+
+ public StaticPokemon() {
+ this.speciesOffsets = new int[0];
+ this.levelOffsets = new int[0];
+ }
+
+ public Pokemon getPokemon(Gen2RomHandler rh) {
+ return rh.pokes[rh.rom[speciesOffsets[0]] & 0xFF];
+ }
+
+ public void setPokemon(Gen2RomHandler rh, Pokemon pkmn) {
+ for (int offset : speciesOffsets) {
+ rh.rom[offset] = (byte) pkmn.number;
+ }
+ }
+
+ public int getLevel(byte[] rom, int i) {
+ if (levelOffsets.length <= i) {
+ return 1;
+ }
+ return rom[levelOffsets[i]];
+ }
+
+ public void setLevel(byte[] rom, int level, int i) {
+ if (levelOffsets.length > i) { // Might not have a level entry e.g., it's an egg
+ rom[levelOffsets[i]] = (byte) level;
+ }
+ }
+ }
+
+ private static class StaticPokemonGameCorner extends StaticPokemon {
+ @Override
+ public void setPokemon(Gen2RomHandler rh, Pokemon pkmn) {
+ // Last offset is a pointer to the name
+ int offsetSize = speciesOffsets.length;
+ for (int i = 0; i < offsetSize - 1; i++) {
+ rh.rom[speciesOffsets[i]] = (byte) pkmn.number;
+ }
+ rh.writePaddedPokemonName(pkmn.name, rh.romEntry.getValue("GameCornerPokemonNameLength"),
+ speciesOffsets[offsetSize - 1]);
+ }
+ }
+
+ @Override
+ public List<StaticEncounter> getStaticPokemon() {
+ List<StaticEncounter> statics = new ArrayList<>();
+ int[] staticEggOffsets = new int[0];
+ if (romEntry.arrayEntries.containsKey("StaticEggPokemonOffsets")) {
+ staticEggOffsets = romEntry.arrayEntries.get("StaticEggPokemonOffsets");
+ }
+ if (romEntry.getValue("StaticPokemonSupport") > 0) {
+ for (int i = 0; i < romEntry.staticPokemon.size(); i++) {
+ int currentOffset = i;
+ StaticPokemon sp = romEntry.staticPokemon.get(i);
+ StaticEncounter se = new StaticEncounter();
+ se.pkmn = sp.getPokemon(this);
+ se.level = sp.getLevel(rom, 0);
+ se.isEgg = Arrays.stream(staticEggOffsets).anyMatch(x-> x == currentOffset);
+ statics.add(se);
+ }
+ }
+ if (romEntry.getValue("StaticPokemonOddEggOffset") > 0) {
+ int oeOffset = romEntry.getValue("StaticPokemonOddEggOffset");
+ int oeSize = romEntry.getValue("StaticPokemonOddEggDataSize");
+ for (int i = 0; i < Gen2Constants.oddEggPokemonCount; i++) {
+ StaticEncounter se = new StaticEncounter();
+ se.pkmn = pokes[rom[oeOffset + i * oeSize] & 0xFF];
+ se.isEgg = true;
+ statics.add(se);
+ }
+ }
+ return statics;
+ }
+
+ @Override
+ public boolean setStaticPokemon(List<StaticEncounter> staticPokemon) {
+ if (romEntry.getValue("StaticPokemonSupport") == 0) {
+ return false;
+ }
+ if (!havePatchedFleeing) {
+ patchFleeing();
+ }
+
+ int desiredSize = romEntry.staticPokemon.size();
+ if (romEntry.getValue("StaticPokemonOddEggOffset") > 0) {
+ desiredSize += Gen2Constants.oddEggPokemonCount;
+ }
+
+ if (staticPokemon.size() != desiredSize) {
+ return false;
+ }
+
+ Iterator<StaticEncounter> statics = staticPokemon.iterator();
+ for (int i = 0; i < romEntry.staticPokemon.size(); i++) {
+ StaticEncounter currentStatic = statics.next();
+ StaticPokemon sp = romEntry.staticPokemon.get(i);
+ sp.setPokemon(this, currentStatic.pkmn);
+ sp.setLevel(rom, currentStatic.level, 0);
+ }
+
+ if (romEntry.getValue("StaticPokemonOddEggOffset") > 0) {
+ int oeOffset = romEntry.getValue("StaticPokemonOddEggOffset");
+ int oeSize = romEntry.getValue("StaticPokemonOddEggDataSize");
+ for (int i = 0; i < Gen2Constants.oddEggPokemonCount; i++) {
+ int oddEggPokemonNumber = statics.next().pkmn.number;
+ rom[oeOffset + i * oeSize] = (byte) oddEggPokemonNumber;
+ setMovesForOddEggPokemon(oddEggPokemonNumber, oeOffset + i * oeSize);
+ }
+ }
+
+ return true;
+ }
+
+ // This method depends on movesets being randomized before static Pokemon. This is currently true,
+ // but may not *always* be true, so take care.
+ private void setMovesForOddEggPokemon(int oddEggPokemonNumber, int oddEggPokemonOffset) {
+ // Determine the level 5 moveset, minus Dizzy Punch
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+ List<Move> moves = this.getMoves();
+ List<MoveLearnt> moveset = movesets.get(oddEggPokemonNumber);
+ Queue<Integer> level5Moveset = new LinkedList<>();
+ int currentMoveIndex = 0;
+ while (moveset.size() > currentMoveIndex && moveset.get(currentMoveIndex).level <= 5) {
+ if (level5Moveset.size() == 4) {
+ level5Moveset.remove();
+ }
+ level5Moveset.add(moveset.get(currentMoveIndex).move);
+ currentMoveIndex++;
+ }
+
+ // Now add Dizzy Punch and write the moveset and PP
+ if (level5Moveset.size() == 4) {
+ level5Moveset.remove();
+ }
+ level5Moveset.add(Moves.dizzyPunch);
+ for (int i = 0; i < 4; i++) {
+ int move = 0;
+ int pp = 0;
+ if (level5Moveset.size() > 0) {
+ move = level5Moveset.remove();
+ pp = moves.get(move).pp; // This assumes the ordering of moves matches the internal order
+ }
+ rom[oddEggPokemonOffset + 2 + i] = (byte) move;
+ rom[oddEggPokemonOffset + 23 + i] = (byte) pp;
+ }
+ }
+
+ @Override
+ public boolean canChangeStaticPokemon() {
+ return (romEntry.getValue("StaticPokemonSupport") > 0);
+ }
+
+ @Override
+ public boolean hasStaticAltFormes() {
+ return false;
+ }
+
+ @Override
+ public List<Pokemon> bannedForWildEncounters() {
+ // Ban Unown because they don't show up unless you complete a puzzle in the Ruins of Alph.
+ return new ArrayList<>(Collections.singletonList(pokes[Species.unown]));
+ }
+
+ @Override
+ public List<Pokemon> bannedForStaticPokemon() {
+ return Collections.singletonList(pokes[Species.unown]); // Unown banned
+ }
+
+ @Override
+ public boolean hasMainGameLegendaries() {
+ return false;
+ }
+
+ @Override
+ public List<Integer> getMainGameLegendaries() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getSpecialMusicStatics() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void applyCorrectStaticMusic(Map<Integer, Integer> specialMusicStaticChanges) {
+
+ }
+
+ @Override
+ public boolean hasStaticMusicFix() {
+ return false;
+ }
+
+ @Override
+ public List<TotemPokemon> getTotemPokemon() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setTotemPokemon(List<TotemPokemon> totemPokemon) {
+
+ }
+
+ private void writePaddedPokemonName(String name, int length, int offset) {
+ String paddedName = String.format("%-" + length + "s", name);
+ byte[] rawData = translateString(paddedName);
+ System.arraycopy(rawData, 0, rom, offset, length);
+ }
+
+ @Override
+ public List<Integer> getTMMoves() {
+ List<Integer> tms = new ArrayList<>();
+ int offset = romEntry.getValue("TMMovesOffset");
+ for (int i = 1; i <= Gen2Constants.tmCount; i++) {
+ tms.add(rom[offset + (i - 1)] & 0xFF);
+ }
+ return tms;
+ }
+
+ @Override
+ public List<Integer> getHMMoves() {
+ List<Integer> hms = new ArrayList<>();
+ int offset = romEntry.getValue("TMMovesOffset");
+ for (int i = 1; i <= Gen2Constants.hmCount; i++) {
+ hms.add(rom[offset + Gen2Constants.tmCount + (i - 1)] & 0xFF);
+ }
+ return hms;
+ }
+
+ @Override
+ public void setTMMoves(List<Integer> moveIndexes) {
+ int offset = romEntry.getValue("TMMovesOffset");
+ for (int i = 1; i <= Gen2Constants.tmCount; i++) {
+ rom[offset + (i - 1)] = moveIndexes.get(i - 1).byteValue();
+ }
+
+ // TM Text
+ String[] moveNames = readMoveNames();
+ for (TMTextEntry tte : romEntry.tmTexts) {
+ String moveName = moveNames[moveIndexes.get(tte.number - 1)];
+ String text = tte.template.replace("%m", moveName);
+ writeVariableLengthString(text, tte.offset, true);
+ }
+ }
+
+ @Override
+ public int getTMCount() {
+ return Gen2Constants.tmCount;
+ }
+
+ @Override
+ public int getHMCount() {
+ return Gen2Constants.hmCount;
+ }
+
+ @Override
+ public Map<Pokemon, boolean[]> getTMHMCompatibility() {
+ Map<Pokemon, boolean[]> compat = new TreeMap<>();
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ int baseStatsOffset = romEntry.getValue("PokemonStatsOffset") + (i - 1) * Gen2Constants.baseStatsEntrySize;
+ Pokemon pkmn = pokes[i];
+ boolean[] flags = new boolean[Gen2Constants.tmCount + Gen2Constants.hmCount + 1];
+ for (int j = 0; j < 8; j++) {
+ readByteIntoFlags(flags, j * 8 + 1, baseStatsOffset + Gen2Constants.bsTMHMCompatOffset + j);
+ }
+ compat.put(pkmn, flags);
+ }
+ return compat;
+ }
+
+ @Override
+ public void setTMHMCompatibility(Map<Pokemon, boolean[]> compatData) {
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ int baseStatsOffset = romEntry.getValue("PokemonStatsOffset") + (pkmn.number - 1)
+ * Gen2Constants.baseStatsEntrySize;
+ for (int j = 0; j < 8; j++) {
+ if (!romEntry.isCrystal || j != 7) {
+ rom[baseStatsOffset + Gen2Constants.bsTMHMCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1);
+ } else {
+ // Move tutor data
+ // bits 1,2,3 of byte 7
+ int changedByte = getByteFromFlags(flags, j * 8 + 1) & 0xFF;
+ int currentByte = rom[baseStatsOffset + Gen2Constants.bsTMHMCompatOffset + j];
+ changedByte |= ((currentByte >> 1) & 0x01) << 1;
+ changedByte |= ((currentByte >> 2) & 0x01) << 2;
+ changedByte |= ((currentByte >> 3) & 0x01) << 3;
+ rom[baseStatsOffset + 0x18 + j] = (byte) changedByte;
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean hasMoveTutors() {
+ return romEntry.isCrystal;
+ }
+
+ @Override
+ public List<Integer> getMoveTutorMoves() {
+ if (romEntry.isCrystal) {
+ List<Integer> mtMoves = new ArrayList<>();
+ for (int offset : romEntry.arrayEntries.get("MoveTutorMoves")) {
+ mtMoves.add(rom[offset] & 0xFF);
+ }
+ return mtMoves;
+ }
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setMoveTutorMoves(List<Integer> moves) {
+ if (!romEntry.isCrystal) {
+ return;
+ }
+ if (moves.size() != 3) {
+ return;
+ }
+ Iterator<Integer> mvList = moves.iterator();
+ for (int offset : romEntry.arrayEntries.get("MoveTutorMoves")) {
+ rom[offset] = mvList.next().byteValue();
+ }
+
+ // Construct a new menu
+ if (romEntry.getValue("MoveTutorMenuOffset") > 0 && romEntry.getValue("MoveTutorMenuNewSpace") > 0) {
+ String[] moveNames = readMoveNames();
+ String[] names = new String[] { moveNames[moves.get(0)], moveNames[moves.get(1)], moveNames[moves.get(2)],
+ Gen2Constants.mtMenuCancelString };
+ int menuOffset = romEntry.getValue("MoveTutorMenuNewSpace");
+ rom[menuOffset++] = Gen2Constants.mtMenuInitByte;
+ rom[menuOffset++] = 0x4;
+ for (int i = 0; i < 4; i++) {
+ byte[] trans = translateString(names[i]);
+ System.arraycopy(trans, 0, rom, menuOffset, trans.length);
+ menuOffset += trans.length;
+ rom[menuOffset++] = GBConstants.stringTerminator;
+ }
+ int pointerOffset = romEntry.getValue("MoveTutorMenuOffset");
+ writeWord(pointerOffset, makeGBPointer(romEntry.getValue("MoveTutorMenuNewSpace")));
+ }
+ }
+
+ @Override
+ public Map<Pokemon, boolean[]> getMoveTutorCompatibility() {
+ if (!romEntry.isCrystal) {
+ return new TreeMap<>();
+ }
+ Map<Pokemon, boolean[]> compat = new TreeMap<>();
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ int baseStatsOffset = romEntry.getValue("PokemonStatsOffset") + (i - 1) * Gen2Constants.baseStatsEntrySize;
+ Pokemon pkmn = pokes[i];
+ boolean[] flags = new boolean[4];
+ int mtByte = rom[baseStatsOffset + Gen2Constants.bsMTCompatOffset] & 0xFF;
+ for (int j = 1; j <= 3; j++) {
+ flags[j] = ((mtByte >> j) & 0x01) > 0;
+ }
+ compat.put(pkmn, flags);
+ }
+ return compat;
+ }
+
+ @Override
+ public void setMoveTutorCompatibility(Map<Pokemon, boolean[]> compatData) {
+ if (!romEntry.isCrystal) {
+ return;
+ }
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ int baseStatsOffset = romEntry.getValue("PokemonStatsOffset") + (pkmn.number - 1)
+ * Gen2Constants.baseStatsEntrySize;
+ int origMtByte = rom[baseStatsOffset + Gen2Constants.bsMTCompatOffset] & 0xFF;
+ int mtByte = origMtByte & 0x01;
+ for (int j = 1; j <= 3; j++) {
+ mtByte |= flags[j] ? (1 << j) : 0;
+ }
+ rom[baseStatsOffset + Gen2Constants.bsMTCompatOffset] = (byte) mtByte;
+ }
+ }
+
+ @Override
+ public String getROMName() {
+ if (isVietCrystal) {
+ return Gen2Constants.vietCrystalROMName;
+ }
+ return "Pokemon " + romEntry.name;
+ }
+
+ @Override
+ public String getROMCode() {
+ return romEntry.romCode;
+ }
+
+ @Override
+ public String getSupportLevel() {
+ return "Complete";
+ }
+
+ private static int find(byte[] haystack, String hexString) {
+ if (hexString.length() % 2 != 0) {
+ return -3; // error
+ }
+ byte[] searchFor = new byte[hexString.length() / 2];
+ for (int i = 0; i < searchFor.length; i++) {
+ searchFor[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16);
+ }
+ List<Integer> found = RomFunctions.search(haystack, searchFor);
+ if (found.size() == 0) {
+ return -1; // not found
+ } else if (found.size() > 1) {
+ return -2; // not unique
+ } else {
+ return found.get(0);
+ }
+ }
+
+ @Override
+ public boolean hasTimeBasedEncounters() {
+ return true; // All GSC do
+ }
+
+ @Override
+ public boolean hasWildAltFormes() {
+ return false;
+ }
+
+ private void populateEvolutions() {
+ for (Pokemon pkmn : pokes) {
+ if (pkmn != null) {
+ pkmn.evolutionsFrom.clear();
+ pkmn.evolutionsTo.clear();
+ }
+ }
+
+ int pointersOffset = romEntry.getValue("PokemonMovesetsTableOffset");
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ int pointer = readWord(pointersOffset + (i - 1) * 2);
+ int realPointer = calculateOffset(bankOf(pointersOffset), pointer);
+ Pokemon pkmn = pokes[i];
+ while (rom[realPointer] != 0) {
+ int method = rom[realPointer] & 0xFF;
+ int otherPoke = rom[realPointer + 2 + (method == 5 ? 1 : 0)] & 0xFF;
+ EvolutionType type = EvolutionType.fromIndex(2, method);
+ int extraInfo = 0;
+ if (type == EvolutionType.TRADE) {
+ int itemNeeded = rom[realPointer + 1] & 0xFF;
+ if (itemNeeded != 0xFF) {
+ type = EvolutionType.TRADE_ITEM;
+ extraInfo = itemNeeded;
+ }
+ } else if (type == EvolutionType.LEVEL_ATTACK_HIGHER) {
+ int tyrogueCond = rom[realPointer + 2] & 0xFF;
+ if (tyrogueCond == 2) {
+ type = EvolutionType.LEVEL_DEFENSE_HIGHER;
+ } else if (tyrogueCond == 3) {
+ type = EvolutionType.LEVEL_ATK_DEF_SAME;
+ }
+ extraInfo = rom[realPointer + 1] & 0xFF;
+ } else if (type == EvolutionType.HAPPINESS) {
+ int happCond = rom[realPointer + 1] & 0xFF;
+ if (happCond == 2) {
+ type = EvolutionType.HAPPINESS_DAY;
+ } else if (happCond == 3) {
+ type = EvolutionType.HAPPINESS_NIGHT;
+ }
+ } else {
+ extraInfo = rom[realPointer + 1] & 0xFF;
+ }
+ Evolution evo = new Evolution(pokes[i], pokes[otherPoke], true, type, extraInfo);
+ if (!pkmn.evolutionsFrom.contains(evo)) {
+ pkmn.evolutionsFrom.add(evo);
+ pokes[otherPoke].evolutionsTo.add(evo);
+ }
+ realPointer += (method == 5 ? 4 : 3);
+ }
+ // split evos don't carry stats
+ if (pkmn.evolutionsFrom.size() > 1) {
+ for (Evolution e : pkmn.evolutionsFrom) {
+ e.carryStats = false;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void removeImpossibleEvolutions(Settings settings) {
+ // no move evos, so no need to check for those
+ for (Pokemon pkmn : pokes) {
+ if (pkmn != null) {
+ for (Evolution evol : pkmn.evolutionsFrom) {
+ if (evol.type == EvolutionType.TRADE || evol.type == EvolutionType.TRADE_ITEM) {
+ // change
+ if (evol.from.number == Species.slowpoke) {
+ // Slowpoke: Make water stone => Slowking
+ evol.type = EvolutionType.STONE;
+ evol.extraInfo = Gen2Items.waterStone;
+ addEvoUpdateStone(impossibleEvolutionUpdates, evol, itemNames[24]);
+ } else if (evol.from.number == Species.seadra) {
+ // Seadra: level 40
+ evol.type = EvolutionType.LEVEL;
+ evol.extraInfo = 40; // level
+ addEvoUpdateLevel(impossibleEvolutionUpdates, evol);
+ } else if (evol.from.number == Species.poliwhirl || evol.type == EvolutionType.TRADE) {
+ // Poliwhirl or any of the original 4 trade evos
+ // Level 37
+ evol.type = EvolutionType.LEVEL;
+ evol.extraInfo = 37; // level
+ addEvoUpdateLevel(impossibleEvolutionUpdates, evol);
+ } else {
+ // A new trade evo of a single stage Pokemon
+ // level 30
+ evol.type = EvolutionType.LEVEL;
+ evol.extraInfo = 30; // level
+ addEvoUpdateLevel(impossibleEvolutionUpdates, evol);
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public void makeEvolutionsEasier(Settings settings) {
+ // Reduce the amount of happiness required to evolve.
+ int offset = find(rom, Gen2Constants.friendshipValueForEvoLocator);
+ if (offset > 0) {
+ // The thing we're looking at is actually one byte before what we
+ // want to change; this makes it work in both G/S and Crystal.
+ offset++;
+
+ // Amount of required happiness for all happiness evolutions.
+ if (rom[offset] == (byte)220) {
+ rom[offset] = (byte)160;
+ }
+ }
+ }
+
+ @Override
+ public void removeTimeBasedEvolutions() {
+ for (Pokemon pkmn : pokes) {
+ if (pkmn != null) {
+ for (Evolution evol : pkmn.evolutionsFrom) {
+ // In Gen 2, only Eevee has a time-based evolution.
+ if (evol.type == EvolutionType.HAPPINESS_DAY) {
+ // Eevee: Make sun stone => Espeon
+ evol.type = EvolutionType.STONE;
+ evol.extraInfo = Gen2Items.sunStone;
+ addEvoUpdateStone(timeBasedEvolutionUpdates, evol, itemNames[169]);
+ } else if (evol.type == EvolutionType.HAPPINESS_NIGHT) {
+ // Eevee: Make moon stone => Umbreon
+ evol.type = EvolutionType.STONE;
+ evol.extraInfo = Gen2Items.moonStone;
+ addEvoUpdateStone(timeBasedEvolutionUpdates, evol, itemNames[8]);
+ }
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public boolean hasShopRandomization() {
+ return false;
+ }
+
+ @Override
+ public Map<Integer, Shop> getShopItems() {
+ return null; // Not implemented
+ }
+
+ @Override
+ public void setShopItems(Map<Integer, Shop> shopItems) {
+ // Not implemented
+ }
+
+ @Override
+ public void setShopPrices() {
+ // Not implemented
+ }
+
+ @Override
+ public boolean canChangeTrainerText() {
+ return romEntry.getValue("CanChangeTrainerText") > 0;
+ }
+
+ @Override
+ public List<String> getTrainerNames() {
+ int traineroffset = romEntry.getValue("TrainerDataTableOffset");
+ int traineramount = romEntry.getValue("TrainerClassAmount");
+ int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts");
+
+ int[] pointers = new int[traineramount];
+ for (int i = 0; i < traineramount; i++) {
+ int pointer = readWord(traineroffset + i * 2);
+ pointers[i] = calculateOffset(bankOf(traineroffset), pointer);
+ }
+
+ List<String> allTrainers = new ArrayList<>();
+ for (int i = 0; i < traineramount; i++) {
+ int offs = pointers[i];
+ int limit = trainerclasslimits[i];
+ for (int trnum = 0; trnum < limit; trnum++) {
+ String name = readVariableLengthString(offs, false);
+ allTrainers.add(name);
+ offs += lengthOfStringAt(offs, false) + 1;
+ int dataType = rom[offs] & 0xFF;
+ offs++;
+ while ((rom[offs] & 0xFF) != 0xFF) {
+ offs += 2;
+ if (dataType == 2 || dataType == 3) {
+ offs++;
+ }
+ if (dataType % 2 == 1) {
+ offs += 4;
+ }
+ }
+ offs++;
+ }
+ }
+ return allTrainers;
+ }
+
+ @Override
+ public void setTrainerNames(List<String> trainerNames) {
+ if (romEntry.getValue("CanChangeTrainerText") != 0) {
+ int traineroffset = romEntry.getValue("TrainerDataTableOffset");
+ int traineramount = romEntry.getValue("TrainerClassAmount");
+ int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts");
+
+ int[] pointers = new int[traineramount];
+ for (int i = 0; i < traineramount; i++) {
+ int pointer = readWord(traineroffset + i * 2);
+ pointers[i] = calculateOffset(bankOf(traineroffset), pointer);
+ }
+
+ // Build up new trainer data using old as a guideline.
+ int[] offsetsInNew = new int[traineramount];
+ int oInNewCurrent = 0;
+ Iterator<String> allTrainers = trainerNames.iterator();
+ ByteArrayOutputStream newData = new ByteArrayOutputStream();
+ try {
+ for (int i = 0; i < traineramount; i++) {
+ int offs = pointers[i];
+ int limit = trainerclasslimits[i];
+ offsetsInNew[i] = oInNewCurrent;
+ for (int trnum = 0; trnum < limit; trnum++) {
+ String newName = allTrainers.next();
+
+ // The game uses 0xFF as a signifier for the end of the trainer data.
+ // It ALSO uses 0xFF to encode the character "9". If a trainer name has
+ // "9" in it, this causes strange side effects where certain trainers
+ // effectively get skipped when parsing trainer data. Silently strip out
+ // "9"s from trainer names to prevent this from happening.
+ newName = newName.replace("9", "").trim();
+
+ byte[] newNameStr = translateString(newName);
+ newData.write(newNameStr);
+ newData.write(GBConstants.stringTerminator);
+ oInNewCurrent += newNameStr.length + 1;
+ offs += lengthOfStringAt(offs, false) + 1;
+ int dataType = rom[offs] & 0xFF;
+ offs++;
+ newData.write(dataType);
+ oInNewCurrent++;
+ while ((rom[offs] & 0xFF) != 0xFF) {
+ newData.write(rom, offs, 2);
+ oInNewCurrent += 2;
+ offs += 2;
+ if (dataType == 2 || dataType == 3) {
+ newData.write(rom, offs, 1);
+ oInNewCurrent++;
+ offs++;
+ }
+ if (dataType % 2 == 1) {
+ newData.write(rom, offs, 4);
+ oInNewCurrent += 4;
+ offs += 4;
+ }
+ }
+ newData.write(0xFF);
+ oInNewCurrent++;
+ offs++;
+ }
+ }
+
+ // Copy new data into ROM
+ byte[] newTrainerData = newData.toByteArray();
+ int tdBase = pointers[0];
+ System.arraycopy(newTrainerData, 0, rom, pointers[0], newTrainerData.length);
+
+ // Finally, update the pointers
+ for (int i = 1; i < traineramount; i++) {
+ int newOffset = tdBase + offsetsInNew[i];
+ writeWord(traineroffset + i * 2, makeGBPointer(newOffset));
+ }
+ } catch (IOException ex) {
+ // This should never happen, but abort if it does.
+ }
+ }
+
+ }
+
+ @Override
+ public TrainerNameMode trainerNameMode() {
+ return TrainerNameMode.MAX_LENGTH_WITH_CLASS;
+ }
+
+ @Override
+ public int maxTrainerNameLength() {
+ // line size minus one for space
+ return Gen2Constants.maxTrainerNameLength;
+ }
+
+ @Override
+ public int maxSumOfTrainerNameLengths() {
+ return romEntry.getValue("MaxSumOfTrainerNameLengths");
+ }
+
+ @Override
+ public List<Integer> getTCNameLengthsByTrainer() {
+ int traineramount = romEntry.getValue("TrainerClassAmount");
+ int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts");
+ List<String> tcNames = this.getTrainerClassNames();
+ List<Integer> tcLengthsByT = new ArrayList<>();
+
+ for (int i = 0; i < traineramount; i++) {
+ int len = internalStringLength(tcNames.get(i));
+ for (int k = 0; k < trainerclasslimits[i]; k++) {
+ tcLengthsByT.add(len);
+ }
+ }
+
+ return tcLengthsByT;
+ }
+
+ @Override
+ public List<String> getTrainerClassNames() {
+ int amount = romEntry.getValue("TrainerClassAmount");
+ int offset = romEntry.getValue("TrainerClassNamesOffset");
+ List<String> trainerClassNames = new ArrayList<>();
+ for (int j = 0; j < amount; j++) {
+ String name = readVariableLengthString(offset, false);
+ offset += lengthOfStringAt(offset, false) + 1;
+ trainerClassNames.add(name);
+ }
+ return trainerClassNames;
+ }
+
+ @Override
+ public List<Integer> getEvolutionItems() {
+ return null;
+ }
+
+ @Override
+ public void setTrainerClassNames(List<String> trainerClassNames) {
+ if (romEntry.getValue("CanChangeTrainerText") != 0) {
+ int amount = romEntry.getValue("TrainerClassAmount");
+ int offset = romEntry.getValue("TrainerClassNamesOffset");
+ Iterator<String> trainerClassNamesI = trainerClassNames.iterator();
+ for (int j = 0; j < amount; j++) {
+ int len = lengthOfStringAt(offset, false) + 1;
+ String newName = trainerClassNamesI.next();
+ writeFixedLengthString(newName, offset, len);
+ offset += len;
+ }
+ }
+ }
+
+ @Override
+ public boolean fixedTrainerClassNamesLength() {
+ return true;
+ }
+
+ @Override
+ public List<Integer> getDoublesTrainerClasses() {
+ int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses");
+ List<Integer> doubles = new ArrayList<>();
+ for (int tClass : doublesClasses) {
+ doubles.add(tClass);
+ }
+ return doubles;
+ }
+
+ @Override
+ public String getDefaultExtension() {
+ return "gbc";
+ }
+
+ @Override
+ public int abilitiesPerPokemon() {
+ return 0;
+ }
+
+ @Override
+ public int highestAbilityIndex() {
+ return 0;
+ }
+
+ @Override
+ public Map<Integer, List<Integer>> getAbilityVariations() {
+ return new HashMap<>();
+ }
+
+ @Override
+ public boolean hasMegaEvolutions() {
+ return false;
+ }
+
+ @Override
+ public int internalStringLength(String string) {
+ return translateString(string).length;
+ }
+
+ @Override
+ public int miscTweaksAvailable() {
+ int available = MiscTweak.LOWER_CASE_POKEMON_NAMES.getValue();
+ available |= MiscTweak.UPDATE_TYPE_EFFECTIVENESS.getValue();
+ if (romEntry.codeTweaks.get("BWXPTweak") != null) {
+ available |= MiscTweak.BW_EXP_PATCH.getValue();
+ }
+ if (romEntry.getValue("TextDelayFunctionOffset") != 0) {
+ available |= MiscTweak.FASTEST_TEXT.getValue();
+ }
+ if (romEntry.arrayEntries.containsKey("CatchingTutorialOffsets")) {
+ available |= MiscTweak.RANDOMIZE_CATCHING_TUTORIAL.getValue();
+ }
+ available |= MiscTweak.BAN_LUCKY_EGG.getValue();
+ return available;
+ }
+
+ @Override
+ public void applyMiscTweak(MiscTweak tweak) {
+ if (tweak == MiscTweak.BW_EXP_PATCH) {
+ applyBWEXPPatch();
+ } else if (tweak == MiscTweak.FASTEST_TEXT) {
+ applyFastestTextPatch();
+ } else if (tweak == MiscTweak.LOWER_CASE_POKEMON_NAMES) {
+ applyCamelCaseNames();
+ } else if (tweak == MiscTweak.RANDOMIZE_CATCHING_TUTORIAL) {
+ randomizeCatchingTutorial();
+ } else if (tweak == MiscTweak.BAN_LUCKY_EGG) {
+ allowedItems.banSingles(Gen2Items.luckyEgg);
+ nonBadItems.banSingles(Gen2Items.luckyEgg);
+ } else if (tweak == MiscTweak.UPDATE_TYPE_EFFECTIVENESS) {
+ updateTypeEffectiveness();
+ }
+ }
+
+ @Override
+ public boolean isEffectivenessUpdated() {
+ return effectivenessUpdated;
+ }
+
+ private void randomizeCatchingTutorial() {
+ if (romEntry.arrayEntries.containsKey("CatchingTutorialOffsets")) {
+ // Pick a pokemon
+ int pokemon = this.random.nextInt(Gen2Constants.pokemonCount) + 1;
+ while (pokemon == Species.unown) {
+ // Unown is banned
+ pokemon = this.random.nextInt(Gen2Constants.pokemonCount) + 1;
+ }
+
+ int[] offsets = romEntry.arrayEntries.get("CatchingTutorialOffsets");
+ for (int offset : offsets) {
+ rom[offset] = (byte) pokemon;
+ }
+ }
+
+ }
+
+ private void applyBWEXPPatch() {
+ String patchName = romEntry.codeTweaks.get("BWXPTweak");
+ if (patchName == null) {
+ return;
+ }
+
+ try {
+ FileFunctions.applyPatch(rom, patchName);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void applyFastestTextPatch() {
+ if (romEntry.getValue("TextDelayFunctionOffset") != 0) {
+ rom[romEntry.getValue("TextDelayFunctionOffset")] = GBConstants.gbZ80Ret;
+ }
+ }
+
+ private void updateTypeEffectiveness() {
+ List<TypeRelationship> typeEffectivenessTable = readTypeEffectivenessTable();
+ log("--Updating Type Effectiveness--");
+ for (TypeRelationship relationship : typeEffectivenessTable) {
+ // Change Ghost 0.5x against Steel to Ghost 1x to Steel
+ if (relationship.attacker == Type.GHOST && relationship.defender == Type.STEEL) {
+ relationship.effectiveness = Effectiveness.NEUTRAL;
+ log("Replaced: Ghost not very effective vs Steel => Ghost neutral vs Steel");
+ }
+
+ // Change Dark 0.5x against Steel to Dark 1x to Steel
+ else if (relationship.attacker == Type.DARK && relationship.defender == Type.STEEL) {
+ relationship.effectiveness = Effectiveness.NEUTRAL;
+ log("Replaced: Dark not very effective vs Steel => Dark neutral vs Steel");
+ }
+ }
+ logBlankLine();
+ writeTypeEffectivenessTable(typeEffectivenessTable);
+ effectivenessUpdated = true;
+ }
+
+ private List<TypeRelationship> readTypeEffectivenessTable() {
+ List<TypeRelationship> typeEffectivenessTable = new ArrayList<>();
+ int currentOffset = romEntry.getValue("TypeEffectivenessOffset");
+ int attackingType = rom[currentOffset];
+ // 0xFE marks the end of the table *not* affected by Foresight, while 0xFF marks
+ // the actual end of the table. Since we don't care about Ghost immunities at all,
+ // just stop once we reach the Foresight section.
+ while (attackingType != (byte) 0xFE) {
+ int defendingType = rom[currentOffset + 1];
+ int effectivenessInternal = rom[currentOffset + 2];
+ Type attacking = Gen2Constants.typeTable[attackingType];
+ Type defending = Gen2Constants.typeTable[defendingType];
+ Effectiveness effectiveness = null;
+ switch (effectivenessInternal) {
+ case 20:
+ effectiveness = Effectiveness.DOUBLE;
+ break;
+ case 10:
+ effectiveness = Effectiveness.NEUTRAL;
+ break;
+ case 5:
+ effectiveness = Effectiveness.HALF;
+ break;
+ case 0:
+ effectiveness = Effectiveness.ZERO;
+ break;
+ }
+ if (effectiveness != null) {
+ TypeRelationship relationship = new TypeRelationship(attacking, defending, effectiveness);
+ typeEffectivenessTable.add(relationship);
+ }
+ currentOffset += 3;
+ attackingType = rom[currentOffset];
+ }
+ return typeEffectivenessTable;
+ }
+
+ private void writeTypeEffectivenessTable(List<TypeRelationship> typeEffectivenessTable) {
+ int currentOffset = romEntry.getValue("TypeEffectivenessOffset");
+ for (TypeRelationship relationship : typeEffectivenessTable) {
+ rom[currentOffset] = Gen2Constants.typeToByte(relationship.attacker);
+ rom[currentOffset + 1] = Gen2Constants.typeToByte(relationship.defender);
+ byte effectivenessInternal = 0;
+ switch (relationship.effectiveness) {
+ case DOUBLE:
+ effectivenessInternal = 20;
+ break;
+ case NEUTRAL:
+ effectivenessInternal = 10;
+ break;
+ case HALF:
+ effectivenessInternal = 5;
+ break;
+ case ZERO:
+ effectivenessInternal = 0;
+ break;
+ }
+ rom[currentOffset + 2] = effectivenessInternal;
+ currentOffset += 3;
+ }
+ }
+
+ @Override
+ public void enableGuaranteedPokemonCatching() {
+ String prefix = romEntry.getString("GuaranteedCatchPrefix");
+ int offset = find(rom, prefix);
+ if (offset > 0) {
+ offset += prefix.length() / 2; // because it was a prefix
+
+ // The game guarantees that the catching tutorial always succeeds in catching by running
+ // the following code:
+ // ld a, [wBattleType]
+ // cp BATTLETYPE_TUTORIAL
+ // jp z, .catch_without_fail
+ // By making the jump here unconditional, we can ensure that catching always succeeds no
+ // matter the battle type. We check that the original condition is present just for safety.
+ if (rom[offset] == (byte)0xCA) {
+ rom[offset] = (byte)0xC3;
+ }
+ }
+ }
+
+ @Override
+ public void randomizeIntroPokemon() {
+ // Intro sprite
+
+ // Pick a pokemon
+ int pokemon = this.random.nextInt(Gen2Constants.pokemonCount) + 1;
+ while (pokemon == Species.unown) {
+ // Unown is banned
+ pokemon = this.random.nextInt(Gen2Constants.pokemonCount) + 1;
+ }
+
+ rom[romEntry.getValue("IntroSpriteOffset")] = (byte) pokemon;
+ rom[romEntry.getValue("IntroCryOffset")] = (byte) pokemon;
+
+ }
+
+ @Override
+ public ItemList getAllowedItems() {
+ return allowedItems;
+ }
+
+ @Override
+ public ItemList getNonBadItems() {
+ return nonBadItems;
+ }
+
+ @Override
+ public List<Integer> getUniqueNoSellItems() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getRegularShopItems() {
+ return null; // Not implemented
+ }
+
+ @Override
+ public List<Integer> getOPShopItems() {
+ return null; // Not implemented
+ }
+
+ private void loadItemNames() {
+ itemNames = new String[256];
+ itemNames[0] = "glitch";
+ // trying to emulate pretty much what the game does here
+ // normal items
+ int origOffset = romEntry.getValue("ItemNamesOffset");
+ int itemNameOffset = origOffset;
+ for (int index = 1; index <= 0x100; index++) {
+ if (itemNameOffset / GBConstants.bankSize > origOffset / GBConstants.bankSize) {
+ // the game would continue making its merry way into VRAM here,
+ // but we don't have VRAM to simulate.
+ // just give up.
+ break;
+ }
+ int startOfText = itemNameOffset;
+ while ((rom[itemNameOffset] & 0xFF) != GBConstants.stringTerminator) {
+ itemNameOffset++;
+ }
+ itemNameOffset++;
+ itemNames[index % 256] = readFixedLengthString(startOfText, 20);
+ }
+ }
+
+ @Override
+ public String[] getItemNames() {
+ return itemNames;
+ }
+
+ private void patchFleeing() {
+ havePatchedFleeing = true;
+ int offset = romEntry.getValue("FleeingDataOffset");
+ rom[offset] = (byte) 0xFF;
+ rom[offset + Gen2Constants.fleeingSetTwoOffset] = (byte) 0xFF;
+ rom[offset + Gen2Constants.fleeingSetThreeOffset] = (byte) 0xFF;
+ }
+
+ private void loadLandmarkNames() {
+
+ int lmOffset = romEntry.getValue("LandmarkTableOffset");
+ int lmBank = bankOf(lmOffset);
+ int lmCount = romEntry.getValue("LandmarkCount");
+
+ landmarkNames = new String[lmCount];
+
+ for (int i = 0; i < lmCount; i++) {
+ int lmNameOffset = calculateOffset(lmBank, readWord(lmOffset + i * 4 + 2));
+ landmarkNames[i] = readVariableLengthString(lmNameOffset, false).replace("\\x1F", " ");
+ }
+
+ }
+
+ private void preprocessMaps() {
+ itemOffs = new ArrayList<>();
+
+ int mhOffset = romEntry.getValue("MapHeaders");
+ int mapGroupCount = Gen2Constants.mapGroupCount;
+ int mapsInLastGroup = Gen2Constants.mapsInLastGroup;
+ int mhBank = bankOf(mhOffset);
+ mapNames = new String[mapGroupCount + 1][100];
+
+ int[] groupOffsets = new int[mapGroupCount];
+ for (int i = 0; i < mapGroupCount; i++) {
+ groupOffsets[i] = calculateOffset(mhBank, readWord(mhOffset + i * 2));
+ }
+
+ // Read maps
+ for (int mg = 0; mg < mapGroupCount; mg++) {
+ int offset = groupOffsets[mg];
+ int maxOffset = (mg == mapGroupCount - 1) ? (mhBank + 1) * GBConstants.bankSize : groupOffsets[mg + 1];
+ int map = 0;
+ int maxMap = (mg == mapGroupCount - 1) ? mapsInLastGroup : Integer.MAX_VALUE;
+ while (offset < maxOffset && map < maxMap) {
+ processMapAt(offset, mg + 1, map + 1);
+ offset += 9;
+ map++;
+ }
+ }
+ }
+
+ private void processMapAt(int offset, int mapBank, int mapNumber) {
+
+ // second map header
+ int smhBank = rom[offset] & 0xFF;
+ int smhPointer = readWord(offset + 3);
+ int smhOffset = calculateOffset(smhBank, smhPointer);
+
+ // map name
+ int mapLandmark = rom[offset + 5] & 0xFF;
+ mapNames[mapBank][mapNumber] = landmarkNames[mapLandmark];
+
+ // event header
+ // event header is in same bank as script header
+ int ehBank = rom[smhOffset + 6] & 0xFF;
+ int ehPointer = readWord(smhOffset + 9);
+ int ehOffset = calculateOffset(ehBank, ehPointer);
+
+ // skip over filler
+ ehOffset += 2;
+
+ // warps
+ int warpCount = rom[ehOffset++] & 0xFF;
+ // warps are skipped
+ ehOffset += warpCount * 5;
+
+ // xy triggers
+ int triggerCount = rom[ehOffset++] & 0xFF;
+ // xy triggers are skipped
+ ehOffset += triggerCount * 8;
+
+ // signposts
+ int signpostCount = rom[ehOffset++] & 0xFF;
+ // we do care about these
+ for (int sp = 0; sp < signpostCount; sp++) {
+ // type=7 are hidden items
+ int spType = rom[ehOffset + sp * 5 + 2] & 0xFF;
+ if (spType == 7) {
+ // get event pointer
+ int spPointer = readWord(ehOffset + sp * 5 + 3);
+ int spOffset = calculateOffset(ehBank, spPointer);
+ // item is at spOffset+2 (first two bytes are the flag id)
+ itemOffs.add(spOffset + 2);
+ }
+ }
+ // now skip past them
+ ehOffset += signpostCount * 5;
+
+ // visible objects/people
+ int peopleCount = rom[ehOffset++] & 0xFF;
+ // we also care about these
+ for (int p = 0; p < peopleCount; p++) {
+ // color_function & 1 = 1 if itemball
+ int pColorFunction = rom[ehOffset + p * 13 + 7];
+ if ((pColorFunction & 1) == 1) {
+ // get event pointer
+ int pPointer = readWord(ehOffset + p * 13 + 9);
+ int pOffset = calculateOffset(ehBank, pPointer);
+ // item is at the pOffset for non-hidden items
+ itemOffs.add(pOffset);
+ }
+ }
+
+ }
+
+ @Override
+ public List<Integer> getRequiredFieldTMs() {
+ return Gen2Constants.requiredFieldTMs;
+ }
+
+ @Override
+ public List<Integer> getCurrentFieldTMs() {
+ List<Integer> fieldTMs = new ArrayList<>();
+
+ for (int offset : itemOffs) {
+ int itemHere = rom[offset] & 0xFF;
+ if (Gen2Constants.allowedItems.isTM(itemHere)) {
+ int thisTM;
+ if (itemHere >= Gen2Constants.tmBlockOneIndex
+ && itemHere < Gen2Constants.tmBlockOneIndex + Gen2Constants.tmBlockOneSize) {
+ thisTM = itemHere - Gen2Constants.tmBlockOneIndex + 1;
+ } else if (itemHere >= Gen2Constants.tmBlockTwoIndex
+ && itemHere < Gen2Constants.tmBlockTwoIndex + Gen2Constants.tmBlockTwoSize) {
+ thisTM = itemHere - Gen2Constants.tmBlockTwoIndex + 1 + Gen2Constants.tmBlockOneSize; // TM
+ // block
+ // 2
+ // offset
+ } else {
+ thisTM = itemHere - Gen2Constants.tmBlockThreeIndex + 1 + Gen2Constants.tmBlockOneSize
+ + Gen2Constants.tmBlockTwoSize; // TM block 3 offset
+ }
+ // hack for the bug catching contest repeat TM28
+ if (!fieldTMs.contains(thisTM)) {
+ fieldTMs.add(thisTM);
+ }
+ }
+ }
+ return fieldTMs;
+ }
+
+ @Override
+ public void setFieldTMs(List<Integer> fieldTMs) {
+ Iterator<Integer> iterTMs = fieldTMs.iterator();
+ int[] givenTMs = new int[256];
+
+ for (int offset : itemOffs) {
+ int itemHere = rom[offset] & 0xFF;
+ if (Gen2Constants.allowedItems.isTM(itemHere)) {
+ // Cache replaced TMs to duplicate bug catching contest TM
+ if (givenTMs[itemHere] != 0) {
+ rom[offset] = (byte) givenTMs[itemHere];
+ } else {
+ // Replace this with a TM from the list
+ int tm = iterTMs.next();
+ if (tm >= 1 && tm <= Gen2Constants.tmBlockOneSize) {
+ tm += Gen2Constants.tmBlockOneIndex - 1;
+ } else if (tm >= Gen2Constants.tmBlockOneSize + 1
+ && tm <= Gen2Constants.tmBlockOneSize + Gen2Constants.tmBlockTwoSize) {
+ tm += Gen2Constants.tmBlockTwoIndex - 1 - Gen2Constants.tmBlockOneSize;
+ } else {
+ tm += Gen2Constants.tmBlockThreeIndex - 1 - Gen2Constants.tmBlockOneSize
+ - Gen2Constants.tmBlockTwoSize;
+ }
+ givenTMs[itemHere] = tm;
+ rom[offset] = (byte) tm;
+ }
+ }
+ }
+ }
+
+ @Override
+ public List<Integer> getRegularFieldItems() {
+ List<Integer> fieldItems = new ArrayList<>();
+
+ for (int offset : itemOffs) {
+ int itemHere = rom[offset] & 0xFF;
+ if (Gen2Constants.allowedItems.isAllowed(itemHere) && !(Gen2Constants.allowedItems.isTM(itemHere))) {
+ fieldItems.add(itemHere);
+ }
+ }
+ return fieldItems;
+ }
+
+ @Override
+ public void setRegularFieldItems(List<Integer> items) {
+ Iterator<Integer> iterItems = items.iterator();
+
+ for (int offset : itemOffs) {
+ int itemHere = rom[offset] & 0xFF;
+ if (Gen2Constants.allowedItems.isAllowed(itemHere) && !(Gen2Constants.allowedItems.isTM(itemHere))) {
+ // Replace it
+ rom[offset] = (byte) (iterItems.next().intValue());
+ }
+ }
+
+ }
+
+ @Override
+ public List<IngameTrade> getIngameTrades() {
+ List<IngameTrade> trades = new ArrayList<>();
+
+ // info
+ int tableOffset = romEntry.getValue("TradeTableOffset");
+ int tableSize = romEntry.getValue("TradeTableSize");
+ int nicknameLength = romEntry.getValue("TradeNameLength");
+ int otLength = romEntry.getValue("TradeOTLength");
+ int[] unused = romEntry.arrayEntries.get("TradesUnused");
+ int unusedOffset = 0;
+ int entryLength = nicknameLength + otLength + 9;
+ if (entryLength % 2 != 0) {
+ entryLength++;
+ }
+
+ for (int entry = 0; entry < tableSize; entry++) {
+ if (unusedOffset < unused.length && unused[unusedOffset] == entry) {
+ unusedOffset++;
+ continue;
+ }
+ IngameTrade trade = new IngameTrade();
+ int entryOffset = tableOffset + entry * entryLength;
+ trade.requestedPokemon = pokes[rom[entryOffset + 1] & 0xFF];
+ trade.givenPokemon = pokes[rom[entryOffset + 2] & 0xFF];
+ trade.nickname = readString(entryOffset + 3, nicknameLength, false);
+ int atkdef = rom[entryOffset + 3 + nicknameLength] & 0xFF;
+ int spdspc = rom[entryOffset + 4 + nicknameLength] & 0xFF;
+ trade.ivs = new int[] { (atkdef >> 4) & 0xF, atkdef & 0xF, (spdspc >> 4) & 0xF, spdspc & 0xF };
+ trade.item = rom[entryOffset + 5 + nicknameLength] & 0xFF;
+ trade.otId = readWord(entryOffset + 6 + nicknameLength);
+ trade.otName = readString(entryOffset + 8 + nicknameLength, otLength, false);
+ trades.add(trade);
+ }
+
+ return trades;
+
+ }
+
+ @Override
+ public void setIngameTrades(List<IngameTrade> trades) {
+ // info
+ int tableOffset = romEntry.getValue("TradeTableOffset");
+ int tableSize = romEntry.getValue("TradeTableSize");
+ int nicknameLength = romEntry.getValue("TradeNameLength");
+ int otLength = romEntry.getValue("TradeOTLength");
+ int[] unused = romEntry.arrayEntries.get("TradesUnused");
+ int unusedOffset = 0;
+ int entryLength = nicknameLength + otLength + 9;
+ if (entryLength % 2 != 0) {
+ entryLength++;
+ }
+ int tradeOffset = 0;
+
+ for (int entry = 0; entry < tableSize; entry++) {
+ if (unusedOffset < unused.length && unused[unusedOffset] == entry) {
+ unusedOffset++;
+ continue;
+ }
+ IngameTrade trade = trades.get(tradeOffset++);
+ int entryOffset = tableOffset + entry * entryLength;
+ rom[entryOffset + 1] = (byte) trade.requestedPokemon.number;
+ rom[entryOffset + 2] = (byte) trade.givenPokemon.number;
+ if (romEntry.getValue("CanChangeTrainerText") > 0) {
+ writeFixedLengthString(trade.nickname, entryOffset + 3, nicknameLength);
+ }
+ rom[entryOffset + 3 + nicknameLength] = (byte) (trade.ivs[0] << 4 | trade.ivs[1]);
+ rom[entryOffset + 4 + nicknameLength] = (byte) (trade.ivs[2] << 4 | trade.ivs[3]);
+ rom[entryOffset + 5 + nicknameLength] = (byte) trade.item;
+ writeWord(entryOffset + 6 + nicknameLength, trade.otId);
+ if (romEntry.getValue("CanChangeTrainerText") > 0) {
+ writeFixedLengthString(trade.otName, entryOffset + 8 + nicknameLength, otLength);
+ }
+ // remove gender req
+ rom[entryOffset + 8 + nicknameLength + otLength] = 0;
+
+ }
+ }
+
+ @Override
+ public boolean hasDVs() {
+ return true;
+ }
+
+ @Override
+ public int generationOfPokemon() {
+ return 2;
+ }
+
+ @Override
+ public void removeEvosForPokemonPool() {
+ List<Pokemon> pokemonIncluded = this.mainPokemonList;
+ Set<Evolution> keepEvos = new HashSet<>();
+ for (Pokemon pk : pokes) {
+ if (pk != null) {
+ keepEvos.clear();
+ for (Evolution evol : pk.evolutionsFrom) {
+ if (pokemonIncluded.contains(evol.from) && pokemonIncluded.contains(evol.to)) {
+ keepEvos.add(evol);
+ } else {
+ evol.to.evolutionsTo.remove(evol);
+ }
+ }
+ pk.evolutionsFrom.retainAll(keepEvos);
+ }
+ }
+ }
+
+ private void writeEvosAndMovesLearnt(boolean writeEvos, Map<Integer, List<MoveLearnt>> movesets) {
+ // this assumes that the evo/attack pointers & data
+ // are at the end of the bank
+ // which, in every clean G/S/C rom supported, they are
+ // specify null to either argument to copy old values
+ int movesEvosStart = romEntry.getValue("PokemonMovesetsTableOffset");
+ int movesEvosBank = bankOf(movesEvosStart);
+ byte[] pointerTable = new byte[Gen2Constants.pokemonCount * 2];
+ int startOfNextBank;
+ if (isVietCrystal) {
+ startOfNextBank = 0x43E00; // fix for pokedex crash
+ }
+ else {
+ startOfNextBank = ((movesEvosStart / GBConstants.bankSize) + 1) * GBConstants.bankSize;
+ }
+ int dataBlockSize = startOfNextBank - (movesEvosStart + pointerTable.length);
+ int dataBlockOffset = movesEvosStart + pointerTable.length;
+ byte[] dataBlock = new byte[dataBlockSize];
+ int offsetInData = 0;
+ for (int i = 1; i <= Gen2Constants.pokemonCount; i++) {
+ // determine pointer
+ int oldDataOffset = calculateOffset(movesEvosBank, readWord(movesEvosStart + (i - 1) * 2));
+ int offsetStart = dataBlockOffset + offsetInData;
+ boolean evoWritten = false;
+ if (!writeEvos) {
+ // copy old
+ int evoOffset = oldDataOffset;
+ while (rom[evoOffset] != 0x00) {
+ int method = rom[evoOffset] & 0xFF;
+ int limiter = (method == 5) ? 4 : 3;
+ for (int b = 0; b < limiter; b++) {
+ dataBlock[offsetInData++] = rom[evoOffset++];
+ }
+ evoWritten = true;
+ }
+ } else {
+ for (Evolution evo : pokes[i].evolutionsFrom) {
+ // write evos
+ dataBlock[offsetInData++] = (byte) evo.type.toIndex(2);
+ if (evo.type == EvolutionType.LEVEL || evo.type == EvolutionType.STONE
+ || evo.type == EvolutionType.TRADE_ITEM) {
+ // simple types
+ dataBlock[offsetInData++] = (byte) evo.extraInfo;
+ } else if (evo.type == EvolutionType.TRADE) {
+ // non-item trade
+ dataBlock[offsetInData++] = (byte) 0xFF;
+ } else if (evo.type == EvolutionType.HAPPINESS) {
+ // cond 01
+ dataBlock[offsetInData++] = 0x01;
+ } else if (evo.type == EvolutionType.HAPPINESS_DAY) {
+ // cond 02
+ dataBlock[offsetInData++] = 0x02;
+ } else if (evo.type == EvolutionType.HAPPINESS_NIGHT) {
+ // cond 03
+ dataBlock[offsetInData++] = 0x03;
+ } else if (evo.type == EvolutionType.LEVEL_ATTACK_HIGHER) {
+ dataBlock[offsetInData++] = (byte) evo.extraInfo;
+ dataBlock[offsetInData++] = 0x01;
+ } else if (evo.type == EvolutionType.LEVEL_DEFENSE_HIGHER) {
+ dataBlock[offsetInData++] = (byte) evo.extraInfo;
+ dataBlock[offsetInData++] = 0x02;
+ } else if (evo.type == EvolutionType.LEVEL_ATK_DEF_SAME) {
+ dataBlock[offsetInData++] = (byte) evo.extraInfo;
+ dataBlock[offsetInData++] = 0x03;
+ }
+ dataBlock[offsetInData++] = (byte) evo.to.number;
+ evoWritten = true;
+ }
+ }
+ // can we reuse a terminator?
+ if (!evoWritten && offsetStart != dataBlockOffset) {
+ // reuse last pokemon's move terminator for our evos
+ offsetStart -= 1;
+ } else {
+ // write a terminator
+ dataBlock[offsetInData++] = 0x00;
+ }
+ // write table entry now that we're sure of its location
+ int pointerNow = makeGBPointer(offsetStart);
+ writeWord(pointerTable, (i - 1) * 2, pointerNow);
+ // moveset
+ if (movesets == null) {
+ // copy old
+ int movesOffset = oldDataOffset;
+ // move past evos
+ while (rom[movesOffset] != 0x00) {
+ int method = rom[movesOffset] & 0xFF;
+ movesOffset += (method == 5) ? 4 : 3;
+ }
+ movesOffset++;
+ // copy moves
+ while (rom[movesOffset] != 0x00) {
+ dataBlock[offsetInData++] = rom[movesOffset++];
+ dataBlock[offsetInData++] = rom[movesOffset++];
+ }
+ } else {
+ List<MoveLearnt> moves = movesets.get(pokes[i].number);
+ for (MoveLearnt ml : moves) {
+ dataBlock[offsetInData++] = (byte) ml.level;
+ dataBlock[offsetInData++] = (byte) ml.move;
+ }
+ }
+ // terminator
+ dataBlock[offsetInData++] = 0x00;
+ }
+ // write new data
+ System.arraycopy(pointerTable, 0, rom, movesEvosStart, pointerTable.length);
+ System.arraycopy(dataBlock, 0, rom, dataBlockOffset, dataBlock.length);
+ }
+
+ @Override
+ public boolean supportsFourStartingMoves() {
+ return (romEntry.getValue("SupportsFourStartingMoves") > 0);
+ }
+
+ @Override
+ public List<Integer> getGameBreakingMoves() {
+ // add OHKO moves for gen2 because x acc is still broken
+ return Gen2Constants.brokenMoves;
+ }
+
+ @Override
+ public List<Integer> getIllegalMoves() {
+ // 3 moves that crash the game when used by self or opponent
+ if (isVietCrystal) {
+ return Gen2Constants.illegalVietCrystalMoves;
+ }
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getFieldMoves() {
+ // cut, fly, surf, strength, flash,
+ // dig, teleport, whirlpool, waterfall,
+ // rock smash, headbutt, sweet scent
+ // not softboiled or milk drink
+ return Gen2Constants.fieldMoves;
+ }
+
+ @Override
+ public List<Integer> getEarlyRequiredHMMoves() {
+ // just cut
+ return Gen2Constants.earlyRequiredHMMoves;
+ }
+
+ @Override
+ public boolean isRomValid() {
+ return romEntry.expectedCRC32 == actualCRC32;
+ }
+
+ @Override
+ public BufferedImage getMascotImage() {
+ Pokemon mascot = randomPokemon();
+ while (mascot.number == Species.unown) {
+ // Unown is banned as handling it would add a ton of extra effort.
+ mascot = randomPokemon();
+ }
+
+ // Each Pokemon has a front and back pic with a bank and a pointer
+ // (3*2=6)
+ // There is no zero-entry.
+ int picPointer = romEntry.getValue("PicPointers") + (mascot.number - 1) * 6;
+ int picWidth = mascot.picDimensions & 0x0F;
+ int picHeight = (mascot.picDimensions >> 4) & 0x0F;
+
+ int picBank = (rom[picPointer] & 0xFF);
+ if (romEntry.isCrystal) {
+ // Crystal pic banks are offset by x36 for whatever reason.
+ picBank += 0x36;
+ } else {
+ // Hey, G/S are dumb too! Arbitrarily redirected bank numbers.
+ if (picBank == 0x13) {
+ picBank = 0x1F;
+ } else if (picBank == 0x14) {
+ picBank = 0x20;
+ } else if (picBank == 0x1F) {
+ picBank = 0x2E;
+ }
+ }
+ int picOffset = calculateOffset(picBank, readWord(picPointer + 1));
+
+ Gen2Decmp mscSprite = new Gen2Decmp(rom, picOffset, picWidth, picHeight);
+ int w = picWidth * 8;
+ int h = picHeight * 8;
+
+ // Palette?
+ // Two colors per Pokemon + two more for shiny, unlike pics there is a
+ // zero-entry.
+ // Black and white are left alone at the start and end of the palette.
+ int[] palette = new int[] { 0xFFFFFFFF, 0xFFAAAAAA, 0xFF666666, 0xFF000000 };
+ int paletteOffset = romEntry.getValue("PokemonPalettes") + mascot.number * 8;
+ if (random.nextInt(10) == 0) {
+ // Use shiny instead
+ paletteOffset += 4;
+ }
+ for (int i = 0; i < 2; i++) {
+ palette[i + 1] = GFXFunctions.conv16BitColorToARGB(readWord(paletteOffset + i * 2));
+ }
+
+ byte[] data = mscSprite.getFlattenedData();
+
+ BufferedImage bim = GFXFunctions.drawTiledImage(data, palette, w, h, 8);
+ GFXFunctions.pseudoTransparency(bim, palette[0]);
+
+ return bim;
+ }
+
+ @Override
+ public void writeCheckValueToROM(int value) {
+ if (romEntry.getValue("CheckValueOffset") > 0) {
+ int cvOffset = romEntry.getValue("CheckValueOffset");
+ for (int i = 0; i < 4; i++) {
+ rom[cvOffset + i] = (byte) ((value >> (3 - i) * 8) & 0xFF);
+ }
+ }
+ }
+}
diff --git a/src/com/pkrandom/romhandlers/Gen3RomHandler.java b/src/com/pkrandom/romhandlers/Gen3RomHandler.java new file mode 100755 index 0000000..838315d --- /dev/null +++ b/src/com/pkrandom/romhandlers/Gen3RomHandler.java @@ -0,0 +1,4473 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- Gen3RomHandler.java - randomizer handler for R/S/E/FR/LG. --*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.pkrandom.*; +import com.pkrandom.constants.*; +import com.pkrandom.exceptions.RandomizationException; +import com.pkrandom.exceptions.RandomizerIOException; +import com.pkrandom.pokemon.*; +import compressors.DSDecmp; + +public class Gen3RomHandler extends AbstractGBRomHandler { + + public static class Factory extends RomHandler.Factory { + + @Override + public Gen3RomHandler create(Random random, PrintStream logStream) { + return new Gen3RomHandler(random, logStream); + } + + public boolean isLoadable(String filename) { + long fileLength = new File(filename).length(); + if (fileLength > 32 * 1024 * 1024) { + return false; + } + byte[] loaded = loadFilePartial(filename, 0x100000); + // nope + return loaded.length != 0 && detectRomInner(loaded, (int) fileLength); + } + } + + public Gen3RomHandler(Random random) { + super(random, null); + } + + public Gen3RomHandler(Random random, PrintStream logStream) { + super(random, logStream); + } + + private static class RomEntry { + private String name; + private String romCode; + private String tableFile; + private int version; + private int romType; + private boolean copyStaticPokemon; + private Map<String, Integer> entries = new HashMap<>(); + private Map<String, int[]> arrayEntries = new HashMap<>(); + private Map<String, String> strings = new HashMap<>(); + private List<StaticPokemon> staticPokemon = new ArrayList<>(); + private List<StaticPokemon> roamingPokemon = new ArrayList<>(); + private List<TMOrMTTextEntry> tmmtTexts = new ArrayList<>(); + private Map<String, String> codeTweaks = new HashMap<String, String>(); + private long expectedCRC32 = -1; + + public RomEntry() { + + } + + public RomEntry(RomEntry toCopy) { + this.name = toCopy.name; + this.romCode = toCopy.romCode; + this.tableFile = toCopy.tableFile; + this.version = toCopy.version; + this.romType = toCopy.romType; + this.copyStaticPokemon = toCopy.copyStaticPokemon; + this.entries.putAll(toCopy.entries); + this.arrayEntries.putAll(toCopy.arrayEntries); + this.strings.putAll(toCopy.strings); + this.staticPokemon.addAll(toCopy.staticPokemon); + this.roamingPokemon.addAll(toCopy.roamingPokemon); + this.tmmtTexts.addAll(toCopy.tmmtTexts); + this.codeTweaks.putAll(toCopy.codeTweaks); + this.expectedCRC32 = toCopy.expectedCRC32; + } + + private int getValue(String key) { + if (!entries.containsKey(key)) { + entries.put(key, 0); + } + return entries.get(key); + } + + private String getString(String key) { + if (!strings.containsKey(key)) { + strings.put(key, ""); + } + return strings.get(key); + } + } + + private static class TMOrMTTextEntry { + private int number; + private int mapBank, mapNumber; + private int personNum; + private int offsetInScript; + private int actualOffset; + private String template; + private boolean isMoveTutor; + } + + private static List<RomEntry> roms; + + static { + loadROMInfo(); + } + + private static void loadROMInfo() { + roms = new ArrayList<>(); + RomEntry current = null; + try { + Scanner sc = new Scanner(FileFunctions.openConfig("gen3_offsets.ini"), "UTF-8"); + while (sc.hasNextLine()) { + String q = sc.nextLine().trim(); + if (q.contains("//")) { + q = q.substring(0, q.indexOf("//")).trim(); + } + if (!q.isEmpty()) { + if (q.startsWith("[") && q.endsWith("]")) { + // New rom + current = new RomEntry(); + current.name = q.substring(1, q.length() - 1); + roms.add(current); + } else { + String[] r = q.split("=", 2); + if (r.length == 1) { + System.err.println("invalid entry " + q); + continue; + } + if (r[1].endsWith("\r\n")) { + r[1] = r[1].substring(0, r[1].length() - 2); + } + r[1] = r[1].trim(); + // Static Pokemon? + if (r[0].equals("StaticPokemon{}")) { + current.staticPokemon.add(parseStaticPokemon(r[1])); + } else if (r[0].equals("RoamingPokemon{}")) { + current.roamingPokemon.add(parseStaticPokemon(r[1])); + } else if (r[0].equals("TMText[]")) { + if (r[1].startsWith("[") && r[1].endsWith("]")) { + String[] parts = r[1].substring(1, r[1].length() - 1).split(",", 6); + TMOrMTTextEntry tte = new TMOrMTTextEntry(); + tte.number = parseRIInt(parts[0]); + tte.mapBank = parseRIInt(parts[1]); + tte.mapNumber = parseRIInt(parts[2]); + tte.personNum = parseRIInt(parts[3]); + tte.offsetInScript = parseRIInt(parts[4]); + tte.template = parts[5]; + tte.isMoveTutor = false; + current.tmmtTexts.add(tte); + } + } else if (r[0].equals("MoveTutorText[]")) { + if (r[1].startsWith("[") && r[1].endsWith("]")) { + String[] parts = r[1].substring(1, r[1].length() - 1).split(",", 6); + TMOrMTTextEntry tte = new TMOrMTTextEntry(); + tte.number = parseRIInt(parts[0]); + tte.mapBank = parseRIInt(parts[1]); + tte.mapNumber = parseRIInt(parts[2]); + tte.personNum = parseRIInt(parts[3]); + tte.offsetInScript = parseRIInt(parts[4]); + tte.template = parts[5]; + tte.isMoveTutor = true; + current.tmmtTexts.add(tte); + } + } else if (r[0].equals("Game")) { + current.romCode = r[1]; + } else if (r[0].equals("Version")) { + current.version = parseRIInt(r[1]); + } else if (r[0].equals("Type")) { + if (r[1].equalsIgnoreCase("Ruby")) { + current.romType = Gen3Constants.RomType_Ruby; + } else if (r[1].equalsIgnoreCase("Sapp")) { + current.romType = Gen3Constants.RomType_Sapp; + } else if (r[1].equalsIgnoreCase("Em")) { + current.romType = Gen3Constants.RomType_Em; + } else if (r[1].equalsIgnoreCase("FRLG")) { + current.romType = Gen3Constants.RomType_FRLG; + } else { + System.err.println("unrecognised rom type: " + r[1]); + } + } else if (r[0].equals("TableFile")) { + current.tableFile = r[1]; + } else if (r[0].equals("CopyStaticPokemon")) { + int csp = parseRIInt(r[1]); + current.copyStaticPokemon = (csp > 0); + } else if (r[0].equals("CRC32")) { + current.expectedCRC32 = parseRILong("0x" + r[1]); + } else if (r[0].endsWith("Tweak")) { + current.codeTweaks.put(r[0], r[1]); + } else if (r[0].equals("CopyFrom")) { + for (RomEntry otherEntry : roms) { + if (r[1].equalsIgnoreCase(otherEntry.name)) { + // copy from here + current.arrayEntries.putAll(otherEntry.arrayEntries); + current.entries.putAll(otherEntry.entries); + current.strings.putAll(otherEntry.strings); + boolean cTT = (current.getValue("CopyTMText") == 1); + if (current.copyStaticPokemon) { + current.staticPokemon.addAll(otherEntry.staticPokemon); + current.roamingPokemon.addAll(otherEntry.roamingPokemon); + current.entries.put("StaticPokemonSupport", 1); + } else { + current.entries.put("StaticPokemonSupport", 0); + } + if (cTT) { + current.tmmtTexts.addAll(otherEntry.tmmtTexts); + } + current.tableFile = otherEntry.tableFile; + } + } + } else if (r[0].endsWith("Locator") || r[0].endsWith("Prefix")) { + current.strings.put(r[0], r[1]); + } else { + if (r[1].startsWith("[") && r[1].endsWith("]")) { + String[] offsets = r[1].substring(1, r[1].length() - 1).split(","); + if (offsets.length == 1 && offsets[0].trim().isEmpty()) { + current.arrayEntries.put(r[0], new int[0]); + } else { + int[] offs = new int[offsets.length]; + int c = 0; + for (String off : offsets) { + offs[c++] = parseRIInt(off); + } + current.arrayEntries.put(r[0], offs); + } + } else { + int offs = parseRIInt(r[1]); + current.entries.put(r[0], offs); + } + } + } + } + } + sc.close(); + } catch (FileNotFoundException e) { + System.err.println("File not found!"); + } + + } + + private static int parseRIInt(String off) { + int radix = 10; + off = off.trim().toLowerCase(); + if (off.startsWith("0x") || off.startsWith("&h")) { + radix = 16; + off = off.substring(2); + } + try { + return Integer.parseInt(off, radix); + } catch (NumberFormatException ex) { + System.err.println("invalid base " + radix + "number " + off); + return 0; + } + } + + private static long parseRILong(String off) { + int radix = 10; + off = off.trim().toLowerCase(); + if (off.startsWith("0x") || off.startsWith("&h")) { + radix = 16; + off = off.substring(2); + } + try { + return Long.parseLong(off, radix); + } catch (NumberFormatException ex) { + System.err.println("invalid base " + radix + "number " + off); + return 0; + } + } + + private static StaticPokemon parseStaticPokemon(String staticPokemonString) { + StaticPokemon sp = new StaticPokemon(); + String pattern = "[A-z]+=\\[(0x[0-9a-fA-F]+,?\\s?)+]"; + Pattern r = Pattern.compile(pattern); + Matcher m = r.matcher(staticPokemonString); + while (m.find()) { + String[] segments = m.group().split("="); + String[] romOffsets = segments[1].substring(1, segments[1].length() - 1).split(","); + int[] offsets = new int [romOffsets.length]; + for (int i = 0; i < offsets.length; i++) { + offsets[i] = parseRIInt(romOffsets[i]); + } + switch (segments[0]) { + case "Species": + sp.speciesOffsets = offsets; + break; + case "Level": + sp.levelOffsets = offsets; + break; + } + } + return sp; + } + + private void loadTextTable(String filename) { + try { + Scanner sc = new Scanner(FileFunctions.openConfig(filename + ".tbl"), "UTF-8"); + while (sc.hasNextLine()) { + String q = sc.nextLine(); + if (!q.trim().isEmpty()) { + String[] r = q.split("=", 2); + if (r[1].endsWith("\r\n")) { + r[1] = r[1].substring(0, r[1].length() - 2); + } + tb[Integer.parseInt(r[0], 16)] = r[1]; + d.put(r[1], (byte) Integer.parseInt(r[0], 16)); + } + } + sc.close(); + } catch (FileNotFoundException e) { + System.err.println("File not found!"); + } + + } + + // This ROM's data + private Pokemon[] pokes, pokesInternal; + private List<Pokemon> pokemonList; + private int numRealPokemon; + private Move[] moves; + private boolean jamboMovesetHack; + private RomEntry romEntry; + private boolean havePatchedObedience; + private String[] tb; + public Map<String, Byte> d; + private String[] abilityNames; + private String[] itemNames; + private boolean mapLoadingDone; + private List<Integer> itemOffs; + private String[][] mapNames; + private boolean isRomHack; + private int[] internalToPokedex, pokedexToInternal; + private int pokedexCount; + private String[] pokeNames; + private ItemList allowedItems, nonBadItems; + private int pickupItemsTableOffset; + private long actualCRC32; + private boolean effectivenessUpdated; + + @Override + public boolean detectRom(byte[] rom) { + return detectRomInner(rom, rom.length); + } + + private static boolean detectRomInner(byte[] rom, int romSize) { + if (romSize != Gen3Constants.size8M && romSize != Gen3Constants.size16M && romSize != Gen3Constants.size32M) { + return false; // size check + } + // Special case for Emerald unofficial translation + if (romName(rom, Gen3Constants.unofficialEmeraldROMName)) { + // give it a rom code so it can be detected + rom[Gen3Constants.romCodeOffset] = 'B'; + rom[Gen3Constants.romCodeOffset + 1] = 'P'; + rom[Gen3Constants.romCodeOffset + 2] = 'E'; + rom[Gen3Constants.romCodeOffset + 3] = 'T'; + rom[Gen3Constants.headerChecksumOffset] = 0x66; + } + // Wild Pokemon header + if (find(rom, Gen3Constants.wildPokemonPointerPrefix) == -1) { + return false; + } + // Map Banks header + if (find(rom, Gen3Constants.mapBanksPointerPrefix) == -1) { + return false; + } + // Pokedex Order header + if (findMultiple(rom, Gen3Constants.pokedexOrderPointerPrefix).size() != 3) { + return false; + } + for (RomEntry re : roms) { + if (romCode(rom, re.romCode) && (rom[Gen3Constants.romVersionOffset] & 0xFF) == re.version) { + return true; // match + } + } + return false; // GBA rom we don't support yet + } + + @Override + public void loadedRom() { + for (RomEntry re : roms) { + if (romCode(rom, re.romCode) && (rom[0xBC] & 0xFF) == re.version) { + romEntry = new RomEntry(re); // clone so we can modify + break; + } + } + + tb = new String[256]; + d = new HashMap<>(); + isRomHack = false; + jamboMovesetHack = false; + + // Pokemon count stuff, needs to be available first + List<Integer> pokedexOrderPrefixes = findMultiple(rom, Gen3Constants.pokedexOrderPointerPrefix); + romEntry.entries.put("PokedexOrder", readPointer(pokedexOrderPrefixes.get(1) + 16)); + + // Pokemon names offset + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + int baseNomOffset = find(rom, Gen3Constants.rsPokemonNamesPointerSuffix); + romEntry.entries.put("PokemonNames", readPointer(baseNomOffset - 4)); + romEntry.entries.put( + "FrontSprites", + readPointer(findPointerPrefixAndSuffix(Gen3Constants.rsFrontSpritesPointerPrefix, + Gen3Constants.rsFrontSpritesPointerSuffix))); + romEntry.entries.put( + "PokemonPalettes", + readPointer(findPointerPrefixAndSuffix(Gen3Constants.rsPokemonPalettesPointerPrefix, + Gen3Constants.rsPokemonPalettesPointerSuffix))); + } else { + romEntry.entries.put("PokemonNames", readPointer(Gen3Constants.efrlgPokemonNamesPointer)); + romEntry.entries.put("MoveNames", readPointer(Gen3Constants.efrlgMoveNamesPointer)); + romEntry.entries.put("AbilityNames", readPointer(Gen3Constants.efrlgAbilityNamesPointer)); + romEntry.entries.put("ItemData", readPointer(Gen3Constants.efrlgItemDataPointer)); + romEntry.entries.put("MoveData", readPointer(Gen3Constants.efrlgMoveDataPointer)); + romEntry.entries.put("PokemonStats", readPointer(Gen3Constants.efrlgPokemonStatsPointer)); + romEntry.entries.put("FrontSprites", readPointer(Gen3Constants.efrlgFrontSpritesPointer)); + romEntry.entries.put("PokemonPalettes", readPointer(Gen3Constants.efrlgPokemonPalettesPointer)); + romEntry.entries.put("MoveTutorCompatibility", + romEntry.getValue("MoveTutorData") + romEntry.getValue("MoveTutorMoves") * 2); + } + + loadTextTable(romEntry.tableFile); + + if (romEntry.romCode.equals("BPRE") && romEntry.version == 0) { + basicBPRE10HackSupport(); + } + + loadPokemonNames(); + loadPokedex(); + loadPokemonStats(); + constructPokemonList(); + populateEvolutions(); + loadMoves(); + + // Get wild Pokemon offset + int baseWPOffset = findMultiple(rom, Gen3Constants.wildPokemonPointerPrefix).get(0); + romEntry.entries.put("WildPokemon", readPointer(baseWPOffset + 12)); + + // map banks + int baseMapsOffset = findMultiple(rom, Gen3Constants.mapBanksPointerPrefix).get(0); + romEntry.entries.put("MapHeaders", readPointer(baseMapsOffset + 12)); + this.determineMapBankSizes(); + + // map labels + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + int baseMLOffset = find(rom, Gen3Constants.frlgMapLabelsPointerPrefix); + romEntry.entries.put("MapLabels", readPointer(baseMLOffset + 12)); + } else { + int baseMLOffset = find(rom, Gen3Constants.rseMapLabelsPointerPrefix); + romEntry.entries.put("MapLabels", readPointer(baseMLOffset + 12)); + } + + mapLoadingDone = false; + loadAbilityNames(); + loadItemNames(); + + allowedItems = Gen3Constants.allowedItems.copy(); + nonBadItems = Gen3Constants.getNonBadItems(romEntry.romType).copy(); + + actualCRC32 = FileFunctions.getCRC32(rom); + } + + private int findPointerPrefixAndSuffix(String prefix, String suffix) { + if (prefix.length() % 2 != 0 || suffix.length() % 2 != 0) { + return -1; + } + byte[] searchPref = new byte[prefix.length() / 2]; + for (int i = 0; i < searchPref.length; i++) { + searchPref[i] = (byte) Integer.parseInt(prefix.substring(i * 2, i * 2 + 2), 16); + } + byte[] searchSuff = new byte[suffix.length() / 2]; + for (int i = 0; i < searchSuff.length; i++) { + searchSuff[i] = (byte) Integer.parseInt(suffix.substring(i * 2, i * 2 + 2), 16); + } + if (searchPref.length >= searchSuff.length) { + // Prefix first + List<Integer> offsets = RomFunctions.search(rom, searchPref); + if (offsets.size() == 0) { + return -1; + } + for (int prefOffset : offsets) { + if (prefOffset + 4 + searchSuff.length > rom.length) { + continue; // not enough room for this to be valid + } + int ptrOffset = prefOffset + searchPref.length; + int pointerValue = readPointer(ptrOffset); + if (pointerValue < 0 || pointerValue >= rom.length) { + // Not a valid pointer + continue; + } + boolean suffixMatch = true; + for (int i = 0; i < searchSuff.length; i++) { + if (rom[ptrOffset + 4 + i] != searchSuff[i]) { + suffixMatch = false; + break; + } + } + if (suffixMatch) { + return ptrOffset; + } + } + return -1; // No match + } else { + // Suffix first + List<Integer> offsets = RomFunctions.search(rom, searchSuff); + if (offsets.size() == 0) { + return -1; + } + for (int suffOffset : offsets) { + if (suffOffset - 4 - searchPref.length < 0) { + continue; // not enough room for this to be valid + } + int ptrOffset = suffOffset - 4; + int pointerValue = readPointer(ptrOffset); + if (pointerValue < 0 || pointerValue >= rom.length) { + // Not a valid pointer + continue; + } + boolean prefixMatch = true; + for (int i = 0; i < searchPref.length; i++) { + if (rom[ptrOffset - searchPref.length + i] != searchPref[i]) { + prefixMatch = false; + break; + } + } + if (prefixMatch) { + return ptrOffset; + } + } + return -1; // No match + } + } + + private void basicBPRE10HackSupport() { + if (basicBPRE10HackDetection()) { + this.isRomHack = true; + // NUMBER OF POKEMON DETECTION + + // this is the most annoying bit + // we'll try to get it from the pokemon names, + // and sanity check it using other things + // this of course means we can't support + // any hack with extended length names + + int iPokemonCount = 0; + int namesOffset = romEntry.getValue("PokemonNames"); + int nameLen = romEntry.getValue("PokemonNameLength"); + while (true) { + int nameOffset = namesOffset + (iPokemonCount + 1) * nameLen; + int nameStrLen = lengthOfStringAt(nameOffset); + if (nameStrLen > 0 && nameStrLen < nameLen && rom[nameOffset] != 0) { + iPokemonCount++; + } else { + break; + } + } + + // Is there an unused egg slot at the end? + String lastName = readVariableLengthString(namesOffset + iPokemonCount * nameLen); + if (lastName.equals("?") || lastName.equals("-")) { + iPokemonCount--; + } + + // Jambo's Moves Learnt table hack? + // need to check this before using moveset pointers + int movesetsTable; + if (readLong(0x3EB20) == 0x47084918) { + // Hack applied, adjust accordingly + int firstRoutinePtr = readPointer(0x3EB84); + movesetsTable = readPointer(firstRoutinePtr + 75); + jamboMovesetHack = true; + } else { + movesetsTable = readPointer(0x3EA7C); + jamboMovesetHack = false; + } + + // secondary check: moveset pointers + // if a slot has an invalid moveset pointer, it's not a real slot + // Before that, grab the moveset table from a known pointer to it. + romEntry.entries.put("PokemonMovesets", movesetsTable); + while (iPokemonCount >= 0) { + int movesetPtr = readPointer(movesetsTable + iPokemonCount * 4); + if (movesetPtr < 0 || movesetPtr >= rom.length) { + iPokemonCount--; + } else { + break; + } + } + + // sanity check: pokedex order + // pokedex entries have to be within 0-1023 + // even after extending the dex + // (at least with conventional methods) + // so if we run into an invalid one + // then we can cut off the count + int pdOffset = romEntry.getValue("PokedexOrder"); + for (int i = 1; i <= iPokemonCount; i++) { + int pdEntry = readWord(pdOffset + (i - 1) * 2); + if (pdEntry > 1023) { + iPokemonCount = i - 1; + break; + } + } + + // write new pokemon count + romEntry.entries.put("PokemonCount", iPokemonCount); + + // update some key offsets from known pointers + romEntry.entries.put("PokemonTMHMCompat", readPointer(0x43C68)); + romEntry.entries.put("PokemonEvolutions", readPointer(0x42F6C)); + romEntry.entries.put("MoveTutorCompatibility", readPointer(0x120C30)); + int descsTable = readPointer(0xE5440); + romEntry.entries.put("MoveDescriptions", descsTable); + int trainersTable = readPointer(0xFC00); + romEntry.entries.put("TrainerData", trainersTable); + + // try to detect number of moves using the descriptions + int moveCount = 0; + while (true) { + int descPointer = readPointer(descsTable + (moveCount) * 4); + if (descPointer >= 0 && descPointer < rom.length) { + int descStrLen = lengthOfStringAt(descPointer); + if (descStrLen > 0 && descStrLen < 100) { + // okay, this does seem fine + moveCount++; + continue; + } + } + break; + } + romEntry.entries.put("MoveCount", moveCount); + + // attempt to detect number of trainers using various tells + int trainerCount = 1; + int tEntryLen = romEntry.getValue("TrainerEntrySize"); + int tNameLen = romEntry.getValue("TrainerNameLength"); + while (true) { + int trOffset = trainersTable + tEntryLen * trainerCount; + int pokeDataType = rom[trOffset] & 0xFF; + if (pokeDataType >= 4) { + // only allowed 0-3 + break; + } + int numPokes = rom[trOffset + (tEntryLen - 8)] & 0xFF; + if (numPokes == 0 || numPokes > 6) { + break; + } + int pointerToPokes = readPointer(trOffset + (tEntryLen - 4)); + if (pointerToPokes < 0 || pointerToPokes >= rom.length) { + break; + } + int nameLength = lengthOfStringAt(trOffset + 4); + if (nameLength >= tNameLen) { + break; + } + // found a valid trainer entry, recognize it + trainerCount++; + } + romEntry.entries.put("TrainerCount", trainerCount); + } + + } + + private boolean basicBPRE10HackDetection() { + if (rom.length != Gen3Constants.size16M) { + return true; + } + long csum = FileFunctions.getCRC32(rom); + return csum != 3716707868L; + } + + @Override + public void savingRom() { + savePokemonStats(); + saveMoves(); + } + + private void loadPokedex() { + int pdOffset = romEntry.getValue("PokedexOrder"); + int numInternalPokes = romEntry.getValue("PokemonCount"); + int maxPokedex = 0; + internalToPokedex = new int[numInternalPokes + 1]; + pokedexToInternal = new int[numInternalPokes + 1]; + for (int i = 1; i <= numInternalPokes; i++) { + int dexEntry = readWord(rom, pdOffset + (i - 1) * 2); + if (dexEntry != 0) { + internalToPokedex[i] = dexEntry; + // take the first pokemon only for each dex entry + if (pokedexToInternal[dexEntry] == 0) { + pokedexToInternal[dexEntry] = i; + } + maxPokedex = Math.max(maxPokedex, dexEntry); + } + } + if (maxPokedex == Gen3Constants.unhackedMaxPokedex) { + // see if the slots between johto and hoenn are in use + // old rom hacks use them instead of expanding pokes + int offs = romEntry.getValue("PokemonStats"); + int usedSlots = 0; + for (int i = 0; i < Gen3Constants.unhackedMaxPokedex - Gen3Constants.unhackedRealPokedex; i++) { + int pokeSlot = Gen3Constants.hoennPokesStart + i; + int pokeOffs = offs + pokeSlot * Gen3Constants.baseStatsEntrySize; + String lowerName = pokeNames[pokeSlot].toLowerCase(); + if (!this.matches(rom, pokeOffs, Gen3Constants.emptyPokemonSig) && !lowerName.contains("unused") + && !lowerName.equals("?") && !lowerName.equals("-")) { + usedSlots++; + pokedexToInternal[Gen3Constants.unhackedRealPokedex + usedSlots] = pokeSlot; + internalToPokedex[pokeSlot] = Gen3Constants.unhackedRealPokedex + usedSlots; + } else { + internalToPokedex[pokeSlot] = 0; + } + } + // remove the fake extra slots + for (int i = usedSlots + 1; i <= Gen3Constants.unhackedMaxPokedex - Gen3Constants.unhackedRealPokedex; i++) { + pokedexToInternal[Gen3Constants.unhackedRealPokedex + i] = 0; + } + // if any slots were used at all, this is a rom hack + if (usedSlots > 0) { + this.isRomHack = true; + } + this.pokedexCount = Gen3Constants.unhackedRealPokedex + usedSlots; + } else { + this.isRomHack = true; + this.pokedexCount = maxPokedex; + } + + } + + private void constructPokemonList() { + if (!this.isRomHack) { + // simple behavior: all pokes in the dex are valid + pokemonList = Arrays.asList(pokes); + } else { + // only include "valid" pokes + pokemonList = new ArrayList<>(); + pokemonList.add(null); + for (int i = 1; i < pokes.length; i++) { + Pokemon pk = pokes[i]; + if (pk != null) { + String lowerName = pk.name.toLowerCase(); + if (!lowerName.contains("unused") && !lowerName.equals("?")) { + pokemonList.add(pk); + } + } + } + } + numRealPokemon = pokemonList.size() - 1; + + } + + private void loadPokemonStats() { + pokes = new Pokemon[this.pokedexCount + 1]; + int numInternalPokes = romEntry.getValue("PokemonCount"); + pokesInternal = new Pokemon[numInternalPokes + 1]; + int offs = romEntry.getValue("PokemonStats"); + for (int i = 1; i <= numInternalPokes; i++) { + Pokemon pk = new Pokemon(); + pk.name = pokeNames[i]; + pk.number = internalToPokedex[i]; + if (pk.number != 0) { + pokes[pk.number] = pk; + } + pokesInternal[i] = pk; + int pkoffs = offs + i * Gen3Constants.baseStatsEntrySize; + loadBasicPokeStats(pk, pkoffs); + } + + // In these games, the alternate formes of Deoxys have hardcoded stats that are used 99% of the time; + // the only times these hardcoded stats are ignored are during Link Battles. Since not many people + // are using the randomizer to battle against others, let's just always use these stats. + if (romEntry.romType == Gen3Constants.RomType_FRLG || romEntry.romType == Gen3Constants.RomType_Em) { + String deoxysStatPrefix = romEntry.strings.get("DeoxysStatPrefix"); + int offset = find(deoxysStatPrefix); + if (offset > 0) { + offset += deoxysStatPrefix.length() / 2; // because it was a prefix + Pokemon deoxys = pokes[Species.deoxys]; + deoxys.hp = readWord(offset); + deoxys.attack = readWord(offset + 2); + deoxys.defense = readWord(offset + 4); + deoxys.speed = readWord(offset + 6); + deoxys.spatk = readWord(offset + 8); + deoxys.spdef = readWord(offset + 10); + } + } + } + + private void savePokemonStats() { + // Write pokemon names & stats + int offs = romEntry.getValue("PokemonNames"); + int nameLen = romEntry.getValue("PokemonNameLength"); + int offs2 = romEntry.getValue("PokemonStats"); + int numInternalPokes = romEntry.getValue("PokemonCount"); + for (int i = 1; i <= numInternalPokes; i++) { + Pokemon pk = pokesInternal[i]; + int stringOffset = offs + i * nameLen; + writeFixedLengthString(pk.name, stringOffset, nameLen); + saveBasicPokeStats(pk, offs2 + i * Gen3Constants.baseStatsEntrySize); + } + + // Make sure to write to the hardcoded Deoxys stat location, since otherwise it will just have vanilla + // stats no matter what settings the user selected. + if (romEntry.romType == Gen3Constants.RomType_FRLG || romEntry.romType == Gen3Constants.RomType_Em) { + String deoxysStatPrefix = romEntry.strings.get("DeoxysStatPrefix"); + int offset = find(deoxysStatPrefix); + if (offset > 0) { + offset += deoxysStatPrefix.length() / 2; // because it was a prefix + Pokemon deoxys = pokes[Species.deoxys]; + writeWord(offset, deoxys.hp); + writeWord(offset + 2, deoxys.attack); + writeWord(offset + 4, deoxys.defense); + writeWord(offset + 6, deoxys.speed); + writeWord(offset + 8, deoxys.spatk); + writeWord(offset + 10, deoxys.spdef); + } + } + + writeEvolutions(); + } + + private void loadMoves() { + int moveCount = romEntry.getValue("MoveCount"); + moves = new Move[moveCount + 1]; + int offs = romEntry.getValue("MoveData"); + int nameoffs = romEntry.getValue("MoveNames"); + int namelen = romEntry.getValue("MoveNameLength"); + for (int i = 1; i <= moveCount; i++) { + moves[i] = new Move(); + moves[i].name = readFixedLengthString(nameoffs + i * namelen, namelen); + moves[i].number = i; + moves[i].internalId = i; + moves[i].effectIndex = rom[offs + i * 0xC] & 0xFF; + moves[i].hitratio = ((rom[offs + i * 0xC + 3] & 0xFF)); + moves[i].power = rom[offs + i * 0xC + 1] & 0xFF; + moves[i].pp = rom[offs + i * 0xC + 4] & 0xFF; + moves[i].type = Gen3Constants.typeTable[rom[offs + i * 0xC + 2]]; + moves[i].target = rom[offs + i * 0xC + 6] & 0xFF; + moves[i].category = GBConstants.physicalTypes.contains(moves[i].type) ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL; + if (moves[i].power == 0 && !GlobalConstants.noPowerNonStatusMoves.contains(i)) { + moves[i].category = MoveCategory.STATUS; + } + moves[i].priority = rom[offs + i * 0xC + 7]; + int flags = rom[offs + i * 0xC + 8] & 0xFF; + moves[i].makesContact = (flags & 1) != 0; + moves[i].isSoundMove = Gen3Constants.soundMoves.contains(moves[i].number); + + if (i == Moves.swift) { + perfectAccuracy = (int)moves[i].hitratio; + } + + if (GlobalConstants.normalMultihitMoves.contains(i)) { + moves[i].hitCount = 3; + } else if (GlobalConstants.doubleHitMoves.contains(i)) { + moves[i].hitCount = 2; + } else if (i == Moves.tripleKick) { + moves[i].hitCount = 2.71; // this assumes the first hit lands + } + + int secondaryEffectChance = rom[offs + i * 0xC + 5] & 0xFF; + loadStatChangesFromEffect(moves[i], secondaryEffectChance); + loadStatusFromEffect(moves[i], secondaryEffectChance); + loadMiscMoveInfoFromEffect(moves[i], secondaryEffectChance); + } + } + + private void loadStatChangesFromEffect(Move move, int secondaryEffectChance) { + switch (move.effectIndex) { + case Gen3Constants.noDamageAtkPlusOneEffect: + case Gen3Constants.noDamageDefPlusOneEffect: + case Gen3Constants.noDamageSpAtkPlusOneEffect: + case Gen3Constants.noDamageEvasionPlusOneEffect: + case Gen3Constants.noDamageAtkMinusOneEffect: + case Gen3Constants.noDamageDefMinusOneEffect: + case Gen3Constants.noDamageSpeMinusOneEffect: + case Gen3Constants.noDamageAccuracyMinusOneEffect: + case Gen3Constants.noDamageEvasionMinusOneEffect: + case Gen3Constants.noDamageAtkPlusTwoEffect: + case Gen3Constants.noDamageDefPlusTwoEffect: + case Gen3Constants.noDamageSpePlusTwoEffect: + case Gen3Constants.noDamageSpAtkPlusTwoEffect: + case Gen3Constants.noDamageSpDefPlusTwoEffect: + case Gen3Constants.noDamageAtkMinusTwoEffect: + case Gen3Constants.noDamageDefMinusTwoEffect: + case Gen3Constants.noDamageSpeMinusTwoEffect: + case Gen3Constants.noDamageSpDefMinusTwoEffect: + case Gen3Constants.minimizeEffect: + case Gen3Constants.swaggerEffect: + case Gen3Constants.defenseCurlEffect: + case Gen3Constants.flatterEffect: + case Gen3Constants.chargeEffect: + case Gen3Constants.noDamageAtkAndDefMinusOneEffect: + case Gen3Constants.noDamageDefAndSpDefPlusOneEffect: + case Gen3Constants.noDamageAtkAndDefPlusOneEffect: + case Gen3Constants.noDamageSpAtkAndSpDefPlusOneEffect: + case Gen3Constants.noDamageAtkAndSpePlusOneEffect: + if (move.target == 16) { + move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER; + } else { + move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET; + } + break; + + case Gen3Constants.damageAtkMinusOneEffect: + case Gen3Constants.damageDefMinusOneEffect: + case Gen3Constants.damageSpeMinusOneEffect: + case Gen3Constants.damageSpAtkMinusOneEffect: + case Gen3Constants.damageSpDefMinusOneEffect: + case Gen3Constants.damageAccuracyMinusOneEffect: + move.statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET; + break; + + case Gen3Constants.damageUserDefPlusOneEffect: + case Gen3Constants.damageUserAtkPlusOneEffect: + case Gen3Constants.damageUserAllPlusOneEffect: + case Gen3Constants.damageUserAtkAndDefMinusOneEffect: + case Gen3Constants.damageUserSpAtkMinusTwoEffect: + move.statChangeMoveType = StatChangeMoveType.DAMAGE_USER; + break; + + default: + // Move does not have a stat-changing effect + return; + } + + switch (move.effectIndex) { + case Gen3Constants.noDamageAtkPlusOneEffect: + case Gen3Constants.damageUserAtkPlusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.noDamageDefPlusOneEffect: + case Gen3Constants.damageUserDefPlusOneEffect: + case Gen3Constants.defenseCurlEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.noDamageSpAtkPlusOneEffect: + case Gen3Constants.flatterEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.noDamageEvasionPlusOneEffect: + case Gen3Constants.minimizeEffect: + move.statChanges[0].type = StatChangeType.EVASION; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.noDamageAtkMinusOneEffect: + case Gen3Constants.damageAtkMinusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageDefMinusOneEffect: + case Gen3Constants.damageDefMinusOneEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageSpeMinusOneEffect: + case Gen3Constants.damageSpeMinusOneEffect: + move.statChanges[0].type = StatChangeType.SPEED; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageAccuracyMinusOneEffect: + case Gen3Constants.damageAccuracyMinusOneEffect: + move.statChanges[0].type = StatChangeType.ACCURACY; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageEvasionMinusOneEffect: + move.statChanges[0].type = StatChangeType.EVASION; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.noDamageAtkPlusTwoEffect: + case Gen3Constants.swaggerEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageDefPlusTwoEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageSpePlusTwoEffect: + move.statChanges[0].type = StatChangeType.SPEED; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageSpAtkPlusTwoEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageSpDefPlusTwoEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[0].stages = 2; + break; + case Gen3Constants.noDamageAtkMinusTwoEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.noDamageDefMinusTwoEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.noDamageSpeMinusTwoEffect: + move.statChanges[0].type = StatChangeType.SPEED; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.noDamageSpDefMinusTwoEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.damageSpAtkMinusOneEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.damageSpDefMinusOneEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[0].stages = -1; + break; + case Gen3Constants.damageUserAllPlusOneEffect: + move.statChanges[0].type = StatChangeType.ALL; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.chargeEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[0].stages = 1; + break; + case Gen3Constants.damageUserAtkAndDefMinusOneEffect: + case Gen3Constants.noDamageAtkAndDefMinusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = -1; + move.statChanges[1].type = StatChangeType.DEFENSE; + move.statChanges[1].stages = -1; + break; + case Gen3Constants.damageUserSpAtkMinusTwoEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = -2; + break; + case Gen3Constants.noDamageDefAndSpDefPlusOneEffect: + move.statChanges[0].type = StatChangeType.DEFENSE; + move.statChanges[0].stages = 1; + move.statChanges[1].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[1].stages = 1; + break; + case Gen3Constants.noDamageAtkAndDefPlusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = 1; + move.statChanges[1].type = StatChangeType.DEFENSE; + move.statChanges[1].stages = 1; + break; + case Gen3Constants.noDamageSpAtkAndSpDefPlusOneEffect: + move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; + move.statChanges[0].stages = 1; + move.statChanges[1].type = StatChangeType.SPECIAL_DEFENSE; + move.statChanges[1].stages = 1; + break; + case Gen3Constants.noDamageAtkAndSpePlusOneEffect: + move.statChanges[0].type = StatChangeType.ATTACK; + move.statChanges[0].stages = 1; + move.statChanges[1].type = StatChangeType.SPEED; + move.statChanges[1].stages = 1; + break; + } + + if (move.statChangeMoveType == StatChangeMoveType.DAMAGE_TARGET || move.statChangeMoveType == StatChangeMoveType.DAMAGE_USER) { + for (int i = 0; i < move.statChanges.length; i++) { + if (move.statChanges[i].type != StatChangeType.NONE) { + move.statChanges[i].percentChance = secondaryEffectChance; + if (move.statChanges[i].percentChance == 0.0) { + move.statChanges[i].percentChance = 100.0; + } + } + } + } + } + + private void loadStatusFromEffect(Move move, int secondaryEffectChance) { + if (move.number == Moves.bounce) { + // GF hardcoded this, so we have to as well + move.statusMoveType = StatusMoveType.DAMAGE; + move.statusType = StatusType.PARALYZE; + move.statusPercentChance = secondaryEffectChance; + return; + } + + switch (move.effectIndex) { + case Gen3Constants.noDamageSleepEffect: + case Gen3Constants.toxicEffect: + case Gen3Constants.noDamageConfusionEffect: + case Gen3Constants.noDamagePoisonEffect: + case Gen3Constants.noDamageParalyzeEffect: + case Gen3Constants.noDamageBurnEffect: + case Gen3Constants.swaggerEffect: + case Gen3Constants.flatterEffect: + case Gen3Constants.teeterDanceEffect: + move.statusMoveType = StatusMoveType.NO_DAMAGE; + break; + + case Gen3Constants.damagePoisonEffect: + case Gen3Constants.damageBurnEffect: + case Gen3Constants.damageFreezeEffect: + case Gen3Constants.damageParalyzeEffect: + case Gen3Constants.damageConfusionEffect: + case Gen3Constants.twineedleEffect: + case Gen3Constants.damageBurnAndThawUserEffect: + case Gen3Constants.thunderEffect: + case Gen3Constants.blazeKickEffect: + case Gen3Constants.poisonFangEffect: + case Gen3Constants.poisonTailEffect: + move.statusMoveType = StatusMoveType.DAMAGE; + break; + + default: + // Move does not have a status effect + return; + } + + switch (move.effectIndex) { + case Gen3Constants.noDamageSleepEffect: + move.statusType = StatusType.SLEEP; + break; + case Gen3Constants.damagePoisonEffect: + case Gen3Constants.noDamagePoisonEffect: + case Gen3Constants.twineedleEffect: + case Gen3Constants.poisonTailEffect: + move.statusType = StatusType.POISON; + break; + case Gen3Constants.damageBurnEffect: + case Gen3Constants.damageBurnAndThawUserEffect: + case Gen3Constants.noDamageBurnEffect: + case Gen3Constants.blazeKickEffect: + move.statusType = StatusType.BURN; + break; + case Gen3Constants.damageFreezeEffect: + move.statusType = StatusType.FREEZE; + break; + case Gen3Constants.damageParalyzeEffect: + case Gen3Constants.noDamageParalyzeEffect: + case Gen3Constants.thunderEffect: + move.statusType = StatusType.PARALYZE; + break; + case Gen3Constants.toxicEffect: + case Gen3Constants.poisonFangEffect: + move.statusType = StatusType.TOXIC_POISON; + break; + case Gen3Constants.noDamageConfusionEffect: + case Gen3Constants.damageConfusionEffect: + case Gen3Constants.swaggerEffect: + case Gen3Constants.flatterEffect: + case Gen3Constants.teeterDanceEffect: + move.statusType = StatusType.CONFUSION; + break; + } + + if (move.statusMoveType == StatusMoveType.DAMAGE) { + move.statusPercentChance = secondaryEffectChance; + if (move.statusPercentChance == 0.0) { + move.statusPercentChance = 100.0; + } + } + } + + private void loadMiscMoveInfoFromEffect(Move move, int secondaryEffectChance) { + switch (move.effectIndex) { + case Gen3Constants.increasedCritEffect: + case Gen3Constants.blazeKickEffect: + case Gen3Constants.poisonTailEffect: + move.criticalChance = CriticalChance.INCREASED; + break; + + case Gen3Constants.futureSightAndDoomDesireEffect: + case Gen3Constants.spitUpEffect: + move.criticalChance = CriticalChance.NONE; + + case Gen3Constants.flinchEffect: + case Gen3Constants.snoreEffect: + case Gen3Constants.twisterEffect: + case Gen3Constants.flinchWithMinimizeBonusEffect: + case Gen3Constants.fakeOutEffect: + move.flinchPercentChance = secondaryEffectChance; + break; + + case Gen3Constants.damageAbsorbEffect: + case Gen3Constants.dreamEaterEffect: + move.absorbPercent = 50; + break; + + case Gen3Constants.damageRecoil25PercentEffect: + move.recoilPercent = 25; + break; + + case Gen3Constants.damageRecoil33PercentEffect: + move.recoilPercent = 33; + break; + + case Gen3Constants.bindingEffect: + case Gen3Constants.trappingEffect: + move.isTrapMove = true; + break; + + case Gen3Constants.razorWindEffect: + case Gen3Constants.skullBashEffect: + case Gen3Constants.solarbeamEffect: + case Gen3Constants.semiInvulnerableEffect: + move.isChargeMove = true; + break; + + case Gen3Constants.rechargeEffect: + move.isRechargeMove = true; + break; + + case Gen3Constants.skyAttackEffect: + move.criticalChance = CriticalChance.INCREASED; + move.flinchPercentChance = secondaryEffectChance; + move.isChargeMove = true; + break; + } + } + + private void saveMoves() { + int moveCount = romEntry.getValue("MoveCount"); + int offs = romEntry.getValue("MoveData"); + for (int i = 1; i <= moveCount; i++) { + rom[offs + i * 0xC] = (byte) moves[i].effectIndex; + rom[offs + i * 0xC + 1] = (byte) moves[i].power; + rom[offs + i * 0xC + 2] = Gen3Constants.typeToByte(moves[i].type); + int hitratio = (int) Math.round(moves[i].hitratio); + if (hitratio < 0) { + hitratio = 0; + } + if (hitratio > 100) { + hitratio = 100; + } + rom[offs + i * 0xC + 3] = (byte) hitratio; + rom[offs + i * 0xC + 4] = (byte) moves[i].pp; + } + } + + public List<Move> getMoves() { + return Arrays.asList(moves); + } + + private void loadBasicPokeStats(Pokemon pkmn, int offset) { + pkmn.hp = rom[offset + Gen3Constants.bsHPOffset] & 0xFF; + pkmn.attack = rom[offset + Gen3Constants.bsAttackOffset] & 0xFF; + pkmn.defense = rom[offset + Gen3Constants.bsDefenseOffset] & 0xFF; + pkmn.speed = rom[offset + Gen3Constants.bsSpeedOffset] & 0xFF; + pkmn.spatk = rom[offset + Gen3Constants.bsSpAtkOffset] & 0xFF; + pkmn.spdef = rom[offset + Gen3Constants.bsSpDefOffset] & 0xFF; + // Type + pkmn.primaryType = Gen3Constants.typeTable[rom[offset + Gen3Constants.bsPrimaryTypeOffset] & 0xFF]; + pkmn.secondaryType = Gen3Constants.typeTable[rom[offset + Gen3Constants.bsSecondaryTypeOffset] & 0xFF]; + // Only one type? + if (pkmn.secondaryType == pkmn.primaryType) { + pkmn.secondaryType = null; + } + pkmn.catchRate = rom[offset + Gen3Constants.bsCatchRateOffset] & 0xFF; + pkmn.growthCurve = ExpCurve.fromByte(rom[offset + Gen3Constants.bsGrowthCurveOffset]); + // Abilities + pkmn.ability1 = rom[offset + Gen3Constants.bsAbility1Offset] & 0xFF; + pkmn.ability2 = rom[offset + Gen3Constants.bsAbility2Offset] & 0xFF; + + // Held Items? + int item1 = readWord(offset + Gen3Constants.bsCommonHeldItemOffset); + int item2 = readWord(offset + Gen3Constants.bsRareHeldItemOffset); + + if (item1 == item2) { + // guaranteed + pkmn.guaranteedHeldItem = item1; + pkmn.commonHeldItem = 0; + pkmn.rareHeldItem = 0; + } else { + pkmn.guaranteedHeldItem = 0; + pkmn.commonHeldItem = item1; + pkmn.rareHeldItem = item2; + } + pkmn.darkGrassHeldItem = -1; + + pkmn.genderRatio = rom[offset + Gen3Constants.bsGenderRatioOffset] & 0xFF; + } + + private void saveBasicPokeStats(Pokemon pkmn, int offset) { + rom[offset + Gen3Constants.bsHPOffset] = (byte) pkmn.hp; + rom[offset + Gen3Constants.bsAttackOffset] = (byte) pkmn.attack; + rom[offset + Gen3Constants.bsDefenseOffset] = (byte) pkmn.defense; + rom[offset + Gen3Constants.bsSpeedOffset] = (byte) pkmn.speed; + rom[offset + Gen3Constants.bsSpAtkOffset] = (byte) pkmn.spatk; + rom[offset + Gen3Constants.bsSpDefOffset] = (byte) pkmn.spdef; + rom[offset + Gen3Constants.bsPrimaryTypeOffset] = Gen3Constants.typeToByte(pkmn.primaryType); + if (pkmn.secondaryType == null) { + rom[offset + Gen3Constants.bsSecondaryTypeOffset] = rom[offset + Gen3Constants.bsPrimaryTypeOffset]; + } else { + rom[offset + Gen3Constants.bsSecondaryTypeOffset] = Gen3Constants.typeToByte(pkmn.secondaryType); + } + rom[offset + Gen3Constants.bsCatchRateOffset] = (byte) pkmn.catchRate; + rom[offset + Gen3Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte(); + + rom[offset + Gen3Constants.bsAbility1Offset] = (byte) pkmn.ability1; + if (pkmn.ability2 == 0) { + // required to not break evos with random ability + rom[offset + Gen3Constants.bsAbility2Offset] = (byte) pkmn.ability1; + } else { + rom[offset + Gen3Constants.bsAbility2Offset] = (byte) pkmn.ability2; + } + + // Held items + if (pkmn.guaranteedHeldItem > 0) { + writeWord(offset + Gen3Constants.bsCommonHeldItemOffset, pkmn.guaranteedHeldItem); + writeWord(offset + Gen3Constants.bsRareHeldItemOffset, pkmn.guaranteedHeldItem); + } else { + writeWord(offset + Gen3Constants.bsCommonHeldItemOffset, pkmn.commonHeldItem); + writeWord(offset + Gen3Constants.bsRareHeldItemOffset, pkmn.rareHeldItem); + } + + rom[offset + Gen3Constants.bsGenderRatioOffset] = (byte) pkmn.genderRatio; + } + + private void loadPokemonNames() { + int offs = romEntry.getValue("PokemonNames"); + int nameLen = romEntry.getValue("PokemonNameLength"); + int numInternalPokes = romEntry.getValue("PokemonCount"); + pokeNames = new String[numInternalPokes + 1]; + for (int i = 1; i <= numInternalPokes; i++) { + pokeNames[i] = readFixedLengthString(offs + i * nameLen, nameLen); + } + } + + private String readString(int offset, int maxLength) { + StringBuilder string = new StringBuilder(); + for (int c = 0; c < maxLength; c++) { + int currChar = rom[offset + c] & 0xFF; + if (tb[currChar] != null) { + string.append(tb[currChar]); + } else { + if (currChar == Gen3Constants.textTerminator) { + break; + } else if (currChar == Gen3Constants.textVariable) { + int nextChar = rom[offset + c + 1] & 0xFF; + string.append("\\v").append(String.format("%02X", nextChar)); + c++; + } else { + string.append("\\x").append(String.format("%02X", currChar)); + } + } + } + return string.toString(); + } + + private byte[] translateString(String text) { + List<Byte> data = new ArrayList<>(); + while (text.length() != 0) { + int i = Math.max(0, 4 - text.length()); + if (text.charAt(0) == '\\' && text.charAt(1) == 'x') { + data.add((byte) Integer.parseInt(text.substring(2, 4), 16)); + text = text.substring(4); + } else if (text.charAt(0) == '\\' && text.charAt(1) == 'v') { + data.add((byte) Gen3Constants.textVariable); + data.add((byte) Integer.parseInt(text.substring(2, 4), 16)); + text = text.substring(4); + } else { + while (!(d.containsKey(text.substring(0, 4 - i)) || (i == 4))) { + i++; + } + if (i == 4) { + text = text.substring(1); + } else { + data.add(d.get(text.substring(0, 4 - i))); + text = text.substring(4 - i); + } + } + } + byte[] ret = new byte[data.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = data.get(i); + } + return ret; + } + + private String readFixedLengthString(int offset, int length) { + return readString(offset, length); + } + + private String readVariableLengthString(int offset) { + return readString(offset, Integer.MAX_VALUE); + } + + private void writeFixedLengthString(String str, int offset, int length) { + byte[] translated = translateString(str); + int len = Math.min(translated.length, length); + System.arraycopy(translated, 0, rom, offset, len); + if (len < length) { + rom[offset + len] = (byte) Gen3Constants.textTerminator; + len++; + } + while (len < length) { + rom[offset + len] = 0; + len++; + } + } + + private void writeVariableLengthString(String str, int offset) { + byte[] translated = translateString(str); + System.arraycopy(translated, 0, rom, offset, translated.length); + rom[offset + translated.length] = (byte) 0xFF; + } + + private int lengthOfStringAt(int offset) { + int len = 0; + while ((rom[offset + (len++)] & 0xFF) != 0xFF) { + } + return len - 1; + } + + private static boolean romName(byte[] rom, String name) { + try { + int sigOffset = Gen3Constants.romNameOffset; + byte[] sigBytes = name.getBytes("US-ASCII"); + for (int i = 0; i < sigBytes.length; i++) { + if (rom[sigOffset + i] != sigBytes[i]) { + return false; + } + } + return true; + } catch (UnsupportedEncodingException ex) { + return false; + } + + } + + private static boolean romCode(byte[] rom, String codeToCheck) { + try { + int sigOffset = Gen3Constants.romCodeOffset; + byte[] sigBytes = codeToCheck.getBytes("US-ASCII"); + for (int i = 0; i < sigBytes.length; i++) { + if (rom[sigOffset + i] != sigBytes[i]) { + return false; + } + } + return true; + } catch (UnsupportedEncodingException ex) { + return false; + } + + } + + private int readPointer(int offset) { + return readLong(offset) - 0x8000000; + } + + private int readLong(int offset) { + return (rom[offset] & 0xFF) + ((rom[offset + 1] & 0xFF) << 8) + ((rom[offset + 2] & 0xFF) << 16) + + (((rom[offset + 3] & 0xFF)) << 24); + } + + private void writePointer(int offset, int pointer) { + writeLong(offset, pointer + 0x8000000); + } + + private void writeLong(int offset, int value) { + rom[offset] = (byte) (value & 0xFF); + rom[offset + 1] = (byte) ((value >> 8) & 0xFF); + rom[offset + 2] = (byte) ((value >> 16) & 0xFF); + rom[offset + 3] = (byte) (((value >> 24) & 0xFF)); + } + + @Override + public List<Pokemon> getStarters() { + List<Pokemon> starters = new ArrayList<>(); + int baseOffset = romEntry.getValue("StarterPokemon"); + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp + || romEntry.romType == Gen3Constants.RomType_Em) { + // do something + Pokemon starter1 = pokesInternal[readWord(baseOffset)]; + Pokemon starter2 = pokesInternal[readWord(baseOffset + Gen3Constants.rseStarter2Offset)]; + Pokemon starter3 = pokesInternal[readWord(baseOffset + Gen3Constants.rseStarter3Offset)]; + starters.add(starter1); + starters.add(starter2); + starters.add(starter3); + } else { + // do something else + Pokemon starter1 = pokesInternal[readWord(baseOffset)]; + Pokemon starter2 = pokesInternal[readWord(baseOffset + Gen3Constants.frlgStarter2Offset)]; + Pokemon starter3 = pokesInternal[readWord(baseOffset + Gen3Constants.frlgStarter3Offset)]; + starters.add(starter1); + starters.add(starter2); + starters.add(starter3); + } + return starters; + } + + @Override + public boolean setStarters(List<Pokemon> newStarters) { + if (newStarters.size() != 3) { + return false; + } + + // Support Deoxys/Mew starters in E/FR/LG + attemptObedienceEvolutionPatches(); + int baseOffset = romEntry.getValue("StarterPokemon"); + + int starter0 = pokedexToInternal[newStarters.get(0).number]; + int starter1 = pokedexToInternal[newStarters.get(1).number]; + int starter2 = pokedexToInternal[newStarters.get(2).number]; + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp + || romEntry.romType == Gen3Constants.RomType_Em) { + + // US + // order: 0, 1, 2 + writeWord(baseOffset, starter0); + writeWord(baseOffset + Gen3Constants.rseStarter2Offset, starter1); + writeWord(baseOffset + Gen3Constants.rseStarter3Offset, starter2); + + } else { + // frlg: + // order: 0, 1, 2 + writeWord(baseOffset, starter0); + writeWord(baseOffset + Gen3Constants.frlgStarterRepeatOffset, starter1); + + writeWord(baseOffset + Gen3Constants.frlgStarter2Offset, starter1); + writeWord(baseOffset + Gen3Constants.frlgStarter2Offset + Gen3Constants.frlgStarterRepeatOffset, starter2); + + writeWord(baseOffset + Gen3Constants.frlgStarter3Offset, starter2); + writeWord(baseOffset + Gen3Constants.frlgStarter3Offset + Gen3Constants.frlgStarterRepeatOffset, starter0); + + if (romEntry.romCode.charAt(3) != 'J' && romEntry.romCode.charAt(3) != 'B') { + // Update PROF. Oak's descriptions for each starter + // First result for each STARTERNAME is the text we need + List<Integer> bulbasaurFoundTexts = RomFunctions.search(rom, translateString(pokes[Gen3Constants.frlgBaseStarter1].name.toUpperCase())); + List<Integer> charmanderFoundTexts = RomFunctions.search(rom, translateString(pokes[Gen3Constants.frlgBaseStarter2].name.toUpperCase())); + List<Integer> squirtleFoundTexts = RomFunctions.search(rom, translateString(pokes[Gen3Constants.frlgBaseStarter3].name.toUpperCase())); + writeFRLGStarterText(bulbasaurFoundTexts, newStarters.get(0), "you want to go with\\nthe "); + writeFRLGStarterText(charmanderFoundTexts, newStarters.get(1), "you’re claiming the\\n"); + writeFRLGStarterText(squirtleFoundTexts, newStarters.get(2), "you’ve decided on the\\n"); + } + } + return true; + + } + + @Override + public boolean hasStarterAltFormes() { + return false; + } + + @Override + public int starterCount() { + return 3; + } + + @Override + public Map<Integer, StatChange> getUpdatedPokemonStats(int generation) { + return GlobalConstants.getStatChanges(generation); + } + + @Override + public boolean supportsStarterHeldItems() { + return true; + } + + @Override + public List<Integer> getStarterHeldItems() { + List<Integer> sHeldItems = new ArrayList<>(); + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // offset from normal starter offset as a word + int baseOffset = romEntry.getValue("StarterPokemon"); + sHeldItems.add(readWord(baseOffset + Gen3Constants.frlgStarterItemsOffset)); + } else { + int baseOffset = romEntry.getValue("StarterItems"); + int i1 = rom[baseOffset] & 0xFF; + int i2 = rom[baseOffset + 2] & 0xFF; + if (i2 == 0) { + sHeldItems.add(i1); + } else { + sHeldItems.add(i2 + 0xFF); + } + } + return sHeldItems; + } + + @Override + public void setStarterHeldItems(List<Integer> items) { + if (items.size() != 1) { + return; + } + int item = items.get(0); + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // offset from normal starter offset as a word + int baseOffset = romEntry.getValue("StarterPokemon"); + writeWord(baseOffset + Gen3Constants.frlgStarterItemsOffset, item); + } else { + int baseOffset = romEntry.getValue("StarterItems"); + if (item <= 0xFF) { + rom[baseOffset] = (byte) item; + rom[baseOffset + 2] = 0; + rom[baseOffset + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR2; + } else { + rom[baseOffset] = (byte) 0xFF; + rom[baseOffset + 2] = (byte) (item - 0xFF); + rom[baseOffset + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR2; + } + } + } + + private void writeFRLGStarterText(List<Integer> foundTexts, Pokemon pkmn, String oakText) { + if (foundTexts.size() > 0) { + int offset = foundTexts.get(0); + String pokeName = pkmn.name; + String pokeType = pkmn.primaryType == null ? "???" : pkmn.primaryType.toString(); + if (pokeType.equals("NORMAL") && pkmn.secondaryType != null) { + pokeType = pkmn.secondaryType.toString(); + } + String speech = pokeName + " is your choice.\\pSo, \\v01, " + oakText + pokeType + " POKéMON " + pokeName + + "?"; + writeFixedLengthString(speech, offset, lengthOfStringAt(offset) + 1); + } + } + + @Override + public List<EncounterSet> getEncounters(boolean useTimeOfDay) { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + + int startOffs = romEntry.getValue("WildPokemon"); + List<EncounterSet> encounterAreas = new ArrayList<>(); + Set<Integer> seenOffsets = new TreeSet<>(); + int offs = startOffs; + while (true) { + // Read pointers + int bank = rom[offs] & 0xFF; + int map = rom[offs + 1] & 0xFF; + if (bank == 0xFF && map == 0xFF) { + break; + } + + String mapName = mapNames[bank][map]; + + int grassPokes = readPointer(offs + 4); + int waterPokes = readPointer(offs + 8); + int treePokes = readPointer(offs + 12); + int fishPokes = readPointer(offs + 16); + + // Add pokemanz + if (grassPokes >= 0 && grassPokes < rom.length && rom[grassPokes] != 0 + && !seenOffsets.contains(readPointer(grassPokes + 4))) { + encounterAreas.add(readWildArea(grassPokes, Gen3Constants.grassSlots, mapName + " Grass/Cave")); + seenOffsets.add(readPointer(grassPokes + 4)); + } + if (waterPokes >= 0 && waterPokes < rom.length && rom[waterPokes] != 0 + && !seenOffsets.contains(readPointer(waterPokes + 4))) { + encounterAreas.add(readWildArea(waterPokes, Gen3Constants.surfingSlots, mapName + " Surfing")); + seenOffsets.add(readPointer(waterPokes + 4)); + } + if (treePokes >= 0 && treePokes < rom.length && rom[treePokes] != 0 + && !seenOffsets.contains(readPointer(treePokes + 4))) { + encounterAreas.add(readWildArea(treePokes, Gen3Constants.rockSmashSlots, mapName + " Rock Smash")); + seenOffsets.add(readPointer(treePokes + 4)); + } + if (fishPokes >= 0 && fishPokes < rom.length && rom[fishPokes] != 0 + && !seenOffsets.contains(readPointer(fishPokes + 4))) { + encounterAreas.add(readWildArea(fishPokes, Gen3Constants.fishingSlots, mapName + " Fishing")); + seenOffsets.add(readPointer(fishPokes + 4)); + } + + offs += 20; + } + if (romEntry.arrayEntries.containsKey("BattleTrappersBanned")) { + // Some encounter sets aren't allowed to have Pokemon + // with Arena Trap, Shadow Tag etc. + int[] bannedAreas = romEntry.arrayEntries.get("BattleTrappersBanned"); + Set<Pokemon> battleTrappers = new HashSet<>(); + for (Pokemon pk : getPokemon()) { + if (hasBattleTrappingAbility(pk)) { + battleTrappers.add(pk); + } + } + for (int areaIdx : bannedAreas) { + encounterAreas.get(areaIdx).bannedPokemon.addAll(battleTrappers); + } + } + return encounterAreas; + } + + private boolean hasBattleTrappingAbility(Pokemon pokemon) { + return pokemon != null + && (GlobalConstants.battleTrappingAbilities.contains(pokemon.ability1) || GlobalConstants.battleTrappingAbilities + .contains(pokemon.ability2)); + } + + private EncounterSet readWildArea(int offset, int numOfEntries, String setName) { + EncounterSet thisSet = new EncounterSet(); + thisSet.rate = rom[offset]; + thisSet.displayName = setName; + // Grab the *real* pointer to data + int dataOffset = readPointer(offset + 4); + // Read the entries + for (int i = 0; i < numOfEntries; i++) { + // min, max, species, species + Encounter enc = new Encounter(); + enc.level = rom[dataOffset + i * 4]; + enc.maxLevel = rom[dataOffset + i * 4 + 1]; + try { + enc.pokemon = pokesInternal[readWord(dataOffset + i * 4 + 2)]; + } catch (ArrayIndexOutOfBoundsException ex) { + throw ex; + } + thisSet.encounters.add(enc); + } + return thisSet; + } + + @Override + public void setEncounters(boolean useTimeOfDay, List<EncounterSet> encounters) { + // Support Deoxys/Mew catches in E/FR/LG + attemptObedienceEvolutionPatches(); + + int startOffs = romEntry.getValue("WildPokemon"); + Iterator<EncounterSet> encounterAreas = encounters.iterator(); + Set<Integer> seenOffsets = new TreeSet<>(); + int offs = startOffs; + while (true) { + // Read pointers + int bank = rom[offs] & 0xFF; + int map = rom[offs + 1] & 0xFF; + if (bank == 0xFF && map == 0xFF) { + break; + } + + int grassPokes = readPointer(offs + 4); + int waterPokes = readPointer(offs + 8); + int treePokes = readPointer(offs + 12); + int fishPokes = readPointer(offs + 16); + + // Add pokemanz + if (grassPokes >= 0 && grassPokes < rom.length && rom[grassPokes] != 0 + && !seenOffsets.contains(readPointer(grassPokes + 4))) { + writeWildArea(grassPokes, Gen3Constants.grassSlots, encounterAreas.next()); + seenOffsets.add(readPointer(grassPokes + 4)); + } + if (waterPokes >= 0 && waterPokes < rom.length && rom[waterPokes] != 0 + && !seenOffsets.contains(readPointer(waterPokes + 4))) { + writeWildArea(waterPokes, Gen3Constants.surfingSlots, encounterAreas.next()); + seenOffsets.add(readPointer(waterPokes + 4)); + } + if (treePokes >= 0 && treePokes < rom.length && rom[treePokes] != 0 + && !seenOffsets.contains(readPointer(treePokes + 4))) { + writeWildArea(treePokes, Gen3Constants.rockSmashSlots, encounterAreas.next()); + seenOffsets.add(readPointer(treePokes + 4)); + } + if (fishPokes >= 0 && fishPokes < rom.length && rom[fishPokes] != 0 + && !seenOffsets.contains(readPointer(fishPokes + 4))) { + writeWildArea(fishPokes, Gen3Constants.fishingSlots, encounterAreas.next()); + seenOffsets.add(readPointer(fishPokes + 4)); + } + + offs += 20; + } + } + + @Override + public boolean hasWildAltFormes() { + return false; + } + + @Override + public List<Pokemon> bannedForWildEncounters() { + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // Ban Unown in FRLG because the game crashes if it is encountered outside of Tanoby Ruins. + // See GenerateWildMon in wild_encounter.c in pokefirered + return new ArrayList<>(Collections.singletonList(pokes[Species.unown])); + } + return new ArrayList<>(); + } + + @Override + public List<Trainer> getTrainers() { + int baseOffset = romEntry.getValue("TrainerData"); + int amount = romEntry.getValue("TrainerCount"); + int entryLen = romEntry.getValue("TrainerEntrySize"); + List<Trainer> theTrainers = new ArrayList<>(); + List<String> tcnames = this.getTrainerClassNames(); + for (int i = 1; i < amount; i++) { + // Trainer entries are 40 bytes + // Team flags; 1 byte; 0x01 = custom moves, 0x02 = held item + // Class; 1 byte + // Encounter Music and gender; 1 byte + // Battle Sprite; 1 byte + // Name; 12 bytes; 0xff terminated + // Items; 2 bytes each, 4 item slots + // Battle Mode; 1 byte; 0 means single, 1 means double. + // 3 bytes not used + // AI Flags; 1 byte + // 3 bytes not used + // Number of pokemon in team; 1 byte + // 3 bytes not used + // Pointer to pokemon; 4 bytes + // https://github.com/pret/pokefirered/blob/3dce3407d5f9bca69d61b1cf1b314fb1e921d572/include/battle.h#L111 + int trOffset = baseOffset + i * entryLen; + Trainer tr = new Trainer(); + tr.offset = trOffset; + tr.index = i; + int trainerclass = rom[trOffset + 1] & 0xFF; + tr.trainerclass = (rom[trOffset + 2] & 0x80) > 0 ? 1 : 0; + + int pokeDataType = rom[trOffset] & 0xFF; + boolean doubleBattle = rom[trOffset + (entryLen - 16)] == 0x01; + int numPokes = rom[trOffset + (entryLen - 8)] & 0xFF; + int pointerToPokes = readPointer(trOffset + (entryLen - 4)); + tr.poketype = pokeDataType; + tr.name = this.readVariableLengthString(trOffset + 4); + tr.fullDisplayName = tcnames.get(trainerclass) + " " + tr.name; + // Pokemon structure data is like + // IV IV LV SP SP + // (HI HI) + // (M1 M1 M2 M2 M3 M3 M4 M4) + // IV is a "difficulty" level between 0 and 255 to represent 0 to 31 IVs. + // These IVs affect all attributes. For the vanilla games, the majority + // of trainers have 0 IVs; Elite Four members will have 31 IVs. + // https://github.com/pret/pokeemerald/blob/6c38837b266c0dd36ccdd04559199282daa7a8a0/include/data.h#L22 + if (pokeDataType == 0) { + // blocks of 8 bytes + for (int poke = 0; poke < numPokes; poke++) { + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.IVs = ((readWord(pointerToPokes + poke * 8) & 0xFF) * 31) / 255; + thisPoke.level = readWord(pointerToPokes + poke * 8 + 2); + thisPoke.pokemon = pokesInternal[readWord(pointerToPokes + poke * 8 + 4)]; + tr.pokemon.add(thisPoke); + } + } else if (pokeDataType == 2) { + // blocks of 8 bytes + for (int poke = 0; poke < numPokes; poke++) { + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.IVs = ((readWord(pointerToPokes + poke * 8) & 0xFF) * 31) / 255; + thisPoke.level = readWord(pointerToPokes + poke * 8 + 2); + thisPoke.pokemon = pokesInternal[readWord(pointerToPokes + poke * 8 + 4)]; + thisPoke.heldItem = readWord(pointerToPokes + poke * 8 + 6); + tr.pokemon.add(thisPoke); + } + } else if (pokeDataType == 1) { + // blocks of 16 bytes + for (int poke = 0; poke < numPokes; poke++) { + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.IVs = ((readWord(pointerToPokes + poke * 16) & 0xFF) * 31) / 255; + thisPoke.level = readWord(pointerToPokes + poke * 16 + 2); + thisPoke.pokemon = pokesInternal[readWord(pointerToPokes + poke * 16 + 4)]; + for (int move = 0; move < 4; move++) { + thisPoke.moves[move] = readWord(pointerToPokes + poke * 16 + 6 + (move*2)); + } + tr.pokemon.add(thisPoke); + } + } else if (pokeDataType == 3) { + // blocks of 16 bytes + for (int poke = 0; poke < numPokes; poke++) { + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.IVs = ((readWord(pointerToPokes + poke * 16) & 0xFF) * 31) / 255; + thisPoke.level = readWord(pointerToPokes + poke * 16 + 2); + thisPoke.pokemon = pokesInternal[readWord(pointerToPokes + poke * 16 + 4)]; + thisPoke.heldItem = readWord(pointerToPokes + poke * 16 + 6); + for (int move = 0; move < 4; move++) { + thisPoke.moves[move] = readWord(pointerToPokes + poke * 16 + 8 + (move*2)); + } + tr.pokemon.add(thisPoke); + } + } + theTrainers.add(tr); + } + + if (romEntry.romType == Gen3Constants.RomType_Em) { + int mossdeepStevenOffset = romEntry.getValue("MossdeepStevenTeamOffset"); + Trainer mossdeepSteven = new Trainer(); + mossdeepSteven.offset = mossdeepStevenOffset; + mossdeepSteven.index = amount; + mossdeepSteven.poketype = 1; // Custom moves, but no held items + + // This is literally how the game does it too, lol. Have to subtract one because the + // trainers internally are one-indexed, but then theTrainers is zero-indexed. + Trainer meteorFallsSteven = theTrainers.get(Gen3Constants.emMeteorFallsStevenIndex - 1); + mossdeepSteven.trainerclass = meteorFallsSteven.trainerclass; + mossdeepSteven.name = meteorFallsSteven.name; + mossdeepSteven.fullDisplayName = meteorFallsSteven.fullDisplayName; + + for (int i = 0; i < 3; i++) { + int currentOffset = mossdeepStevenOffset + (i * 20); + TrainerPokemon thisPoke = new TrainerPokemon(); + thisPoke.pokemon = pokesInternal[readWord(currentOffset)]; + thisPoke.IVs = rom[currentOffset + 2]; + thisPoke.level = rom[currentOffset + 3]; + for (int move = 0; move < 4; move++) { + thisPoke.moves[move] = readWord(currentOffset + 12 + (move * 2)); + } + mossdeepSteven.pokemon.add(thisPoke); + } + + theTrainers.add(mossdeepSteven); + } + + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + Gen3Constants.trainerTagsRS(theTrainers, romEntry.romType); + } else if (romEntry.romType == Gen3Constants.RomType_Em) { + Gen3Constants.trainerTagsE(theTrainers); + Gen3Constants.setMultiBattleStatusEm(theTrainers); + } else { + Gen3Constants.trainerTagsFRLG(theTrainers); + } + return theTrainers; + } + + @Override + public List<Integer> getEvolutionItems() { + return Gen3Constants.evolutionItems; + } + + @Override + public List<Integer> getXItems() { + return Gen3Constants.xItems; + } + + @Override + public List<Integer> getMainPlaythroughTrainers() { + return new ArrayList<>(); // Not implemented + } + + @Override + public List<Integer> getEliteFourTrainers(boolean isChallengeMode) { + return Arrays.stream(romEntry.arrayEntries.get("EliteFourIndices")).boxed().collect(Collectors.toList()); + } + + + @Override + public void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode) { + int baseOffset = romEntry.getValue("TrainerData"); + int amount = romEntry.getValue("TrainerCount"); + int entryLen = romEntry.getValue("TrainerEntrySize"); + Iterator<Trainer> theTrainers = trainerData.iterator(); + int fso = romEntry.getValue("FreeSpace"); + + // Get current movesets in case we need to reset them for certain + // trainer mons. + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + + for (int i = 1; i < amount; i++) { + int trOffset = baseOffset + i * entryLen; + Trainer tr = theTrainers.next(); + // Do we need to repoint this trainer's data? + int oldPokeType = rom[trOffset] & 0xFF; + int oldPokeCount = rom[trOffset + (entryLen - 8)] & 0xFF; + int newPokeCount = tr.pokemon.size(); + int newDataSize = newPokeCount * ((tr.poketype & 1) == 1 ? 16 : 8); + int oldDataSize = oldPokeCount * ((oldPokeType & 1) == 1 ? 16 : 8); + + // write out new data first... + rom[trOffset] = (byte) tr.poketype; + rom[trOffset + (entryLen - 8)] = (byte) newPokeCount; + if (doubleBattleMode) { + if (!tr.skipImportant()) { + rom[trOffset + (entryLen - 16)] = 0x01; + } + } + + // now, do we need to repoint? + int pointerToPokes; + if (newDataSize > oldDataSize) { + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, newDataSize, fso, true); + if (writeSpace < fso) { + throw new RandomizerIOException("ROM is full"); + } + writePointer(trOffset + (entryLen - 4), writeSpace); + pointerToPokes = writeSpace; + } else { + pointerToPokes = readPointer(trOffset + (entryLen - 4)); + } + + Iterator<TrainerPokemon> pokes = tr.pokemon.iterator(); + + // Write out Pokemon data! + if (tr.pokemonHaveCustomMoves()) { + // custom moves, blocks of 16 bytes + for (int poke = 0; poke < newPokeCount; poke++) { + TrainerPokemon tp = pokes.next(); + // Add 1 to offset integer division truncation + writeWord(pointerToPokes + poke * 16, Math.min(255, 1 + (tp.IVs * 255) / 31)); + writeWord(pointerToPokes + poke * 16 + 2, tp.level); + writeWord(pointerToPokes + poke * 16 + 4, pokedexToInternal[tp.pokemon.number]); + int movesStart; + if (tr.pokemonHaveItems()) { + writeWord(pointerToPokes + poke * 16 + 6, tp.heldItem); + movesStart = 8; + } else { + movesStart = 6; + writeWord(pointerToPokes + poke * 16 + 14, 0); + } + if (tp.resetMoves) { + int[] pokeMoves = RomFunctions.getMovesAtLevel(tp.pokemon.number, movesets, tp.level); + for (int m = 0; m < 4; m++) { + writeWord(pointerToPokes + poke * 16 + movesStart + m * 2, pokeMoves[m]); + } + } else { + writeWord(pointerToPokes + poke * 16 + movesStart, tp.moves[0]); + writeWord(pointerToPokes + poke * 16 + movesStart + 2, tp.moves[1]); + writeWord(pointerToPokes + poke * 16 + movesStart + 4, tp.moves[2]); + writeWord(pointerToPokes + poke * 16 + movesStart + 6, tp.moves[3]); + } + } + } else { + // no moves, blocks of 8 bytes + for (int poke = 0; poke < newPokeCount; poke++) { + TrainerPokemon tp = pokes.next(); + writeWord(pointerToPokes + poke * 8, Math.min(255, 1 + (tp.IVs * 255) / 31)); + writeWord(pointerToPokes + poke * 8 + 2, tp.level); + writeWord(pointerToPokes + poke * 8 + 4, pokedexToInternal[tp.pokemon.number]); + if (tr.pokemonHaveItems()) { + writeWord(pointerToPokes + poke * 8 + 6, tp.heldItem); + } else { + writeWord(pointerToPokes + poke * 8 + 6, 0); + } + } + } + } + + if (romEntry.romType == Gen3Constants.RomType_Em) { + int mossdeepStevenOffset = romEntry.getValue("MossdeepStevenTeamOffset"); + Trainer mossdeepSteven = trainerData.get(amount - 1); + + for (int i = 0; i < 3; i++) { + int currentOffset = mossdeepStevenOffset + (i * 20); + TrainerPokemon tp = mossdeepSteven.pokemon.get(i); + writeWord(currentOffset, pokedexToInternal[tp.pokemon.number]); + rom[currentOffset + 2] = (byte)tp.IVs; + rom[currentOffset + 3] = (byte)tp.level; + for (int move = 0; move < 4; move++) { + writeWord(currentOffset + 12 + (move * 2), tp.moves[move]); + } + } + } + } + + private void writeWildArea(int offset, int numOfEntries, EncounterSet encounters) { + // Grab the *real* pointer to data + int dataOffset = readPointer(offset + 4); + // Write the entries + for (int i = 0; i < numOfEntries; i++) { + Encounter enc = encounters.encounters.get(i); + // min, max, species, species + int levels = enc.level | (enc.maxLevel << 8); + writeWord(dataOffset + i * 4, levels); + writeWord(dataOffset + i * 4 + 2, pokedexToInternal[enc.pokemon.number]); + } + } + + @Override + public List<Pokemon> getPokemon() { + return pokemonList; + } + + @Override + public List<Pokemon> getPokemonInclFormes() { + return pokemonList; // No alt formes for now, should include Deoxys formes in the future + } + + @Override + public List<Pokemon> getAltFormes() { + return new ArrayList<>(); + } + + @Override + public List<MegaEvolution> getMegaEvolutions() { + return new ArrayList<>(); + } + + @Override + public Pokemon getAltFormeOfPokemon(Pokemon pk, int forme) { + return pk; + } + + @Override + public List<Pokemon> getIrregularFormes() { + return new ArrayList<>(); + } + + @Override + public boolean hasFunctionalFormes() { + return false; + } + + @Override + public Map<Integer, List<MoveLearnt>> getMovesLearnt() { + Map<Integer, List<MoveLearnt>> movesets = new TreeMap<>(); + int baseOffset = romEntry.getValue("PokemonMovesets"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pkmn = pokemonList.get(i); + int offsToPtr = baseOffset + (pokedexToInternal[pkmn.number]) * 4; + int moveDataLoc = readPointer(offsToPtr); + List<MoveLearnt> moves = new ArrayList<>(); + if (jamboMovesetHack) { + while ((rom[moveDataLoc] & 0xFF) != 0x00 || (rom[moveDataLoc + 1] & 0xFF) != 0x00 + || (rom[moveDataLoc + 2] & 0xFF) != 0xFF) { + MoveLearnt ml = new MoveLearnt(); + ml.level = rom[moveDataLoc + 2] & 0xFF; + ml.move = readWord(moveDataLoc); + moves.add(ml); + moveDataLoc += 3; + } + } else { + while ((rom[moveDataLoc] & 0xFF) != 0xFF || (rom[moveDataLoc + 1] & 0xFF) != 0xFF) { + int move = (rom[moveDataLoc] & 0xFF); + int level = (rom[moveDataLoc + 1] & 0xFE) >> 1; + if ((rom[moveDataLoc + 1] & 0x01) == 0x01) { + move += 0x100; + } + MoveLearnt ml = new MoveLearnt(); + ml.level = level; + ml.move = move; + moves.add(ml); + moveDataLoc += 2; + } + } + movesets.put(pkmn.number, moves); + } + return movesets; + } + + @Override + public void setMovesLearnt(Map<Integer, List<MoveLearnt>> movesets) { + int baseOffset = romEntry.getValue("PokemonMovesets"); + int fso = romEntry.getValue("FreeSpace"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pkmn = pokemonList.get(i); + int offsToPtr = baseOffset + (pokedexToInternal[pkmn.number]) * 4; + int moveDataLoc = readPointer(offsToPtr); + List<MoveLearnt> moves = movesets.get(pkmn.number); + int newMoveCount = moves.size(); + int mloc = moveDataLoc; + int entrySize; + if (jamboMovesetHack) { + while ((rom[mloc] & 0xFF) != 0x00 || (rom[mloc + 1] & 0xFF) != 0x00 || (rom[mloc + 2] & 0xFF) != 0xFF) { + mloc += 3; + } + entrySize = 3; + } else { + while ((rom[mloc] & 0xFF) != 0xFF || (rom[mloc + 1] & 0xFF) != 0xFF) { + mloc += 2; + } + entrySize = 2; + } + int currentMoveCount = (mloc - moveDataLoc) / entrySize; + + if (newMoveCount > currentMoveCount) { + // Repoint for more space + int newBytesNeeded = newMoveCount * entrySize + entrySize * 2; + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, newBytesNeeded, fso); + if (writeSpace < fso) { + throw new RandomizerIOException("ROM is full"); + } + writePointer(offsToPtr, writeSpace); + moveDataLoc = writeSpace; + } + + // Write new moveset now that space is ensured. + for (MoveLearnt ml : moves) { + moveDataLoc += writeMLToOffset(moveDataLoc, ml); + } + + // If move count changed, new terminator is required + // In the repoint enough space was reserved to add some padding to + // make sure the terminator isn't detected as free space. + // If no repoint, the padding goes over the old moves/terminator. + if (newMoveCount != currentMoveCount) { + if (jamboMovesetHack) { + rom[moveDataLoc] = 0x00; + rom[moveDataLoc + 1] = 0x00; + rom[moveDataLoc + 2] = (byte) 0xFF; + rom[moveDataLoc + 3] = 0x00; + rom[moveDataLoc + 4] = 0x00; + rom[moveDataLoc + 5] = 0x00; + } else { + rom[moveDataLoc] = (byte) 0xFF; + rom[moveDataLoc + 1] = (byte) 0xFF; + rom[moveDataLoc + 2] = 0x00; + rom[moveDataLoc + 3] = 0x00; + } + } + + } + + } + + private int writeMLToOffset(int offset, MoveLearnt ml) { + if (jamboMovesetHack) { + writeWord(offset, ml.move); + rom[offset + 2] = (byte) ml.level; + return 3; + } else { + rom[offset] = (byte) (ml.move & 0xFF); + int levelPart = (ml.level << 1) & 0xFE; + if (ml.move > 255) { + levelPart++; + } + rom[offset + 1] = (byte) levelPart; + return 2; + } + } + + @Override + public Map<Integer, List<Integer>> getEggMoves() { + Map<Integer, List<Integer>> eggMoves = new TreeMap<>(); + int baseOffset = romEntry.getValue("EggMoves"); + int currentOffset = baseOffset; + int currentSpecies = 0; + List<Integer> currentMoves = new ArrayList<>(); + int val = FileFunctions.read2ByteInt(rom, currentOffset); + + // Check egg_moves.h in the Gen 3 decomps for more info on how this algorithm works. + while (val != 0xFFFF) { + if (val > 20000) { + int species = val - 20000; + if (currentMoves.size() > 0) { + eggMoves.put(internalToPokedex[currentSpecies], currentMoves); + } + currentSpecies = species; + currentMoves = new ArrayList<>(); + } else { + currentMoves.add(val); + } + currentOffset += 2; + val = FileFunctions.read2ByteInt(rom, currentOffset); + } + + // Need to make sure the last entry gets recorded too + if (currentMoves.size() > 0) { + eggMoves.put(internalToPokedex[currentSpecies], currentMoves); + } + return eggMoves; + } + + @Override + public void setEggMoves(Map<Integer, List<Integer>> eggMoves) { + int baseOffset = romEntry.getValue("EggMoves"); + int currentOffset = baseOffset; + for (int species : eggMoves.keySet()) { + FileFunctions.write2ByteInt(rom, currentOffset, pokedexToInternal[species] + 20000); + currentOffset += 2; + for (int move : eggMoves.get(species)) { + FileFunctions.write2ByteInt(rom, currentOffset, move); + currentOffset += 2; + } + } + } + + private static class StaticPokemon { + private int[] speciesOffsets; + private int[] levelOffsets; + + public StaticPokemon() { + this.speciesOffsets = new int[0]; + this.levelOffsets = new int[0]; + } + + public Pokemon getPokemon(Gen3RomHandler parent) { + return parent.pokesInternal[parent.readWord(speciesOffsets[0])]; + } + + public void setPokemon(Gen3RomHandler parent, Pokemon pkmn) { + int value = parent.pokedexToInternal[pkmn.number]; + for (int offset : speciesOffsets) { + parent.writeWord(offset, value); + } + } + + public int getLevel(byte[] rom, int i) { + if (levelOffsets.length <= i) { + return 1; + } + return rom[levelOffsets[i]]; + } + + public void setLevel(byte[] rom, int level, int i) { + if (levelOffsets.length > i) { // Might not have a level entry e.g., it's an egg + rom[levelOffsets[i]] = (byte) level; + } + } + } + + @Override + public List<StaticEncounter> getStaticPokemon() { + List<StaticEncounter> statics = new ArrayList<>(); + List<StaticPokemon> staticsHere = romEntry.staticPokemon; + int[] staticEggOffsets = new int[0]; + if (romEntry.arrayEntries.containsKey("StaticEggPokemonOffsets")) { + staticEggOffsets = romEntry.arrayEntries.get("StaticEggPokemonOffsets"); + } + for (int i = 0; i < staticsHere.size(); i++) { + int currentOffset = i; + StaticPokemon staticPK = staticsHere.get(i); + StaticEncounter se = new StaticEncounter(); + se.pkmn = staticPK.getPokemon(this); + se.level = staticPK.getLevel(rom, 0); + se.isEgg = Arrays.stream(staticEggOffsets).anyMatch(x-> x == currentOffset); + statics.add(se); + } + + if (romEntry.codeTweaks.get("StaticFirstBattleTweak") != null) { + // Read in and randomize the static starting Poochyena/Zigzagoon fight in RSE + int startingSpeciesOffset = romEntry.getValue("StaticFirstBattleSpeciesOffset"); + int species = readWord(startingSpeciesOffset); + if (species == 0xFFFF) { + // Patch hasn't been applied, so apply it first + try { + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("StaticFirstBattleTweak")); + species = readWord(startingSpeciesOffset); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + Pokemon pkmn = pokesInternal[species]; + int startingLevelOffset = romEntry.getValue("StaticFirstBattleLevelOffset"); + int level = rom[startingLevelOffset]; + StaticEncounter se = new StaticEncounter(); + se.pkmn = pkmn; + se.level = level; + statics.add(se); + } else if (romEntry.codeTweaks.get("GhostMarowakTweak") != null) { + // Read in and randomize the static Ghost Marowak fight in FRLG + int[] ghostMarowakOffsets = romEntry.arrayEntries.get("GhostMarowakSpeciesOffsets"); + int species = readWord(ghostMarowakOffsets[0]); + if (species == 0xFFFF) { + // Patch hasn't been applied, so apply it first + try { + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("GhostMarowakTweak")); + species = readWord(ghostMarowakOffsets[0]); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + Pokemon pkmn = pokesInternal[species]; + int[] startingLevelOffsets = romEntry.arrayEntries.get("GhostMarowakLevelOffsets"); + int level = rom[startingLevelOffsets[0]]; + StaticEncounter se = new StaticEncounter(); + se.pkmn = pkmn; + se.level = level; + statics.add(se); + } + + try { + getRoamers(statics); + } catch (Exception e) { + throw new RandomizerIOException(e); + } + + return statics; + } + + @Override + public boolean setStaticPokemon(List<StaticEncounter> staticPokemon) { + // Support Deoxys/Mew gifts/catches in E/FR/LG + attemptObedienceEvolutionPatches(); + + List<StaticPokemon> staticsHere = romEntry.staticPokemon; + int roamerSize = romEntry.roamingPokemon.size(); + if (romEntry.romType == Gen3Constants.RomType_Em) { + // Emerald roamers are set as linkedEncounters to their respective + // Southern Island statics and thus don't count. + roamerSize = 0; + } + int hardcodedStaticSize = 0; + if (romEntry.codeTweaks.get("StaticFirstBattleTweak") != null || romEntry.codeTweaks.get("GhostMarowakTweak") != null) { + hardcodedStaticSize = 1; + } + + if (staticPokemon.size() != staticsHere.size() + hardcodedStaticSize + roamerSize) { + return false; + } + + for (int i = 0; i < staticsHere.size(); i++) { + staticsHere.get(i).setPokemon(this, staticPokemon.get(i).pkmn); + staticsHere.get(i).setLevel(rom, staticPokemon.get(i).level, 0); + } + + if (romEntry.codeTweaks.get("StaticFirstBattleTweak") != null) { + StaticEncounter startingFirstBattle = staticPokemon.get(romEntry.getValue("StaticFirstBattleOffset")); + int startingSpeciesOffset = romEntry.getValue("StaticFirstBattleSpeciesOffset"); + writeWord(startingSpeciesOffset, pokedexToInternal[startingFirstBattle.pkmn.number]); + int startingLevelOffset = romEntry.getValue("StaticFirstBattleLevelOffset"); + rom[startingLevelOffset] = (byte) startingFirstBattle.level; + } else if (romEntry.codeTweaks.get("GhostMarowakTweak") != null) { + StaticEncounter ghostMarowak = staticPokemon.get(romEntry.getValue("GhostMarowakOffset")); + int[] ghostMarowakSpeciesOffsets = romEntry.arrayEntries.get("GhostMarowakSpeciesOffsets"); + for (int i = 0; i < ghostMarowakSpeciesOffsets.length; i++) { + writeWord(ghostMarowakSpeciesOffsets[i], pokedexToInternal[ghostMarowak.pkmn.number]); + } + int[] ghostMarowakLevelOffsets = romEntry.arrayEntries.get("GhostMarowakLevelOffsets"); + for (int i = 0; i < ghostMarowakLevelOffsets.length; i++) { + rom[ghostMarowakLevelOffsets[i]] = (byte) ghostMarowak.level; + } + + // The code for creating Ghost Marowak tries to ensure the Pokemon is female. If the Pokemon + // cannot be female (because they are always male or an indeterminate gender), then the game + // will infinite loop trying and failing to make the Pokemon female. For Pokemon that cannot + // be female, change the specified gender to something that actually works. + int ghostMarowakGenderOffset = romEntry.getValue("GhostMarowakGenderOffset"); + if (ghostMarowak.pkmn.genderRatio == 0 || ghostMarowak.pkmn.genderRatio == 0xFF) { + // 0x00 is 100% male, and 0xFF is indeterminate gender + rom[ghostMarowakGenderOffset] = (byte) ghostMarowak.pkmn.genderRatio; + } + } + + setRoamers(staticPokemon); + return true; + } + + private void getRoamers(List<StaticEncounter> statics) throws IOException { + if (romEntry.romType == Gen3Constants.RomType_Ruby) { + int firstSpecies = readWord(rom, romEntry.roamingPokemon.get(0).speciesOffsets[0]); + if (firstSpecies == 0) { + // Before applying the patch, the first species offset will be pointing to + // the lower bytes of 0x2000000, so when it reads a word, it will be 0. + applyRubyRoamerPatch(); + } + StaticPokemon roamer = romEntry.roamingPokemon.get(0); + StaticEncounter se = new StaticEncounter(); + se.pkmn = roamer.getPokemon(this); + se.level = roamer.getLevel(rom, 0); + statics.add(se); + } else if (romEntry.romType == Gen3Constants.RomType_Sapp) { + StaticPokemon roamer = romEntry.roamingPokemon.get(0); + StaticEncounter se = new StaticEncounter(); + se.pkmn = roamer.getPokemon(this); + se.level = roamer.getLevel(rom, 0); + statics.add(se); + } else if (romEntry.romType == Gen3Constants.RomType_FRLG && romEntry.codeTweaks.get("RoamingPokemonTweak") != null) { + int firstSpecies = readWord(rom, romEntry.roamingPokemon.get(0).speciesOffsets[0]); + if (firstSpecies == 0xFFFF) { + // This means that the IPS patch hasn't been applied yet, since the first species + // ID location is free space. + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("RoamingPokemonTweak")); + } + for (int i = 0; i < romEntry.roamingPokemon.size(); i++) { + StaticPokemon roamer = romEntry.roamingPokemon.get(i); + StaticEncounter se = new StaticEncounter(); + se.pkmn = roamer.getPokemon(this); + se.level = roamer.getLevel(rom, 0); + statics.add(se); + } + } else if (romEntry.romType == Gen3Constants.RomType_Em) { + int firstSpecies = readWord(rom, romEntry.roamingPokemon.get(0).speciesOffsets[0]); + if (firstSpecies >= pokesInternal.length) { + // Before applying the patch, the first species offset is a pointer with a huge value. + // Thus, this check is a good indicator that the patch needs to be applied. + applyEmeraldRoamerPatch(); + } + int[] southernIslandOffsets = romEntry.arrayEntries.get("StaticSouthernIslandOffsets"); + for (int i = 0; i < romEntry.roamingPokemon.size(); i++) { + StaticPokemon roamer = romEntry.roamingPokemon.get(i); + StaticEncounter se = new StaticEncounter(); + se.pkmn = roamer.getPokemon(this); + se.level = roamer.getLevel(rom, 0); + + // Link each roamer to their respective Southern Island static encounter so that + // they randomize to the same species. + StaticEncounter southernIslandEncounter = statics.get(southernIslandOffsets[i]); + southernIslandEncounter.linkedEncounters.add(se); + } + } + } + + private void setRoamers(List<StaticEncounter> statics) { + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + StaticEncounter roamerEncounter = statics.get(statics.size() - 1); + StaticPokemon roamer = romEntry.roamingPokemon.get(0); + roamer.setPokemon(this, roamerEncounter.pkmn); + for (int i = 0; i < roamer.levelOffsets.length; i++) { + roamer.setLevel(rom, roamerEncounter.level, i); + } + } else if (romEntry.romType == Gen3Constants.RomType_FRLG && romEntry.codeTweaks.get("RoamingPokemonTweak") != null) { + for (int i = 0; i < romEntry.roamingPokemon.size(); i++) { + int offsetInStaticList = statics.size() - 3 + i; + StaticEncounter roamerEncounter = statics.get(offsetInStaticList); + StaticPokemon roamer = romEntry.roamingPokemon.get(i); + roamer.setPokemon(this, roamerEncounter.pkmn); + for (int j = 0; j < roamer.levelOffsets.length; j++) { + roamer.setLevel(rom, roamerEncounter.level, j); + } + } + } else if (romEntry.romType == Gen3Constants.RomType_Em) { + int[] southernIslandOffsets = romEntry.arrayEntries.get("StaticSouthernIslandOffsets"); + for (int i = 0; i < romEntry.roamingPokemon.size(); i++) { + StaticEncounter southernIslandEncounter = statics.get(southernIslandOffsets[i]); + StaticEncounter roamerEncounter = southernIslandEncounter.linkedEncounters.get(0); + StaticPokemon roamer = romEntry.roamingPokemon.get(i); + roamer.setPokemon(this, roamerEncounter.pkmn); + for (int j = 0; j < roamer.levelOffsets.length; j++) { + roamer.setLevel(rom, roamerEncounter.level, j); + } + } + } + } + + private void applyRubyRoamerPatch() { + int offset = romEntry.getValue("FindMapsWithMonFunctionStartOffset"); + + // The constant 0x2000000 is actually in the function twice, so we'll replace the first instance + // with Latios's ID. First, change the "ldr r2, [pc, #0x68]" near the start of the function to + // "ldr r2, [pc, #0x15C]" so it points to the second usage of 0x2000000 + rom[offset + 22] = 0x57; + + // In the space formerly occupied by the first 0x2000000, write Latios's ID + FileFunctions.writeFullInt(rom, offset + 128, pokedexToInternal[Species.latios]); + + // Where the original function computes Latios's ID by setting r0 to 0xCC << 1, just pc-relative + // load our constant. We have four bytes of space to play with, and we need to make sure the offset + // from the pc is 4-byte aligned; we need to nop for alignment and then perform the load. + rom[offset + 12] = 0x00; + rom[offset + 13] = 0x00; + rom[offset + 14] = 0x1C; + rom[offset + 15] = 0x48; + + offset = romEntry.getValue("CreateInitialRoamerMonFunctionStartOffset"); + + // At the very end of the function, the game pops the lr from the stack and stores it in r0, then + // it does "bx r0" to jump back to the caller, and then it has two bytes of padding afterwards. For + // some reason, Ruby very rarely does "pop { pc }" even though that seemingly works fine. By doing + // that, we only need one instruction to return to the caller, giving us four bytes to write + // Latios's species ID. + rom[offset + 182] = 0x00; + rom[offset + 183] = (byte) 0xBD; + FileFunctions.writeFullInt(rom, offset + 184, pokedexToInternal[Species.latios]); + + // Now write a pc-relative load to this new species ID constant over the original move and lsl. Similar + // to before, we need to write a nop first for alignment, then pc-relative load into r6. + rom[offset + 10] = 0x00; + rom[offset + 11] = 0x00; + rom[offset + 12] = 0x2A; + rom[offset + 13] = 0x4E; + } + + private void applyEmeraldRoamerPatch() { + int offset = romEntry.getValue("CreateInitialRoamerMonFunctionStartOffset"); + + // Latias's species ID is already a pc-relative loaded constant, but Latios's isn't. We need to make + // some room for it; the constant 0x03005D8C is actually in the function twice, so we'll replace the first + // instance with Latios's ID. First, change the "ldr r0, [pc, #0xC]" at the start of the function to + // "ldr r0, [pc, #0x104]", so it points to the second usage of 0x03005D8C + rom[offset + 14] = 0x41; + + // In the space formerly occupied by the first 0x03005D8C, write Latios's ID + FileFunctions.writeFullInt(rom, offset + 28, pokedexToInternal[Species.latios]); + + // In the original function, we "lsl r0, r0, #0x10" then compare r0 to 0. The thing is, this left + // shift doesn't actually matter, because 0 << 0x10 = 0, and [non-zero] << 0x10 = [non-zero]. + // Let's move the compare up to take its place and then load Latios's ID into r3 for use in another + // branch later. + rom[offset + 8] = 0x00; + rom[offset + 9] = 0x28; + rom[offset + 10] = 0x04; + rom[offset + 11] = 0x4B; + + // Lastly, in the branch that normally does r2 = 0xCC << 0x1 to compute Latios's ID, just mov r3 + // into r2, since it was loaded with his ID with the above code. + rom[offset + 48] = 0x1A; + rom[offset + 49] = 0x46; + rom[offset + 50] = 0x00; + rom[offset + 51] = 0x00; + } + + @Override + public List<Integer> getTMMoves() { + List<Integer> tms = new ArrayList<>(); + int offset = romEntry.getValue("TmMoves"); + for (int i = 1; i <= Gen3Constants.tmCount; i++) { + tms.add(readWord(offset + (i - 1) * 2)); + } + return tms; + } + + @Override + public List<Integer> getHMMoves() { + return Gen3Constants.hmMoves; + } + + @Override + public void setTMMoves(List<Integer> moveIndexes) { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + int offset = romEntry.getValue("TmMoves"); + for (int i = 1; i <= Gen3Constants.tmCount; i++) { + writeWord(offset + (i - 1) * 2, moveIndexes.get(i - 1)); + } + int otherOffset = romEntry.getValue("TmMovesDuplicate"); + if (otherOffset > 0) { + // Emerald/FR/LG have *two* TM tables + System.arraycopy(rom, offset, rom, otherOffset, Gen3Constants.tmCount * 2); + } + + int iiOffset = romEntry.getValue("ItemImages"); + if (iiOffset > 0) { + int[] pals = romEntry.arrayEntries.get("TmPals"); + // Update the item image palettes + // Gen3 TMs are 289-338 + for (int i = 0; i < 50; i++) { + Move mv = moves[moveIndexes.get(i)]; + int typeID = Gen3Constants.typeToByte(mv.type); + writePointer(iiOffset + (Gen3Constants.tmItemOffset + i) * 8 + 4, pals[typeID]); + } + } + + int fsOffset = romEntry.getValue("FreeSpace"); + + // Item descriptions + if (romEntry.getValue("MoveDescriptions") > 0) { + // JP blocked for now - uses different item structure anyway + int idOffset = romEntry.getValue("ItemData"); + int mdOffset = romEntry.getValue("MoveDescriptions"); + int entrySize = romEntry.getValue("ItemEntrySize"); + int limitPerLine = (romEntry.romType == Gen3Constants.RomType_FRLG) ? Gen3Constants.frlgItemDescCharsPerLine + : Gen3Constants.rseItemDescCharsPerLine; + for (int i = 0; i < Gen3Constants.tmCount; i++) { + int itemBaseOffset = idOffset + (i + Gen3Constants.tmItemOffset) * entrySize; + int moveBaseOffset = mdOffset + (moveIndexes.get(i) - 1) * 4; + int moveTextPointer = readPointer(moveBaseOffset); + String moveDesc = readVariableLengthString(moveTextPointer); + String newItemDesc = RomFunctions.rewriteDescriptionForNewLineSize(moveDesc, "\\n", limitPerLine, ssd); + // Find freespace + int fsBytesNeeded = translateString(newItemDesc).length + 1; + int newItemDescOffset = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, fsBytesNeeded, + fsOffset); + if (newItemDescOffset < fsOffset) { + String nl = System.getProperty("line.separator"); + log("Couldn't insert new item description." + nl); + return; + } + writeVariableLengthString(newItemDesc, newItemDescOffset); + writePointer(itemBaseOffset + Gen3Constants.itemDataDescriptionOffset, newItemDescOffset); + } + } + + // TM Text? + for (TMOrMTTextEntry tte : romEntry.tmmtTexts) { + if (tte.actualOffset > 0 && !tte.isMoveTutor) { + // create the new TM text + int oldPointer = readPointer(tte.actualOffset); + if (oldPointer < 0 || oldPointer >= rom.length) { + String nl = System.getProperty("line.separator"); + log("Couldn't insert new TM text. Skipping remaining TM text updates." + nl); + return; + } + String moveName = this.moves[moveIndexes.get(tte.number - 1)].name; + // temporarily use underscores to stop the move name being split + String tmpMoveName = moveName.replace(' ', '_'); + String unformatted = tte.template.replace("[move]", tmpMoveName); + String newText = RomFunctions.formatTextWithReplacements(unformatted, null, "\\n", "\\l", "\\p", + Gen3Constants.regularTextboxCharsPerLine, ssd); + // get rid of the underscores + newText = newText.replace(tmpMoveName, moveName); + // insert the new text into free space + int fsBytesNeeded = translateString(newText).length + 1; + int newOffset = RomFunctions.freeSpaceFinder(rom, (byte) 0xFF, fsBytesNeeded, fsOffset); + if (newOffset < fsOffset) { + String nl = System.getProperty("line.separator"); + log("Couldn't insert new TM text." + nl); + return; + } + writeVariableLengthString(newText, newOffset); + // search for copies of the pointer: + // make a needle of the pointer + byte[] searchNeedle = new byte[4]; + System.arraycopy(rom, tte.actualOffset, searchNeedle, 0, 4); + // find copies within 500 bytes either way of actualOffset + int minOffset = Math.max(0, tte.actualOffset - Gen3Constants.pointerSearchRadius); + int maxOffset = Math.min(rom.length, tte.actualOffset + Gen3Constants.pointerSearchRadius); + List<Integer> pointerLocs = RomFunctions.search(rom, minOffset, maxOffset, searchNeedle); + for (int pointerLoc : pointerLocs) { + // write the new pointer + writePointer(pointerLoc, newOffset); + } + } + } + } + + private RomFunctions.StringSizeDeterminer ssd = encodedText -> translateString(encodedText).length; + + @Override + public int getTMCount() { + return Gen3Constants.tmCount; + } + + @Override + public int getHMCount() { + return Gen3Constants.hmCount; + } + + @Override + public Map<Pokemon, boolean[]> getTMHMCompatibility() { + Map<Pokemon, boolean[]> compat = new TreeMap<>(); + int offset = romEntry.getValue("PokemonTMHMCompat"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pkmn = pokemonList.get(i); + int compatOffset = offset + (pokedexToInternal[pkmn.number]) * 8; + boolean[] flags = new boolean[Gen3Constants.tmCount + Gen3Constants.hmCount + 1]; + for (int j = 0; j < 8; j++) { + readByteIntoFlags(flags, j * 8 + 1, compatOffset + j); + } + compat.put(pkmn, flags); + } + return compat; + } + + @Override + public void setTMHMCompatibility(Map<Pokemon, boolean[]> compatData) { + int offset = romEntry.getValue("PokemonTMHMCompat"); + for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) { + Pokemon pkmn = compatEntry.getKey(); + boolean[] flags = compatEntry.getValue(); + int compatOffset = offset + (pokedexToInternal[pkmn.number]) * 8; + for (int j = 0; j < 8; j++) { + rom[compatOffset + j] = getByteFromFlags(flags, j * 8 + 1); + } + } + } + + @Override + public boolean hasMoveTutors() { + return (romEntry.romType == Gen3Constants.RomType_Em || romEntry.romType == Gen3Constants.RomType_FRLG); + } + + @Override + public List<Integer> getMoveTutorMoves() { + if (!hasMoveTutors()) { + return new ArrayList<>(); + } + List<Integer> mts = new ArrayList<>(); + int moveCount = romEntry.getValue("MoveTutorMoves"); + int offset = romEntry.getValue("MoveTutorData"); + for (int i = 0; i < moveCount; i++) { + mts.add(readWord(offset + i * 2)); + } + return mts; + } + + @Override + public void setMoveTutorMoves(List<Integer> moves) { + if (!hasMoveTutors()) { + return; + } + int moveCount = romEntry.getValue("MoveTutorMoves"); + int offset = romEntry.getValue("MoveTutorData"); + if (moveCount != moves.size()) { + return; + } + for (int i = 0; i < moveCount; i++) { + writeWord(offset + i * 2, moves.get(i)); + } + int fsOffset = romEntry.getValue("FreeSpace"); + + // Move Tutor Text? + for (TMOrMTTextEntry tte : romEntry.tmmtTexts) { + if (tte.actualOffset > 0 && tte.isMoveTutor) { + // create the new MT text + int oldPointer = readPointer(tte.actualOffset); + if (oldPointer < 0 || oldPointer >= rom.length) { + throw new RandomizationException( + "Move Tutor Text update failed: couldn't read a move tutor text pointer."); + } + String moveName = this.moves[moves.get(tte.number)].name; + // temporarily use underscores to stop the move name being split + String tmpMoveName = moveName.replace(' ', '_'); + String unformatted = tte.template.replace("[move]", tmpMoveName); + String newText = RomFunctions.formatTextWithReplacements(unformatted, null, "\\n", "\\l", "\\p", + Gen3Constants.regularTextboxCharsPerLine, ssd); + // get rid of the underscores + newText = newText.replace(tmpMoveName, moveName); + // insert the new text into free space + int fsBytesNeeded = translateString(newText).length + 1; + int newOffset = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, fsBytesNeeded, fsOffset); + if (newOffset < fsOffset) { + String nl = System.getProperty("line.separator"); + log("Couldn't insert new Move Tutor text." + nl); + return; + } + writeVariableLengthString(newText, newOffset); + // search for copies of the pointer: + // make a needle of the pointer + byte[] searchNeedle = new byte[4]; + System.arraycopy(rom, tte.actualOffset, searchNeedle, 0, 4); + // find copies within 500 bytes either way of actualOffset + int minOffset = Math.max(0, tte.actualOffset - Gen3Constants.pointerSearchRadius); + int maxOffset = Math.min(rom.length, tte.actualOffset + Gen3Constants.pointerSearchRadius); + List<Integer> pointerLocs = RomFunctions.search(rom, minOffset, maxOffset, searchNeedle); + for (int pointerLoc : pointerLocs) { + // write the new pointer + writePointer(pointerLoc, newOffset); + } + } + } + } + + @Override + public Map<Pokemon, boolean[]> getMoveTutorCompatibility() { + if (!hasMoveTutors()) { + return new TreeMap<>(); + } + Map<Pokemon, boolean[]> compat = new TreeMap<>(); + int moveCount = romEntry.getValue("MoveTutorMoves"); + int offset = romEntry.getValue("MoveTutorCompatibility"); + int bytesRequired = ((moveCount + 7) & ~7) / 8; + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pkmn = pokemonList.get(i); + int compatOffset = offset + pokedexToInternal[pkmn.number] * bytesRequired; + boolean[] flags = new boolean[moveCount + 1]; + for (int j = 0; j < bytesRequired; j++) { + readByteIntoFlags(flags, j * 8 + 1, compatOffset + j); + } + compat.put(pkmn, flags); + } + return compat; + } + + @Override + public void setMoveTutorCompatibility(Map<Pokemon, boolean[]> compatData) { + if (!hasMoveTutors()) { + return; + } + int moveCount = romEntry.getValue("MoveTutorMoves"); + int offset = romEntry.getValue("MoveTutorCompatibility"); + int bytesRequired = ((moveCount + 7) & ~7) / 8; + for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) { + Pokemon pkmn = compatEntry.getKey(); + boolean[] flags = compatEntry.getValue(); + int compatOffset = offset + pokedexToInternal[pkmn.number] * bytesRequired; + for (int j = 0; j < bytesRequired; j++) { + rom[compatOffset + j] = getByteFromFlags(flags, j * 8 + 1); + } + } + } + + @Override + public String getROMName() { + return romEntry.name; + } + + @Override + public String getROMCode() { + return romEntry.romCode; + } + + @Override + public String getSupportLevel() { + return (romEntry.getValue("StaticPokemonSupport") > 0) ? "Complete" : "No Static Pokemon"; + } + + // For dynamic offsets later + private int find(String hexString) { + return find(rom, hexString); + } + + private static int find(byte[] haystack, String hexString) { + if (hexString.length() % 2 != 0) { + return -3; // error + } + byte[] searchFor = new byte[hexString.length() / 2]; + for (int i = 0; i < searchFor.length; i++) { + searchFor[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16); + } + List<Integer> found = RomFunctions.search(haystack, searchFor); + if (found.size() == 0) { + return -1; // not found + } else if (found.size() > 1) { + return -2; // not unique + } else { + return found.get(0); + } + } + + private List<Integer> findMultiple(String hexString) { + return findMultiple(rom, hexString); + } + + private static List<Integer> findMultiple(byte[] haystack, String hexString) { + if (hexString.length() % 2 != 0) { + return new ArrayList<>(); // error + } + byte[] searchFor = new byte[hexString.length() / 2]; + for (int i = 0; i < searchFor.length; i++) { + searchFor[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16); + } + return RomFunctions.search(haystack, searchFor); + } + + private void writeHexString(String hexString, int offset) { + if (hexString.length() % 2 != 0) { + return; // error + } + for (int i = 0; i < hexString.length() / 2; i++) { + rom[offset + i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16); + } + } + + private void attemptObedienceEvolutionPatches() { + if (havePatchedObedience) { + return; + } + + havePatchedObedience = true; + // This routine *appears* to only exist in E/FR/LG... + // Look for the deoxys part which is + // MOVS R1, 0x19A + // CMP R0, R1 + // BEQ <mew/deoxys case> + // Hex is CD214900 8842 0FD0 + int deoxysObOffset = find(Gen3Constants.deoxysObeyCode); + if (deoxysObOffset > 0) { + // We found the deoxys check... + // Replacing it with MOVS R1, 0x0 would work fine. + // This would make it so species 0x0 (glitch only) would disobey. + // But MOVS R1, 0x0 (the version I know) is 2-byte + // So we just use it twice... + // the equivalent of nop'ing the second time. + rom[deoxysObOffset] = 0x00; + rom[deoxysObOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + rom[deoxysObOffset + 2] = 0x00; + rom[deoxysObOffset + 3] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + // Look for the mew check too... it's 0x16 ahead + if (readWord(deoxysObOffset + Gen3Constants.mewObeyOffsetFromDeoxysObey) == (((Gen3Constants.gbaCmpRxOpcode | Gen3Constants.gbaR0) << 8) | (Species.mew))) { + // Bingo, thats CMP R0, 0x97 + // change to CMP R0, 0x0 + writeWord(deoxysObOffset + Gen3Constants.mewObeyOffsetFromDeoxysObey, + (((Gen3Constants.gbaCmpRxOpcode | Gen3Constants.gbaR0) << 8) | (0))); + } + } + + // Look for evolutions too + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + int evoJumpOffset = find(Gen3Constants.levelEvoKantoDexCheckCode); + if (evoJumpOffset > 0) { + // This currently compares species to 0x97 and then allows + // evolution if it's <= that. + // Allow it regardless by using an unconditional jump instead + writeWord(evoJumpOffset, Gen3Constants.gbaNopOpcode); + writeWord(evoJumpOffset + 2, + ((Gen3Constants.gbaUnconditionalJumpOpcode << 8) | (Gen3Constants.levelEvoKantoDexJumpAmount))); + } + + int stoneJumpOffset = find(Gen3Constants.stoneEvoKantoDexCheckCode); + if (stoneJumpOffset > 0) { + // same as the above, but for stone evos + writeWord(stoneJumpOffset, Gen3Constants.gbaNopOpcode); + writeWord(stoneJumpOffset + 2, + ((Gen3Constants.gbaUnconditionalJumpOpcode << 8) | (Gen3Constants.stoneEvoKantoDexJumpAmount))); + } + } + } + + private void patchForNationalDex() { + log("--Patching for National Dex at Start of Game--"); + String nl = System.getProperty("line.separator"); + int fso = romEntry.getValue("FreeSpace"); + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + // Find the original pokedex script + int pkDexOffset = find(Gen3Constants.rsPokedexScriptIdentifier); + if (pkDexOffset < 0) { + log("Patch unsuccessful." + nl); + return; + } + int textPointer = readPointer(pkDexOffset - 4); + int realScriptLocation = pkDexOffset - 8; + int pointerLocToScript = find(pointerToHexString(realScriptLocation)); + if (pointerLocToScript < 0) { + log("Patch unsuccessful." + nl); + return; + } + // Find free space for our new routine + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, 44, fso); + if (writeSpace < fso) { + log("Patch unsuccessful." + nl); + // Somehow this ROM is full + return; + } + writePointer(pointerLocToScript, writeSpace); + writeHexString(Gen3Constants.rsNatDexScriptPart1, writeSpace); + writePointer(writeSpace + 4, textPointer); + writeHexString(Gen3Constants.rsNatDexScriptPart2, writeSpace + 8); + + } else if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // Find the original pokedex script + int pkDexOffset = find(Gen3Constants.frlgPokedexScriptIdentifier); + if (pkDexOffset < 0) { + log("Patch unsuccessful." + nl); + return; + } + // Find free space for our new routine + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, 10, fso); + if (writeSpace < fso) { + // Somehow this ROM is full + log("Patch unsuccessful." + nl); + return; + } + rom[pkDexOffset] = 4; + writePointer(pkDexOffset + 1, writeSpace); + rom[pkDexOffset + 5] = 0; // NOP + + // Now write our new routine + writeHexString(Gen3Constants.frlgNatDexScript, writeSpace); + + // Fix people using the national dex flag + List<Integer> ndexChecks = findMultiple(Gen3Constants.frlgNatDexFlagChecker); + for (int ndexCheckOffset : ndexChecks) { + // change to a flag-check + // 82C = "beaten e4/gary once" + writeHexString(Gen3Constants.frlgE4FlagChecker, ndexCheckOffset); + } + + // Fix oak in his lab + int oakLabCheckOffs = find(Gen3Constants.frlgOaksLabKantoDexChecker); + if (oakLabCheckOffs > 0) { + // replace it + writeHexString(Gen3Constants.frlgOaksLabFix, oakLabCheckOffs); + } + + // Fix oak outside your house + int oakHouseCheckOffs = find(Gen3Constants.frlgOakOutsideHouseCheck); + if (oakHouseCheckOffs > 0) { + // fix him to use ndex count + writeHexString(Gen3Constants.frlgOakOutsideHouseFix, oakHouseCheckOffs); + } + + // Fix Oak's aides so they look for your National Dex seen/caught, + // not your Kanto Dex seen/caught + int oakAideCheckOffs = find(Gen3Constants.frlgOakAideCheckPrefix); + if (oakAideCheckOffs > 0) { + oakAideCheckOffs += Gen3Constants.frlgOakAideCheckPrefix.length() / 2; // because it was a prefix + // Change the bne instruction to an unconditional branch to always use National Dex + rom[oakAideCheckOffs + 1] = (byte) 0xE0; + } + } else { + // Find the original pokedex script + int pkDexOffset = find(Gen3Constants.ePokedexScriptIdentifier); + if (pkDexOffset < 0) { + log("Patch unsuccessful." + nl); + return; + } + int textPointer = readPointer(pkDexOffset - 4); + int realScriptLocation = pkDexOffset - 8; + int pointerLocToScript = find(pointerToHexString(realScriptLocation)); + if (pointerLocToScript < 0) { + log("Patch unsuccessful." + nl); + return; + } + // Find free space for our new routine + int writeSpace = RomFunctions.freeSpaceFinder(rom, Gen3Constants.freeSpaceByte, 27, fso); + if (writeSpace < fso) { + // Somehow this ROM is full + log("Patch unsuccessful." + nl); + return; + } + writePointer(pointerLocToScript, writeSpace); + writeHexString(Gen3Constants.eNatDexScriptPart1, writeSpace); + writePointer(writeSpace + 4, textPointer); + writeHexString(Gen3Constants.eNatDexScriptPart2, writeSpace + 8); + } + log("Patch successful!" + nl); + } + + private String pointerToHexString(int pointer) { + String hex = String.format("%08X", pointer + 0x08000000); + return new String(new char[] { hex.charAt(6), hex.charAt(7), hex.charAt(4), hex.charAt(5), hex.charAt(2), + hex.charAt(3), hex.charAt(0), hex.charAt(1) }); + } + + private void populateEvolutions() { + for (Pokemon pkmn : pokes) { + if (pkmn != null) { + pkmn.evolutionsFrom.clear(); + pkmn.evolutionsTo.clear(); + } + } + + int baseOffset = romEntry.getValue("PokemonEvolutions"); + int numInternalPokes = romEntry.getValue("PokemonCount"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pk = pokemonList.get(i); + int idx = pokedexToInternal[pk.number]; + int evoOffset = baseOffset + (idx) * 0x28; + for (int j = 0; j < 5; j++) { + int method = readWord(evoOffset + j * 8); + int evolvingTo = readWord(evoOffset + j * 8 + 4); + if (method >= 1 && method <= Gen3Constants.evolutionMethodCount && evolvingTo >= 1 + && evolvingTo <= numInternalPokes) { + int extraInfo = readWord(evoOffset + j * 8 + 2); + EvolutionType et = EvolutionType.fromIndex(3, method); + Evolution evo = new Evolution(pk, pokesInternal[evolvingTo], true, et, extraInfo); + if (!pk.evolutionsFrom.contains(evo)) { + pk.evolutionsFrom.add(evo); + pokesInternal[evolvingTo].evolutionsTo.add(evo); + } + } + } + // Split evos shouldn't carry stats unless the evo is Nincada's + // In that case, we should have Ninjask carry stats + if (pk.evolutionsFrom.size() > 1) { + for (Evolution e : pk.evolutionsFrom) { + if (e.type != EvolutionType.LEVEL_CREATE_EXTRA) { + e.carryStats = false; + } + } + } + } + } + + private void writeEvolutions() { + int baseOffset = romEntry.getValue("PokemonEvolutions"); + for (int i = 1; i <= numRealPokemon; i++) { + Pokemon pk = pokemonList.get(i); + int idx = pokedexToInternal[pk.number]; + int evoOffset = baseOffset + (idx) * 0x28; + int evosWritten = 0; + for (Evolution evo : pk.evolutionsFrom) { + writeWord(evoOffset, evo.type.toIndex(3)); + writeWord(evoOffset + 2, evo.extraInfo); + writeWord(evoOffset + 4, pokedexToInternal[evo.to.number]); + writeWord(evoOffset + 6, 0); + evoOffset += 8; + evosWritten++; + if (evosWritten == 5) { + break; + } + } + while (evosWritten < 5) { + writeWord(evoOffset, 0); + writeWord(evoOffset + 2, 0); + writeWord(evoOffset + 4, 0); + writeWord(evoOffset + 6, 0); + evoOffset += 8; + evosWritten++; + } + } + } + + @Override + public void removeImpossibleEvolutions(Settings settings) { + attemptObedienceEvolutionPatches(); + + // no move evos, so no need to check for those + for (Pokemon pkmn : pokes) { + if (pkmn != null) { + for (Evolution evo : pkmn.evolutionsFrom) { + // Not trades, but impossible without trading + if (evo.type == EvolutionType.HAPPINESS_DAY && romEntry.romType == Gen3Constants.RomType_FRLG) { + // happiness day change to Sun Stone + evo.type = EvolutionType.STONE; + evo.extraInfo = Gen3Items.sunStone; + addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames[Gen3Items.sunStone]); + } + if (evo.type == EvolutionType.HAPPINESS_NIGHT && romEntry.romType == Gen3Constants.RomType_FRLG) { + // happiness night change to Moon Stone + evo.type = EvolutionType.STONE; + evo.extraInfo = Gen3Items.moonStone; + addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames[Gen3Items.moonStone]); + } + if (evo.type == EvolutionType.LEVEL_HIGH_BEAUTY && romEntry.romType == Gen3Constants.RomType_FRLG) { + // beauty change to level 35 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 35; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } + // Pure Trade + if (evo.type == EvolutionType.TRADE) { + // Haunter, Machoke, Kadabra, Graveler + // Make it into level 37, we're done. + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 37; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } + // Trade w/ Held Item + if (evo.type == EvolutionType.TRADE_ITEM) { + if (evo.from.number == Species.poliwhirl) { + // Poliwhirl: Lv 37 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 37; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } else if (evo.from.number == Species.slowpoke) { + // Slowpoke: Water Stone + evo.type = EvolutionType.STONE; + evo.extraInfo = Gen3Items.waterStone; + addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames[Gen3Items.waterStone]); + } else if (evo.from.number == Species.seadra) { + // Seadra: Lv 40 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 40; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } else if (evo.from.number == Species.clamperl + && evo.extraInfo == Gen3Items.deepSeaTooth) { + // Clamperl -> Huntail: Lv30 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 30; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } else if (evo.from.number == Species.clamperl + && evo.extraInfo == Gen3Items.deepSeaScale) { + // Clamperl -> Gorebyss: Water Stone + evo.type = EvolutionType.STONE; + evo.extraInfo = Gen3Items.waterStone; + addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames[Gen3Items.waterStone]); + } else { + // Onix, Scyther or Porygon: Lv30 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 30; + addEvoUpdateLevel(impossibleEvolutionUpdates, evo); + } + } + } + } + } + + } + + @Override + public void makeEvolutionsEasier(Settings settings) { + // Reduce the amount of happiness required to evolve. + int offset = find(rom, Gen3Constants.friendshipValueForEvoLocator); + if (offset > 0) { + // Amount of required happiness for HAPPINESS evolutions. + if (rom[offset] == (byte)219) { + rom[offset] = (byte)159; + } + // FRLG doesn't have code to handle time-based evolutions. + if (romEntry.romType != Gen3Constants.RomType_FRLG) { + // Amount of required happiness for HAPPINESS_DAY evolutions. + if (rom[offset + 38] == (byte)219) { + rom[offset + 38] = (byte)159; + } + // Amount of required happiness for HAPPINESS_NIGHT evolutions. + if (rom[offset + 66] == (byte)219) { + rom[offset + 66] = (byte)159; + } + } + } + } + + @Override + public void removeTimeBasedEvolutions() { + for (Pokemon pkmn : pokes) { + if (pkmn != null) { + for (Evolution evol : pkmn.evolutionsFrom) { + // In Gen 3, only Eevee has a time-based evolution. + if (evol.type == EvolutionType.HAPPINESS_DAY) { + // Eevee: Make sun stone => Espeon + evol.type = EvolutionType.STONE; + evol.extraInfo = Gen3Items.sunStone; + addEvoUpdateStone(timeBasedEvolutionUpdates, evol, itemNames[evol.extraInfo]); + } else if (evol.type == EvolutionType.HAPPINESS_NIGHT) { + // Eevee: Make moon stone => Umbreon + evol.type = EvolutionType.STONE; + evol.extraInfo = Gen3Items.moonStone; + addEvoUpdateStone(timeBasedEvolutionUpdates, evol, itemNames[evol.extraInfo]); + } + } + } + } + } + + @Override + public boolean hasShopRandomization() { + return true; + } + + @Override + public Map<Integer, Shop> getShopItems() { + List<String> shopNames = Gen3Constants.getShopNames(romEntry.romType); + List<Integer> mainGameShops = Arrays.stream(romEntry.arrayEntries.get("MainGameShops")).boxed().collect(Collectors.toList()); + List<Integer> skipShops = Arrays.stream(romEntry.arrayEntries.get("SkipShops")).boxed().collect(Collectors.toList()); + Map<Integer, Shop> shopItemsMap = new TreeMap<>(); + int[] shopItemOffsets = romEntry.arrayEntries.get("ShopItemOffsets"); + for (int i = 0; i < shopItemOffsets.length; i++) { + if (!skipShops.contains(i)) { + int offset = shopItemOffsets[i]; + List<Integer> items = new ArrayList<>(); + int val = FileFunctions.read2ByteInt(rom, offset); + while (val != 0x0000) { + items.add(val); + offset += 2; + val = FileFunctions.read2ByteInt(rom, offset); + } + Shop shop = new Shop(); + shop.items = items; + shop.name = shopNames.get(i); + shop.isMainGame = mainGameShops.contains(i); + shopItemsMap.put(i, shop); + } + } + return shopItemsMap; + } + + @Override + public void setShopItems(Map<Integer, Shop> shopItems) { + int[] shopItemOffsets = romEntry.arrayEntries.get("ShopItemOffsets"); + for (int i = 0; i < shopItemOffsets.length; i++) { + Shop thisShop = shopItems.get(i); + if (thisShop != null && thisShop.items != null) { + int offset = shopItemOffsets[i]; + Iterator<Integer> iterItems = thisShop.items.iterator(); + while (iterItems.hasNext()) { + FileFunctions.write2ByteInt(rom, offset, iterItems.next()); + offset += 2; + } + } + } + } + + @Override + public void setShopPrices() { + int itemDataOffset = romEntry.getValue("ItemData"); + int entrySize = romEntry.getValue("ItemEntrySize"); + int itemCount = romEntry.getValue("ItemCount"); + for (int i = 1; i < itemCount; i++) { + int balancedPrice = Gen3Constants.balancedItemPrices.get(i) * 10; + int offset = itemDataOffset + (i * entrySize) + 16; + FileFunctions.write2ByteInt(rom, offset, balancedPrice); + } + } + + @Override + public List<PickupItem> getPickupItems() { + List<PickupItem> pickupItems = new ArrayList<>(); + int pickupItemCount = romEntry.getValue("PickupItemCount"); + int sizeOfPickupEntry = romEntry.romType == Gen3Constants.RomType_Em ? 2 : 4; + + // If we haven't found the pickup table for this ROM already, find it. + if (pickupItemsTableOffset == 0) { + String pickupTableStartLocator = romEntry.getString("PickupTableStartLocator"); + int offset = find(pickupTableStartLocator); + if (offset > 0) { + pickupItemsTableOffset = offset; + } + } + + // Assuming we've found the pickup table, extract the items out of it. + if (pickupItemsTableOffset > 0) { + for (int i = 0; i < pickupItemCount; i++) { + int itemOffset = pickupItemsTableOffset + (sizeOfPickupEntry * i); + int item = FileFunctions.read2ByteInt(rom, itemOffset); + PickupItem pickupItem = new PickupItem(item); + pickupItems.add(pickupItem); + } + } + + // Assuming we got the items from the last step, fill out the probabilities based on the game. + if (pickupItems.size() > 0) { + if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + for (int levelRange = 0; levelRange < 10; levelRange++) { + pickupItems.get(0).probabilities[levelRange] = 30; + pickupItems.get(7).probabilities[levelRange] = 5; + pickupItems.get(8).probabilities[levelRange] = 4; + pickupItems.get(9).probabilities[levelRange] = 1; + for (int i = 1; i < 7; i++) { + pickupItems.get(i).probabilities[levelRange] = 10; + } + } + } else if (romEntry.romType == Gen3Constants.RomType_FRLG) { + for (int levelRange = 0; levelRange < 10; levelRange++) { + pickupItems.get(0).probabilities[levelRange] = 15; + for (int i = 1; i < 7; i++) { + pickupItems.get(i).probabilities[levelRange] = 10; + } + for (int i = 7; i < 11; i++) { + pickupItems.get(i).probabilities[levelRange] = 5; + } + for (int i = 11; i < 16; i++) { + pickupItems.get(i).probabilities[levelRange] = 1; + } + } + } else { + for (int levelRange = 0; levelRange < 10; levelRange++) { + int startingCommonItemOffset = levelRange; + int startingRareItemOffset = 18 + levelRange; + pickupItems.get(startingCommonItemOffset).probabilities[levelRange] = 30; + for (int i = 1; i < 7; i++) { + pickupItems.get(startingCommonItemOffset + i).probabilities[levelRange] = 10; + } + pickupItems.get(startingCommonItemOffset + 7).probabilities[levelRange] = 4; + pickupItems.get(startingCommonItemOffset + 8).probabilities[levelRange] = 4; + pickupItems.get(startingRareItemOffset).probabilities[levelRange] = 1; + pickupItems.get(startingRareItemOffset + 1).probabilities[levelRange] = 1; + } + } + } + return pickupItems; + } + + @Override + public void setPickupItems(List<PickupItem> pickupItems) { + int sizeOfPickupEntry = romEntry.romType == Gen3Constants.RomType_Em ? 2 : 4; + if (pickupItemsTableOffset > 0) { + for (int i = 0; i < pickupItems.size(); i++) { + int itemOffset = pickupItemsTableOffset + (sizeOfPickupEntry * i); + FileFunctions.write2ByteInt(rom, itemOffset, pickupItems.get(i).item); + } + } + } + + @Override + public boolean canChangeTrainerText() { + return true; + } + + @Override + public List<String> getTrainerNames() { + int baseOffset = romEntry.getValue("TrainerData"); + int amount = romEntry.getValue("TrainerCount"); + int entryLen = romEntry.getValue("TrainerEntrySize"); + List<String> theTrainers = new ArrayList<>(); + for (int i = 1; i < amount; i++) { + theTrainers.add(readVariableLengthString(baseOffset + i * entryLen + 4)); + } + return theTrainers; + } + + @Override + public void setTrainerNames(List<String> trainerNames) { + int baseOffset = romEntry.getValue("TrainerData"); + int amount = romEntry.getValue("TrainerCount"); + int entryLen = romEntry.getValue("TrainerEntrySize"); + int nameLen = romEntry.getValue("TrainerNameLength"); + Iterator<String> theTrainers = trainerNames.iterator(); + for (int i = 1; i < amount; i++) { + String newName = theTrainers.next(); + writeFixedLengthString(newName, baseOffset + i * entryLen + 4, nameLen); + } + + } + + @Override + public TrainerNameMode trainerNameMode() { + return TrainerNameMode.MAX_LENGTH; + } + + @Override + public List<Integer> getTCNameLengthsByTrainer() { + // not needed + return new ArrayList<>(); + } + + @Override + public int maxTrainerNameLength() { + return romEntry.getValue("TrainerNameLength") - 1; + } + + @Override + public List<String> getTrainerClassNames() { + int baseOffset = romEntry.getValue("TrainerClassNames"); + int amount = romEntry.getValue("TrainerClassCount"); + int length = romEntry.getValue("TrainerClassNameLength"); + List<String> trainerClasses = new ArrayList<>(); + for (int i = 0; i < amount; i++) { + trainerClasses.add(readVariableLengthString(baseOffset + i * length)); + } + return trainerClasses; + } + + @Override + public void setTrainerClassNames(List<String> trainerClassNames) { + int baseOffset = romEntry.getValue("TrainerClassNames"); + int amount = romEntry.getValue("TrainerClassCount"); + int length = romEntry.getValue("TrainerClassNameLength"); + Iterator<String> trainerClasses = trainerClassNames.iterator(); + for (int i = 0; i < amount; i++) { + writeFixedLengthString(trainerClasses.next(), baseOffset + i * length, length); + } + } + + @Override + public int maxTrainerClassNameLength() { + return romEntry.getValue("TrainerClassNameLength") - 1; + } + + @Override + public boolean fixedTrainerClassNamesLength() { + return false; + } + + @Override + public List<Integer> getDoublesTrainerClasses() { + int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses"); + List<Integer> doubles = new ArrayList<>(); + for (int tClass : doublesClasses) { + doubles.add(tClass); + } + return doubles; + } + + @Override + public boolean canChangeStaticPokemon() { + return (romEntry.getValue("StaticPokemonSupport") > 0); + } + + @Override + public boolean hasStaticAltFormes() { + return false; + } + + @Override + public boolean hasMainGameLegendaries() { + return romEntry.arrayEntries.get("MainGameLegendaries") != null; + } + + @Override + public List<Integer> getMainGameLegendaries() { + if (this.hasMainGameLegendaries()) { + return Arrays.stream(romEntry.arrayEntries.get("MainGameLegendaries")).boxed().collect(Collectors.toList()); + } + return new ArrayList<>(); + } + + @Override + public List<Integer> getSpecialMusicStatics() { + return Arrays.stream(romEntry.arrayEntries.get("SpecialMusicStatics")).boxed().collect(Collectors.toList()); + } + + @Override + public void applyCorrectStaticMusic(Map<Integer, Integer> specialMusicStaticChanges) { + List<Integer> replaced = new ArrayList<>(); + int newIndexToMusicPoolOffset; + + if (romEntry.codeTweaks.get("NewIndexToMusicTweak") != null) { + try { + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("NewIndexToMusicTweak")); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + newIndexToMusicPoolOffset = romEntry.getValue("NewIndexToMusicPoolOffset"); + + if (newIndexToMusicPoolOffset > 0) { + + for (int oldStatic: specialMusicStaticChanges.keySet()) { + int i = newIndexToMusicPoolOffset; + int index = internalToPokedex[readWord(rom, i)]; + while (index != oldStatic || replaced.contains(i)) { + i += 4; + index = internalToPokedex[readWord(rom, i)]; + } + writeWord(rom, i, pokedexToInternal[specialMusicStaticChanges.get(oldStatic)]); + replaced.add(i); + } + } + } + } + + @Override + public boolean hasStaticMusicFix() { + return romEntry.codeTweaks.get("NewIndexToMusicTweak") != null; + } + + @Override + public List<TotemPokemon> getTotemPokemon() { + return new ArrayList<>(); + } + + @Override + public void setTotemPokemon(List<TotemPokemon> totemPokemon) { + + } + + @Override + public String getDefaultExtension() { + return "gba"; + } + + @Override + public int abilitiesPerPokemon() { + return 2; + } + + @Override + public int highestAbilityIndex() { + return Gen3Constants.highestAbilityIndex; + } + + private void loadAbilityNames() { + int nameoffs = romEntry.getValue("AbilityNames"); + int namelen = romEntry.getValue("AbilityNameLength"); + abilityNames = new String[Gen3Constants.highestAbilityIndex + 1]; + for (int i = 0; i <= Gen3Constants.highestAbilityIndex; i++) { + abilityNames[i] = readFixedLengthString(nameoffs + namelen * i, namelen); + } + } + + @Override + public String abilityName(int number) { + return abilityNames[number]; + } + + @Override + public Map<Integer, List<Integer>> getAbilityVariations() { + return Gen3Constants.abilityVariations; + } + + @Override + public List<Integer> getUselessAbilities() { + return new ArrayList<>(Gen3Constants.uselessAbilities); + } + + @Override + public int getAbilityForTrainerPokemon(TrainerPokemon tp) { + // In Gen 3, Trainer Pokemon *always* use the first Ability, no matter what + return tp.pokemon.ability1; + } + + @Override + public boolean hasMegaEvolutions() { + return false; + } + + @Override + public int internalStringLength(String string) { + return translateString(string).length; + } + + @Override + public void randomizeIntroPokemon() { + // FRLG + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + // intro sprites : first 255 only due to size + Pokemon introPk = randomPokemonLimited(255, false); + if (introPk == null) { + return; + } + int introPokemon = pokedexToInternal[introPk.number]; + int frontSprites = romEntry.getValue("FrontSprites"); + int palettes = romEntry.getValue("PokemonPalettes"); + + rom[romEntry.getValue("IntroCryOffset")] = (byte) introPokemon; + rom[romEntry.getValue("IntroOtherOffset")] = (byte) introPokemon; + + int spriteBase = romEntry.getValue("IntroSpriteOffset"); + writePointer(spriteBase, frontSprites + introPokemon * 8); + writePointer(spriteBase + 4, palettes + introPokemon * 8); + } else if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + // intro sprites : any pokemon in the range 0-510 except bulbasaur + int introPokemon = pokedexToInternal[randomPokemon().number]; + while (introPokemon == 1 || introPokemon > 510) { + introPokemon = pokedexToInternal[randomPokemon().number]; + } + int frontSprites = romEntry.getValue("PokemonFrontSprites"); + int palettes = romEntry.getValue("PokemonNormalPalettes"); + int cryCommand = romEntry.getValue("IntroCryOffset"); + int otherCommand = romEntry.getValue("IntroOtherOffset"); + + if (introPokemon > 255) { + rom[cryCommand] = (byte) 0xFF; + rom[cryCommand + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR0; + + rom[cryCommand + 2] = (byte) (introPokemon - 0xFF); + rom[cryCommand + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR0; + + rom[otherCommand] = (byte) 0xFF; + rom[otherCommand + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR4; + + rom[otherCommand + 2] = (byte) (introPokemon - 0xFF); + rom[otherCommand + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR4; + } else { + rom[cryCommand] = (byte) introPokemon; + rom[cryCommand + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR0; + + writeWord(cryCommand + 2, Gen3Constants.gbaNopOpcode); + + rom[otherCommand] = (byte) introPokemon; + rom[otherCommand + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR4; + + writeWord(otherCommand + 2, Gen3Constants.gbaNopOpcode); + } + + writePointer(romEntry.getValue("IntroSpriteOffset"), frontSprites + introPokemon * 8); + writePointer(romEntry.getValue("IntroPaletteOffset"), palettes + introPokemon * 8); + } else { + // Emerald, intro sprite: any Pokemon. + int introPokemon = pokedexToInternal[randomPokemon().number]; + writeWord(romEntry.getValue("IntroSpriteOffset"), introPokemon); + writeWord(romEntry.getValue("IntroCryOffset"), introPokemon); + } + + } + + private Pokemon randomPokemonLimited(int maxValue, boolean blockNonMales) { + checkPokemonRestrictions(); + List<Pokemon> validPokemon = new ArrayList<>(); + for (Pokemon pk : this.mainPokemonList) { + if (pokedexToInternal[pk.number] <= maxValue && (!blockNonMales || pk.genderRatio <= 0xFD)) { + validPokemon.add(pk); + } + } + if (validPokemon.size() == 0) { + return null; + } else { + return validPokemon.get(random.nextInt(validPokemon.size())); + } + } + + private void determineMapBankSizes() { + int mbpsOffset = romEntry.getValue("MapHeaders"); + List<Integer> mapBankOffsets = new ArrayList<>(); + + int offset = mbpsOffset; + + // find map banks + while (true) { + boolean valid = true; + for (int mbOffset : mapBankOffsets) { + if (mbpsOffset < mbOffset && offset >= mbOffset) { + valid = false; + break; + } + } + if (!valid) { + break; + } + int newMBOffset = readPointer(offset); + if (newMBOffset < 0 || newMBOffset >= rom.length) { + break; + } + mapBankOffsets.add(newMBOffset); + offset += 4; + } + int bankCount = mapBankOffsets.size(); + int[] bankMapCounts = new int[bankCount]; + for (int bank = 0; bank < bankCount; bank++) { + int baseBankOffset = mapBankOffsets.get(bank); + int count = 0; + offset = baseBankOffset; + while (true) { + boolean valid = true; + for (int mbOffset : mapBankOffsets) { + if (baseBankOffset < mbOffset && offset >= mbOffset) { + valid = false; + break; + } + } + if (!valid) { + break; + } + if (baseBankOffset < mbpsOffset && offset >= mbpsOffset) { + break; + } + int newMapOffset = readPointer(offset); + if (newMapOffset < 0 || newMapOffset >= rom.length) { + break; + } + count++; + offset += 4; + } + bankMapCounts[bank] = count; + } + + romEntry.entries.put("MapBankCount", bankCount); + romEntry.arrayEntries.put("MapBankSizes", bankMapCounts); + } + + private void preprocessMaps() { + itemOffs = new ArrayList<>(); + int bankCount = romEntry.getValue("MapBankCount"); + int[] bankMapCounts = romEntry.arrayEntries.get("MapBankSizes"); + int itemBall = romEntry.getValue("ItemBallPic"); + mapNames = new String[bankCount][]; + int mbpsOffset = romEntry.getValue("MapHeaders"); + int mapLabels = romEntry.getValue("MapLabels"); + Map<Integer, String> mapLabelsM = new HashMap<>(); + for (int bank = 0; bank < bankCount; bank++) { + int bankOffset = readPointer(mbpsOffset + bank * 4); + mapNames[bank] = new String[bankMapCounts[bank]]; + for (int map = 0; map < bankMapCounts[bank]; map++) { + int mhOffset = readPointer(bankOffset + map * 4); + + // map name + int mapLabel = rom[mhOffset + 0x14] & 0xFF; + if (mapLabelsM.containsKey(mapLabel)) { + mapNames[bank][map] = mapLabelsM.get(mapLabel); + } else { + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + mapNames[bank][map] = readVariableLengthString(readPointer(mapLabels + + (mapLabel - Gen3Constants.frlgMapLabelsStart) * 4)); + } else { + mapNames[bank][map] = readVariableLengthString(readPointer(mapLabels + mapLabel * 8 + 4)); + } + mapLabelsM.put(mapLabel, mapNames[bank][map]); + } + + // events + int eventOffset = readPointer(mhOffset + 4); + if (eventOffset >= 0 && eventOffset < rom.length) { + + int pCount = rom[eventOffset] & 0xFF; + int spCount = rom[eventOffset + 3] & 0xFF; + + if (pCount > 0) { + int peopleOffset = readPointer(eventOffset + 4); + for (int p = 0; p < pCount; p++) { + int pSprite = rom[peopleOffset + p * 24 + 1]; + if (pSprite == itemBall && readPointer(peopleOffset + p * 24 + 16) >= 0) { + // Get script and look inside + int scriptOffset = readPointer(peopleOffset + p * 24 + 16); + if (rom[scriptOffset] == 0x1A && rom[scriptOffset + 1] == 0x00 + && (rom[scriptOffset + 2] & 0xFF) == 0x80 && rom[scriptOffset + 5] == 0x1A + && rom[scriptOffset + 6] == 0x01 && (rom[scriptOffset + 7] & 0xFF) == 0x80 + && rom[scriptOffset + 10] == 0x09 + && (rom[scriptOffset + 11] == 0x00 || rom[scriptOffset + 11] == 0x01)) { + // item ball script + itemOffs.add(scriptOffset + 3); + } + } + } + // TM Text? + for (TMOrMTTextEntry tte : romEntry.tmmtTexts) { + if (tte.mapBank == bank && tte.mapNumber == map) { + // process this one + int scriptOffset = readPointer(peopleOffset + (tte.personNum - 1) * 24 + 16); + if (scriptOffset >= 0) { + if (romEntry.romType == Gen3Constants.RomType_FRLG && tte.isMoveTutor + && (tte.number == 5 || (tte.number >= 8 && tte.number <= 11))) { + scriptOffset = readPointer(scriptOffset + 1); + } else if (romEntry.romType == Gen3Constants.RomType_FRLG && tte.isMoveTutor + && tte.number == 7) { + scriptOffset = readPointer(scriptOffset + 0x1F); + } + int lookAt = scriptOffset + tte.offsetInScript; + // make sure this actually looks like a text + // pointer + if (lookAt >= 0 && lookAt < rom.length - 2) { + if (rom[lookAt + 3] == 0x08 || rom[lookAt + 3] == 0x09) { + // okay, it passes the basic test + tte.actualOffset = lookAt; + } + } + } + } + } + } + + if (spCount > 0) { + int signpostsOffset = readPointer(eventOffset + 16); + for (int sp = 0; sp < spCount; sp++) { + int spType = rom[signpostsOffset + sp * 12 + 5]; + if (spType >= 5 && spType <= 7) { + // hidden item + int itemHere = readWord(signpostsOffset + sp * 12 + 8); + if (itemHere != 0) { + // itemid 0 is coins + itemOffs.add(signpostsOffset + sp * 12 + 8); + } + } + } + } + } + } + } + } + + @Override + public ItemList getAllowedItems() { + return allowedItems; + } + + @Override + public ItemList getNonBadItems() { + return nonBadItems; + } + + @Override + public List<Integer> getUniqueNoSellItems() { + return new ArrayList<>(); + } + + @Override + public List<Integer> getRegularShopItems() { + return Gen3Constants.regularShopItems; + } + + @Override + public List<Integer> getOPShopItems() { + return Gen3Constants.opShopItems; + } + + private void loadItemNames() { + int nameoffs = romEntry.getValue("ItemData"); + int structlen = romEntry.getValue("ItemEntrySize"); + int maxcount = romEntry.getValue("ItemCount"); + itemNames = new String[maxcount + 1]; + for (int i = 0; i <= maxcount; i++) { + itemNames[i] = readVariableLengthString(nameoffs + structlen * i); + } + } + + @Override + public String[] getItemNames() { + return itemNames; + } + + @Override + public List<Integer> getRequiredFieldTMs() { + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + return Gen3Constants.frlgRequiredFieldTMs; + } else if (romEntry.romType == Gen3Constants.RomType_Ruby || romEntry.romType == Gen3Constants.RomType_Sapp) { + return Gen3Constants.rsRequiredFieldTMs; + } else { + // emerald has a few TMs from pickup + return Gen3Constants.eRequiredFieldTMs; + } + } + + @Override + public List<Integer> getCurrentFieldTMs() { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + List<Integer> fieldTMs = new ArrayList<>(); + + for (int offset : itemOffs) { + int itemHere = readWord(offset); + if (Gen3Constants.allowedItems.isTM(itemHere)) { + int thisTM = itemHere - Gen3Constants.tmItemOffset + 1; + // hack for repeat TMs + if (!fieldTMs.contains(thisTM)) { + fieldTMs.add(thisTM); + } + } + } + return fieldTMs; + } + + @Override + public void setFieldTMs(List<Integer> fieldTMs) { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + Iterator<Integer> iterTMs = fieldTMs.iterator(); + int[] givenTMs = new int[512]; + + for (int offset : itemOffs) { + int itemHere = readWord(offset); + if (Gen3Constants.allowedItems.isTM(itemHere)) { + // Cache replaced TMs to duplicate repeats + if (givenTMs[itemHere] != 0) { + rom[offset] = (byte) givenTMs[itemHere]; + } else { + // Replace this with a TM from the list + int tm = iterTMs.next(); + tm += Gen3Constants.tmItemOffset - 1; + givenTMs[itemHere] = tm; + writeWord(offset, tm); + } + } + } + } + + @Override + public List<Integer> getRegularFieldItems() { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + List<Integer> fieldItems = new ArrayList<>(); + + for (int offset : itemOffs) { + int itemHere = readWord(offset); + if (Gen3Constants.allowedItems.isAllowed(itemHere) && !(Gen3Constants.allowedItems.isTM(itemHere))) { + fieldItems.add(itemHere); + } + } + return fieldItems; + } + + @Override + public void setRegularFieldItems(List<Integer> items) { + if (!mapLoadingDone) { + preprocessMaps(); + mapLoadingDone = true; + } + Iterator<Integer> iterItems = items.iterator(); + + for (int offset : itemOffs) { + int itemHere = readWord(offset); + if (Gen3Constants.allowedItems.isAllowed(itemHere) && !(Gen3Constants.allowedItems.isTM(itemHere))) { + // Replace it + writeWord(offset, iterItems.next()); + } + } + + } + + @Override + public List<IngameTrade> getIngameTrades() { + List<IngameTrade> trades = new ArrayList<>(); + + // info + int tableOffset = romEntry.getValue("TradeTableOffset"); + int tableSize = romEntry.getValue("TradeTableSize"); + int[] unused = romEntry.arrayEntries.get("TradesUnused"); + int unusedOffset = 0; + int entryLength = 60; + + for (int entry = 0; entry < tableSize; entry++) { + if (unusedOffset < unused.length && unused[unusedOffset] == entry) { + unusedOffset++; + continue; + } + IngameTrade trade = new IngameTrade(); + int entryOffset = tableOffset + entry * entryLength; + trade.nickname = readVariableLengthString(entryOffset); + trade.givenPokemon = pokesInternal[readWord(entryOffset + 12)]; + trade.ivs = new int[6]; + for (int i = 0; i < 6; i++) { + trade.ivs[i] = rom[entryOffset + 14 + i] & 0xFF; + } + trade.otId = readWord(entryOffset + 24); + trade.item = readWord(entryOffset + 40); + trade.otName = readVariableLengthString(entryOffset + 43); + trade.requestedPokemon = pokesInternal[readWord(entryOffset + 56)]; + trades.add(trade); + } + + return trades; + + } + + @Override + public void setIngameTrades(List<IngameTrade> trades) { + // info + int tableOffset = romEntry.getValue("TradeTableOffset"); + int tableSize = romEntry.getValue("TradeTableSize"); + int[] unused = romEntry.arrayEntries.get("TradesUnused"); + int unusedOffset = 0; + int entryLength = 60; + int tradeOffset = 0; + + for (int entry = 0; entry < tableSize; entry++) { + if (unusedOffset < unused.length && unused[unusedOffset] == entry) { + unusedOffset++; + continue; + } + IngameTrade trade = trades.get(tradeOffset++); + int entryOffset = tableOffset + entry * entryLength; + writeFixedLengthString(trade.nickname, entryOffset, 12); + writeWord(entryOffset + 12, pokedexToInternal[trade.givenPokemon.number]); + for (int i = 0; i < 6; i++) { + rom[entryOffset + 14 + i] = (byte) trade.ivs[i]; + } + writeWord(entryOffset + 24, trade.otId); + writeWord(entryOffset + 40, trade.item); + writeFixedLengthString(trade.otName, entryOffset + 43, 11); + writeWord(entryOffset + 56, pokedexToInternal[trade.requestedPokemon.number]); + } + } + + @Override + public boolean hasDVs() { + return false; + } + + @Override + public int generationOfPokemon() { + return 3; + } + + @Override + public void removeEvosForPokemonPool() { + List<Pokemon> pokemonIncluded = this.mainPokemonList; + Set<Evolution> keepEvos = new HashSet<>(); + for (Pokemon pk : pokes) { + if (pk != null) { + keepEvos.clear(); + for (Evolution evol : pk.evolutionsFrom) { + if (pokemonIncluded.contains(evol.from) && pokemonIncluded.contains(evol.to)) { + keepEvos.add(evol); + } else { + evol.to.evolutionsTo.remove(evol); + } + } + pk.evolutionsFrom.retainAll(keepEvos); + } + } + } + + @Override + public boolean supportsFourStartingMoves() { + return true; + } + + @Override + public List<Integer> getFieldMoves() { + // cut, fly, surf, strength, flash, + // dig, teleport, waterfall, + // rock smash, sweet scent + // not softboiled or milk drink + // dive and secret power in RSE only + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + return Gen3Constants.frlgFieldMoves; + } else { + return Gen3Constants.rseFieldMoves; + } + } + + @Override + public List<Integer> getEarlyRequiredHMMoves() { + // RSE: rock smash + // FRLG: cut + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + return Gen3Constants.frlgEarlyRequiredHMMoves; + } else { + return Gen3Constants.rseEarlyRequiredHMMoves; + } + } + + @Override + public int miscTweaksAvailable() { + int available = MiscTweak.LOWER_CASE_POKEMON_NAMES.getValue(); + available |= MiscTweak.NATIONAL_DEX_AT_START.getValue(); + available |= MiscTweak.UPDATE_TYPE_EFFECTIVENESS.getValue(); + if (romEntry.getValue("RunIndoorsTweakOffset") > 0) { + available |= MiscTweak.RUNNING_SHOES_INDOORS.getValue(); + } + if (romEntry.getValue("TextSpeedValuesOffset") > 0 || romEntry.codeTweaks.get("InstantTextTweak") != null) { + available |= MiscTweak.FASTEST_TEXT.getValue(); + } + if (romEntry.getValue("CatchingTutorialOpponentMonOffset") > 0 + || romEntry.getValue("CatchingTutorialPlayerMonOffset") > 0) { + available |= MiscTweak.RANDOMIZE_CATCHING_TUTORIAL.getValue(); + } + if (romEntry.getValue("PCPotionOffset") != 0) { + available |= MiscTweak.RANDOMIZE_PC_POTION.getValue(); + } + available |= MiscTweak.BAN_LUCKY_EGG.getValue(); + available |= MiscTweak.RUN_WITHOUT_RUNNING_SHOES.getValue(); + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + available |= MiscTweak.BALANCE_STATIC_LEVELS.getValue(); + } + return available; + } + + @Override + public void applyMiscTweak(MiscTweak tweak) { + if (tweak == MiscTweak.RUNNING_SHOES_INDOORS) { + applyRunningShoesIndoorsPatch(); + } else if (tweak == MiscTweak.FASTEST_TEXT) { + applyFastestTextPatch(); + } else if (tweak == MiscTweak.LOWER_CASE_POKEMON_NAMES) { + applyCamelCaseNames(); + } else if (tweak == MiscTweak.NATIONAL_DEX_AT_START) { + patchForNationalDex(); + } else if (tweak == MiscTweak.RANDOMIZE_CATCHING_TUTORIAL) { + randomizeCatchingTutorial(); + } else if (tweak == MiscTweak.BAN_LUCKY_EGG) { + allowedItems.banSingles(Gen3Items.luckyEgg); + nonBadItems.banSingles(Gen3Items.luckyEgg); + } else if (tweak == MiscTweak.RANDOMIZE_PC_POTION) { + randomizePCPotion(); + } else if (tweak == MiscTweak.RUN_WITHOUT_RUNNING_SHOES) { + applyRunWithoutRunningShoesPatch(); + } else if (tweak == MiscTweak.BALANCE_STATIC_LEVELS) { + int[] fossilLevelOffsets = romEntry.arrayEntries.get("FossilLevelOffsets"); + for (int fossilLevelOffset : fossilLevelOffsets) { + writeWord(rom, fossilLevelOffset, 30); + } + } else if (tweak == MiscTweak.UPDATE_TYPE_EFFECTIVENESS) { + updateTypeEffectiveness(); + } + } + + @Override + public boolean isEffectivenessUpdated() { + return effectivenessUpdated; + } + + private void randomizeCatchingTutorial() { + if (romEntry.getValue("CatchingTutorialOpponentMonOffset") > 0) { + int oppOffset = romEntry.getValue("CatchingTutorialOpponentMonOffset"); + if (romEntry.romType == Gen3Constants.RomType_FRLG) { + Pokemon opponent = randomPokemonLimited(255, true); + if (opponent != null) { + + int oppValue = pokedexToInternal[opponent.number]; + rom[oppOffset] = (byte) oppValue; + rom[oppOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + } + } else { + Pokemon opponent = randomPokemonLimited(510, true); + if (opponent != null) { + int oppValue = pokedexToInternal[opponent.number]; + if (oppValue > 255) { + rom[oppOffset] = (byte) 0xFF; + rom[oppOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + + rom[oppOffset + 2] = (byte) (oppValue - 0xFF); + rom[oppOffset + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR1; + } else { + rom[oppOffset] = (byte) oppValue; + rom[oppOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + + writeWord(oppOffset + 2, Gen3Constants.gbaNopOpcode); + } + } + } + } + + if (romEntry.getValue("CatchingTutorialPlayerMonOffset") > 0) { + int playerOffset = romEntry.getValue("CatchingTutorialPlayerMonOffset"); + Pokemon playerMon = randomPokemonLimited(510, false); + if (playerMon != null) { + int plyValue = pokedexToInternal[playerMon.number]; + if (plyValue > 255) { + rom[playerOffset] = (byte) 0xFF; + rom[playerOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + + rom[playerOffset + 2] = (byte) (plyValue - 0xFF); + rom[playerOffset + 3] = Gen3Constants.gbaAddRxOpcode | Gen3Constants.gbaR1; + } else { + rom[playerOffset] = (byte) plyValue; + rom[playerOffset + 1] = Gen3Constants.gbaSetRxOpcode | Gen3Constants.gbaR1; + + writeWord(playerOffset + 2, Gen3Constants.gbaNopOpcode); + } + } + } + + } + + private void applyRunningShoesIndoorsPatch() { + if (romEntry.getValue("RunIndoorsTweakOffset") != 0) { + rom[romEntry.getValue("RunIndoorsTweakOffset")] = 0x00; + } + } + + private void applyFastestTextPatch() { + if(romEntry.codeTweaks.get("InstantTextTweak") != null) { + try { + FileFunctions.applyPatch(rom, romEntry.codeTweaks.get("InstantTextTweak")); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } else if (romEntry.getValue("TextSpeedValuesOffset") > 0) { + int tsvOffset = romEntry.getValue("TextSpeedValuesOffset"); + rom[tsvOffset] = 4; // slow = medium + rom[tsvOffset + 1] = 1; // medium = fast + rom[tsvOffset + 2] = 0; // fast = instant + } + } + + private void randomizePCPotion() { + if (romEntry.getValue("PCPotionOffset") != 0) { + writeWord(romEntry.getValue("PCPotionOffset"), this.getNonBadItems().randomNonTM(this.random)); + } + } + + private void applyRunWithoutRunningShoesPatch() { + String prefix = Gen3Constants.getRunningShoesCheckPrefix(romEntry.romType); + int offset = find(prefix); + if (offset != 0) { + // The prefix starts 0x12 bytes from what we want to patch because what comes + // between is region and revision dependent. To start running, the game checks: + // 1. That you're not underwater (RSE only) + // 2. That you're holding the B button + // 3. That the FLAG_SYS_B_DASH flag is set (aka, you've acquired Running Shoes) + // 4. That you're allowed to run in this location + // For #3, if the flag is unset, it jumps to a different part of the + // code to make you walk instead. This simply nops out this jump so the + // game stops caring about the FLAG_SYS_B_DASH flag entirely. + writeWord(offset + 0x12, 0); + } + } + + private void updateTypeEffectiveness() { + List<TypeRelationship> typeEffectivenessTable = readTypeEffectivenessTable(); + log("--Updating Type Effectiveness--"); + for (TypeRelationship relationship : typeEffectivenessTable) { + // Change Ghost 0.5x against Steel to Ghost 1x to Steel + if (relationship.attacker == Type.GHOST && relationship.defender == Type.STEEL) { + relationship.effectiveness = Effectiveness.NEUTRAL; + log("Replaced: Ghost not very effective vs Steel => Ghost neutral vs Steel"); + } + + // Change Dark 0.5x against Steel to Dark 1x to Steel + else if (relationship.attacker == Type.DARK && relationship.defender == Type.STEEL) { + relationship.effectiveness = Effectiveness.NEUTRAL; + log("Replaced: Dark not very effective vs Steel => Dark neutral vs Steel"); + } + } + logBlankLine(); + writeTypeEffectivenessTable(typeEffectivenessTable); + effectivenessUpdated = true; + } + + private List<TypeRelationship> readTypeEffectivenessTable() { + List<TypeRelationship> typeEffectivenessTable = new ArrayList<>(); + int currentOffset = romEntry.getValue("TypeEffectivenessOffset"); + int attackingType = rom[currentOffset]; + // 0xFE marks the end of the table *not* affected by Foresight, while 0xFF marks + // the actual end of the table. Since we don't care about Ghost immunities at all, + // just stop once we reach the Foresight section. + while (attackingType != (byte) 0xFE) { + int defendingType = rom[currentOffset + 1]; + int effectivenessInternal = rom[currentOffset + 2]; + Type attacking = Gen3Constants.typeTable[attackingType]; + Type defending = Gen3Constants.typeTable[defendingType]; + Effectiveness effectiveness = null; + switch (effectivenessInternal) { + case 20: + effectiveness = Effectiveness.DOUBLE; + break; + case 10: + effectiveness = Effectiveness.NEUTRAL; + break; + case 5: + effectiveness = Effectiveness.HALF; + break; + case 0: + effectiveness = Effectiveness.ZERO; + break; + } + if (effectiveness != null) { + TypeRelationship relationship = new TypeRelationship(attacking, defending, effectiveness); + typeEffectivenessTable.add(relationship); + } + currentOffset += 3; + attackingType = rom[currentOffset]; + } + return typeEffectivenessTable; + } + + private void writeTypeEffectivenessTable(List<TypeRelationship> typeEffectivenessTable) { + int currentOffset = romEntry.getValue("TypeEffectivenessOffset"); + for (TypeRelationship relationship : typeEffectivenessTable) { + rom[currentOffset] = Gen3Constants.typeToByte(relationship.attacker); + rom[currentOffset + 1] = Gen3Constants.typeToByte(relationship.defender); + byte effectivenessInternal = 0; + switch (relationship.effectiveness) { + case DOUBLE: + effectivenessInternal = 20; + break; + case NEUTRAL: + effectivenessInternal = 10; + break; + case HALF: + effectivenessInternal = 5; + break; + case ZERO: + effectivenessInternal = 0; + break; + } + rom[currentOffset + 2] = effectivenessInternal; + currentOffset += 3; + } + } + + @Override + public void enableGuaranteedPokemonCatching() { + int offset = find(rom, Gen3Constants.perfectOddsBranchLocator); + if (offset > 0) { + // In Cmd_handleballthrow, the middle of the function checks if the odds of catching a Pokemon + // is greater than 254; if it is, then the Pokemon is automatically caught. In ASM, this is + // represented by: + // cmp r6, #0xFE + // bls oddsLessThanOrEqualTo254 + // The below code just nops these two instructions so that we *always* act like our odds are 255, + // and Pokemon are automatically caught no matter what. + rom[offset] = 0x00; + rom[offset + 1] = 0x00; + rom[offset + 2] = 0x00; + rom[offset + 3] = 0x00; + } + } + + @Override + public boolean isRomValid() { + return romEntry.expectedCRC32 == actualCRC32; + } + + @Override + public BufferedImage getMascotImage() { + Pokemon mascotPk = randomPokemon(); + int mascotPokemon = pokedexToInternal[mascotPk.number]; + int frontSprites = romEntry.getValue("FrontSprites"); + int palettes = romEntry.getValue("PokemonPalettes"); + int fsOffset = readPointer(frontSprites + mascotPokemon * 8); + int palOffset = readPointer(palettes + mascotPokemon * 8); + + byte[] trueFrontSprite = DSDecmp.Decompress(rom, fsOffset); + byte[] truePalette = DSDecmp.Decompress(rom, palOffset); + + // Convert palette into RGB + int[] convPalette = new int[16]; + // Leave palette[0] as 00000000 for transparency + for (int i = 0; i < 15; i++) { + int palValue = readWord(truePalette, i * 2 + 2); + convPalette[i + 1] = GFXFunctions.conv16BitColorToARGB(palValue); + } + + // Make image, 4bpp + return GFXFunctions.drawTiledImage(trueFrontSprite, convPalette, 64, 64, 4); + } + + @Override + public List<Integer> getAllHeldItems() { + return Gen3Constants.allHeldItems; + } + + @Override + public boolean hasRivalFinalBattle() { + return romEntry.romType == Gen3Constants.RomType_FRLG; + } + + @Override + public List<Integer> getAllConsumableHeldItems() { + return Gen3Constants.consumableHeldItems; + } + + @Override + public List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, int[] pokeMoves) { + List<Integer> items = new ArrayList<>(); + items.addAll(Gen3Constants.generalPurposeConsumableItems); + if (!consumableOnly) { + items.addAll(Gen3Constants.generalPurposeItems); + } + for (int moveIdx : pokeMoves) { + Move move = moves.get(moveIdx); + if (move == null) { + continue; + } + if (GBConstants.physicalTypes.contains(move.type) && move.power > 0) { + items.add(Gen3Items.liechiBerry); + if (!consumableOnly) { + items.addAll(Gen3Constants.typeBoostingItems.get(move.type)); + items.add(Gen3Items.choiceBand); + } + } + if (!GBConstants.physicalTypes.contains(move.type) && move.power > 0) { + items.add(Gen3Items.petayaBerry); + if (!consumableOnly) { + items.addAll(Gen3Constants.typeBoostingItems.get(move.type)); + } + } + } + if (!consumableOnly) { + List<Integer> speciesItems = Gen3Constants.speciesBoostingItems.get(tp.pokemon.number); + if (speciesItems != null) { + for (int i = 0; i < 6; i++) { // Increase the likelihood of using species specific items. + items.addAll(speciesItems); + } + } + } + return items; + } +} diff --git a/src/com/pkrandom/romhandlers/Gen4RomHandler.java b/src/com/pkrandom/romhandlers/Gen4RomHandler.java new file mode 100755 index 0000000..417799e --- /dev/null +++ b/src/com/pkrandom/romhandlers/Gen4RomHandler.java @@ -0,0 +1,5841 @@ +package com.pkrandom.romhandlers;
+
+/*----------------------------------------------------------------------------*/
+/*-- Gen4RomHandler.java - randomizer handler for D/P/Pt/HG/SS. --*/
+/*-- --*/
+/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/
+/*-- Pokemon and any associated names and the like are --*/
+/*-- trademark and (C) Nintendo 1996-2020. --*/
+/*-- --*/
+/*-- The custom code written here is licensed under the terms of the GPL: --*/
+/*-- --*/
+/*-- This program is free software: you can redistribute it and/or modify --*/
+/*-- it under the terms of the GNU General Public License as published by --*/
+/*-- the Free Software Foundation, either version 3 of the License, or --*/
+/*-- (at your option) any later version. --*/
+/*-- --*/
+/*-- This program is distributed in the hope that it will be useful, --*/
+/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/
+/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/
+/*-- GNU General Public License for more details. --*/
+/*-- --*/
+/*-- You should have received a copy of the GNU General Public License --*/
+/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/
+/*----------------------------------------------------------------------------*/
+
+import java.awt.image.BufferedImage;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.pkrandom.*;
+import com.pkrandom.constants.*;
+import com.pkrandom.exceptions.RandomizationException;
+import com.pkrandom.pokemon.*;
+import thenewpoketext.PokeTextData;
+import thenewpoketext.TextToPoke;
+
+import com.pkrandom.exceptions.RandomizerIOException;
+import com.pkrandom.newnds.NARCArchive;
+
+public class Gen4RomHandler extends AbstractDSRomHandler {
+
+ public static class Factory extends RomHandler.Factory {
+
+ @Override
+ public Gen4RomHandler create(Random random, PrintStream logStream) {
+ return new Gen4RomHandler(random, logStream);
+ }
+
+ public boolean isLoadable(String filename) {
+ return detectNDSRomInner(getROMCodeFromFile(filename), getVersionFromFile(filename));
+ }
+ }
+
+ public Gen4RomHandler(Random random) {
+ super(random, null);
+ }
+
+ public Gen4RomHandler(Random random, PrintStream logStream) {
+ super(random, logStream);
+ }
+
+ private static class RomFileEntry {
+ public String path;
+ public long expectedCRC32;
+ }
+
+ private static class RomEntry {
+ private String name;
+ private String romCode;
+ private byte version;
+ private int romType;
+ private long arm9ExpectedCRC32;
+ private boolean staticPokemonSupport = false, copyStaticPokemon = false,copyRoamingPokemon = false,
+ ignoreGameCornerStatics = false, copyText = false;
+ private Map<String, String> strings = new HashMap<>();
+ private Map<String, String> tweakFiles = new HashMap<>();
+ private Map<String, Integer> numbers = new HashMap<>();
+ private Map<String, int[]> arrayEntries = new HashMap<>();
+ private Map<String, RomFileEntry> files = new HashMap<>();
+ private Map<Integer, Long> overlayExpectedCRC32s = new HashMap<>();
+ private List<StaticPokemon> staticPokemon = new ArrayList<>();
+ private List<RoamingPokemon> roamingPokemon = new ArrayList<>();
+ private List<ScriptEntry> marillCryScriptEntries = new ArrayList<>();
+ private Map<Integer, List<TextEntry>> tmTexts = new HashMap<>();
+ private Map<Integer, TextEntry> tmTextsGameCorner = new HashMap<>();
+ private Map<Integer, Integer> tmScriptOffsetsFrontier = new HashMap<>();
+ private Map<Integer, Integer> tmTextsFrontier = 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<RomEntry> roms;
+
+ static {
+ loadROMInfo();
+
+ }
+
+ private static void loadROMInfo() {
+ roms = new ArrayList<>();
+ RomEntry current = null;
+ try {
+ Scanner sc = new Scanner(FileFunctions.openConfig("gen4_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("Version")) {
+ current.version = Byte.parseByte(r[1]);
+ } else if (r[0].equals("Type")) {
+ if (r[1].equalsIgnoreCase("DP")) {
+ current.romType = Gen4Constants.Type_DP;
+ } else if (r[1].equalsIgnoreCase("Plat")) {
+ current.romType = Gen4Constants.Type_Plat;
+ } else if (r[1].equalsIgnoreCase("HGSS")) {
+ current.romType = Gen4Constants.Type_HGSS;
+ } else {
+ System.err.println("unrecognised rom type: " + r[1]);
+ }
+ } else if (r[0].equals("CopyFrom")) {
+ for (RomEntry otherEntry : roms) {
+ if (r[1].equalsIgnoreCase(otherEntry.name)) {
+ // copy from here
+ current.arrayEntries.putAll(otherEntry.arrayEntries);
+ current.numbers.putAll(otherEntry.numbers);
+ current.strings.putAll(otherEntry.strings);
+ current.files.putAll(otherEntry.files);
+ if (current.copyStaticPokemon) {
+ current.staticPokemon.addAll(otherEntry.staticPokemon);
+ if (current.ignoreGameCornerStatics) {
+ current.staticPokemon.removeIf(staticPokemon -> staticPokemon instanceof StaticPokemonGameCorner);
+ }
+ current.staticPokemonSupport = true;
+ } else {
+ current.staticPokemonSupport = false;
+ }
+ if (current.copyRoamingPokemon) {
+ current.roamingPokemon.addAll(otherEntry.roamingPokemon);
+ }
+ if (current.copyText) {
+ current.tmTexts.putAll(otherEntry.tmTexts);
+ current.tmTextsGameCorner.putAll(otherEntry.tmTextsGameCorner);
+ current.tmScriptOffsetsFrontier.putAll(otherEntry.tmScriptOffsetsFrontier);
+ current.tmTextsFrontier.putAll(otherEntry.tmTextsFrontier);
+ }
+ current.marillCryScriptEntries.addAll(otherEntry.marillCryScriptEntries);
+ }
+ }
+ } 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(",");
+ RomFileEntry entry = new RomFileEntry();
+ entry.path = values[0].trim();
+ entry.expectedCRC32 = parseRILong("0x" + values[1].trim());
+ current.files.put(key, entry);
+ } else if (r[0].equals("Arm9CRC32")) {
+ current.arm9ExpectedCRC32 = parseRILong("0x" + r[1]);
+ } else if (r[0].startsWith("OverlayCRC32<")) {
+ String keyString = r[0].split("<")[1].split(">")[0];
+ int key = parseRIInt(keyString);
+ long value = parseRILong("0x" + r[1]);
+ current.overlayExpectedCRC32s.put(key, value);
+ } else if (r[0].equals("StaticPokemon{}")) {
+ current.staticPokemon.add(parseStaticPokemon(r[1]));
+ } else if (r[0].equals("RoamingPokemon{}")) {
+ current.roamingPokemon.add(parseRoamingPokemon(r[1]));
+ } else if (r[0].equals("StaticPokemonGameCorner{}")) {
+ current.staticPokemon.add(parseStaticPokemonGameCorner(r[1]));
+ } else if (r[0].equals("TMText{}")) {
+ parseTMText(r[1], current.tmTexts);
+ } else if (r[0].equals("TMTextGameCorner{}")) {
+ parseTMTextGameCorner(r[1], current.tmTextsGameCorner);
+ } else if (r[0].equals("FrontierScriptTMOffsets{}")) {
+ String[] offsets = r[1].substring(1, r[1].length() - 1).split(",");
+ for (String off : offsets) {
+ String[] parts = off.split("=");
+ int tmNum = parseRIInt(parts[0]);
+ int offset = parseRIInt(parts[1]);
+ current.tmScriptOffsetsFrontier.put(tmNum, offset);
+ }
+ } else if (r[0].equals("FrontierTMText{}")) {
+ String[] offsets = r[1].substring(1, r[1].length() - 1).split(",");
+ for (String off : offsets) {
+ String[] parts = off.split("=");
+ int tmNum = parseRIInt(parts[0]);
+ int stringNumber = parseRIInt(parts[1]);
+ current.tmTextsFrontier.put(tmNum, stringNumber);
+ }
+ } else if (r[0].equals("StaticPokemonSupport")) {
+ int spsupport = parseRIInt(r[1]);
+ current.staticPokemonSupport = (spsupport > 0);
+ } else if (r[0].equals("CopyStaticPokemon")) {
+ int csp = parseRIInt(r[1]);
+ current.copyStaticPokemon = (csp > 0);
+ } else if (r[0].equals("CopyRoamingPokemon")) {
+ int crp = parseRIInt(r[1]);
+ current.copyRoamingPokemon = (crp > 0);
+ } else if (r[0].equals("CopyText")) {
+ int ct = parseRIInt(r[1]);
+ current.copyText = (ct > 0);
+ } else if (r[0].equals("IgnoreGameCornerStatics")) {
+ int ct = parseRIInt(r[1]);
+ current.ignoreGameCornerStatics = (ct > 0);
+ } else if (r[0].endsWith("Tweak")) {
+ current.tweakFiles.put(r[0], r[1]);
+ } else if (r[0].endsWith("MarillCryScripts")) {
+ current.marillCryScriptEntries.clear();
+ String[] offsets = r[1].substring(1, r[1].length() - 1).split(",");
+ for (String off : offsets) {
+ String[] parts = off.split(":");
+ int file = parseRIInt(parts[0]);
+ int offset = parseRIInt(parts[1]);
+ ScriptEntry entry = new ScriptEntry(file, offset);
+ current.marillCryScriptEntries.add(entry);
+ }
+ } 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")
+ || r[0].endsWith("Size") || r[0].endsWith("Index")) {
+ 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;
+ }
+ }
+
+ private static StaticPokemon parseStaticPokemon(String staticPokemonString) {
+ StaticPokemon sp = new StaticPokemon();
+ String pattern = "[A-z]+=\\[([0-9]+:0x[0-9a-fA-F]+,?\\s?)+]";
+ Pattern r = Pattern.compile(pattern);
+ Matcher m = r.matcher(staticPokemonString);
+ while (m.find()) {
+ String[] segments = m.group().split("=");
+ String[] offsets = segments[1].substring(1, segments[1].length() - 1).split(",");
+ ScriptEntry[] entries = new ScriptEntry[offsets.length];
+ for (int i = 0; i < entries.length; i++) {
+ String[] parts = offsets[i].split(":");
+ entries[i] = new ScriptEntry(parseRIInt(parts[0]), parseRIInt(parts[1]));
+ }
+ switch (segments[0]) {
+ case "Species":
+ sp.speciesEntries = entries;
+ break;
+ case "Level":
+ sp.levelEntries = entries;
+ break;
+ case "Forme":
+ sp.formeEntries = entries;
+ break;
+ }
+ }
+ return sp;
+ }
+
+ private static StaticPokemonGameCorner parseStaticPokemonGameCorner(String staticPokemonString) {
+ StaticPokemonGameCorner sp = new StaticPokemonGameCorner();
+ String pattern = "[A-z]+=\\[([0-9]+:0x[0-9a-fA-F]+,?\\s?)+]";
+ Pattern r = Pattern.compile(pattern);
+ Matcher m = r.matcher(staticPokemonString);
+ while (m.find()) {
+ String[] segments = m.group().split("=");
+ String[] offsets = segments[1].substring(1, segments[1].length() - 1).split(",");
+ switch (segments[0]) {
+ case "Species":
+ ScriptEntry[] speciesEntries = new ScriptEntry[offsets.length];
+ for (int i = 0; i < speciesEntries.length; i++) {
+ String[] parts = offsets[i].split(":");
+ speciesEntries[i] = new ScriptEntry(parseRIInt(parts[0]), parseRIInt(parts[1]));
+ }
+ sp.speciesEntries = speciesEntries;
+ break;
+ case "Level":
+ ScriptEntry[] levelEntries = new ScriptEntry[offsets.length];
+ for (int i = 0; i < levelEntries.length; i++) {
+ String[] parts = offsets[i].split(":");
+ levelEntries[i] = new ScriptEntry(parseRIInt(parts[0]), parseRIInt(parts[1]));
+ }
+ sp.levelEntries = levelEntries;
+ break;
+ case "Text":
+ TextEntry[] textEntries = new TextEntry[offsets.length];
+ for (int i = 0; i < textEntries.length; i++) {
+ String[] parts = offsets[i].split(":");
+ textEntries[i] = new TextEntry(parseRIInt(parts[0]), parseRIInt(parts[1]));
+ }
+ sp.textEntries = textEntries;
+ break;
+ }
+ }
+ return sp;
+ }
+
+ private static RoamingPokemon parseRoamingPokemon(String roamingPokemonString) {
+ RoamingPokemon rp = new RoamingPokemon();
+ String pattern = "[A-z]+=\\[(0x[0-9a-fA-F]+,?\\s?)+]|[A-z]+=\\[([0-9]+:0x[0-9a-fA-F]+,?\\s?)+]";
+ Pattern r = Pattern.compile(pattern);
+ Matcher m = r.matcher(roamingPokemonString);
+ while (m.find()) {
+ String[] segments = m.group().split("=");
+ String[] offsets = segments[1].substring(1, segments[1].length() - 1).split(",");
+ switch (segments[0]) {
+ case "Species":
+ int[] speciesCodeOffsets = new int[offsets.length];
+ for (int i = 0; i < speciesCodeOffsets.length; i++) {
+ speciesCodeOffsets[i] = parseRIInt(offsets[i]);
+ }
+ rp.speciesCodeOffsets = speciesCodeOffsets;
+ break;
+ case "Level":
+ int[] levelCodeOffsets = new int[offsets.length];
+ for (int i = 0; i < levelCodeOffsets.length; i++) {
+ levelCodeOffsets[i] = parseRIInt(offsets[i]);
+ }
+ rp.levelCodeOffsets = levelCodeOffsets;
+ break;
+ case "Script":
+ ScriptEntry[] scriptEntries = new ScriptEntry[offsets.length];
+ for (int i = 0; i < scriptEntries.length; i++) {
+ String[] parts = offsets[i].split(":");
+ scriptEntries[i] = new ScriptEntry(parseRIInt(parts[0]), parseRIInt(parts[1]));
+ }
+ rp.speciesScriptOffsets = scriptEntries;
+ break;
+ case "Gender":
+ ScriptEntry[] genderEntries = new ScriptEntry[offsets.length];
+ for (int i = 0; i < genderEntries.length; i++) {
+ String[] parts = offsets[i].split(":");
+ genderEntries[i] = new ScriptEntry(parseRIInt(parts[0]), parseRIInt(parts[1]));
+ }
+ rp.genderOffsets = genderEntries;
+ break;
+ }
+ }
+ return rp;
+ }
+
+ private static void parseTMText(String tmTextString, Map<Integer, List<TextEntry>> tmTexts) {
+ String pattern = "[0-9]+=\\[([0-9]+:[0-9]+,?\\s?)+]";
+ Pattern r = Pattern.compile(pattern);
+ Matcher m = r.matcher(tmTextString);
+ while (m.find()) {
+ String[] segments = m.group().split("=");
+ int tmNum = parseRIInt(segments[0]);
+ String[] entries = segments[1].substring(1, segments[1].length() - 1).split(",");
+ List<TextEntry> textEntries = new ArrayList<>();
+ for (String entry : entries) {
+ String[] textSegments = entry.split(":");
+ TextEntry textEntry = new TextEntry(parseRIInt(textSegments[0]), parseRIInt(textSegments[1]));
+ textEntries.add(textEntry);
+ }
+ tmTexts.put(tmNum, textEntries);
+ }
+ }
+
+ private static void parseTMTextGameCorner(String tmTextGameCornerString, Map<Integer, TextEntry> tmTextGameCorner) {
+ String[] tmTextGameCornerEntries = tmTextGameCornerString.substring(1, tmTextGameCornerString.length() - 1).split(",");
+ for (String tmTextGameCornerEntry : tmTextGameCornerEntries) {
+ String[] segments = tmTextGameCornerEntry.trim().split("=");
+ int tmNum = parseRIInt(segments[0]);
+ String textEntry = segments[1].substring(1, segments[1].length() - 1);
+ String[] textSegments = textEntry.split(":");
+ TextEntry entry = new TextEntry(parseRIInt(textSegments[0]), parseRIInt(textSegments[1]));
+ tmTextGameCorner.put(tmNum, entry);
+ }
+ }
+
+ // This rom
+ private Pokemon[] pokes;
+ private List<Pokemon> pokemonListInclFormes;
+ private List<Pokemon> pokemonList;
+ private Move[] moves;
+ private NARCArchive pokeNarc, moveNarc;
+ private NARCArchive msgNarc;
+ private NARCArchive scriptNarc;
+ private NARCArchive eventNarc;
+ private byte[] arm9;
+ private List<String> abilityNames;
+ private List<String> itemNames;
+ private boolean loadedWildMapNames;
+ private Map<Integer, String> wildMapNames, headbuttMapNames;
+ private ItemList allowedItems, nonBadItems;
+ private boolean roamerRandomizationEnabled;
+ private boolean effectivenessUpdated;
+ private int pickupItemsTableOffset, rarePickupItemsTableOffset;
+ private long actualArm9CRC32;
+ private Map<Integer, Long> actualOverlayCRC32s;
+ private Map<String, Long> actualFileCRC32s;
+
+ private RomEntry romEntry;
+
+ @Override
+ protected boolean detectNDSRom(String ndsCode, byte version) {
+ return detectNDSRomInner(ndsCode, version);
+ }
+
+ private static boolean detectNDSRomInner(String ndsCode, byte version) {
+ return entryFor(ndsCode, version) != null;
+ }
+
+ private static RomEntry entryFor(String ndsCode, byte version) {
+ for (RomEntry re : roms) {
+ if (ndsCode.equals(re.romCode) && version == re.version) {
+ return re;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void loadedROM(String romCode, byte version) {
+ this.romEntry = entryFor(romCode, version);
+ try {
+ arm9 = readARM9();
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ try {
+ msgNarc = readNARC(romEntry.getFile("Text"));
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ try {
+ scriptNarc = readNARC(romEntry.getFile("Scripts"));
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ try {
+ eventNarc = readNARC(romEntry.getFile("Events"));
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ loadPokemonStats();
+ pokemonListInclFormes = Arrays.asList(pokes);
+ pokemonList = Arrays.asList(Arrays.copyOfRange(pokes,0,Gen4Constants.pokemonCount + 1));
+ loadMoves();
+ abilityNames = getStrings(romEntry.getInt("AbilityNamesTextOffset"));
+ itemNames = getStrings(romEntry.getInt("ItemNamesTextOffset"));
+ loadedWildMapNames = false;
+
+ allowedItems = Gen4Constants.allowedItems.copy();
+ nonBadItems = Gen4Constants.nonBadItems.copy();
+
+ roamerRandomizationEnabled =
+ (romEntry.romType == Gen4Constants.Type_DP && romEntry.roamingPokemon.size() > 0) ||
+ (romEntry.romType == Gen4Constants.Type_Plat && romEntry.tweakFiles.containsKey("NewRoamerSubroutineTweak")) ||
+ (romEntry.romType == Gen4Constants.Type_HGSS && romEntry.tweakFiles.containsKey("NewRoamerSubroutineTweak"));
+
+ try {
+ computeCRC32sForRom();
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ // We want to guarantee that the catching tutorial in HGSS has Ethan/Lyra's new Pokemon. We also
+ // want to allow the option of randomizing the enemy Pokemon too. Unfortunately, the latter can
+ // occur *before* the former, but there's no guarantee that it will even happen. Since we *know*
+ // we'll need to do this patch eventually, just expand the arm9 here to make things easy.
+ if (romEntry.romType == Gen4Constants.Type_HGSS && romEntry.tweakFiles.containsKey("NewCatchingTutorialSubroutineTweak")) {
+ int extendBy = romEntry.getInt("Arm9ExtensionSize");
+ arm9 = extendARM9(arm9, extendBy, romEntry.getString("TCMCopyingPrefix"), Gen4Constants.arm9Offset);
+ genericIPSPatch(arm9, "NewCatchingTutorialSubroutineTweak");
+ }
+ }
+
+ private void loadMoves() {
+ try {
+ moveNarc = this.readNARC(romEntry.getFile("MoveData"));
+ moves = new Move[Gen4Constants.moveCount + 1];
+ List<String> moveNames = getStrings(romEntry.getInt("MoveNamesTextOffset"));
+ for (int i = 1; i <= Gen4Constants.moveCount; i++) {
+ byte[] moveData = moveNarc.files.get(i);
+ moves[i] = new Move();
+ moves[i].name = moveNames.get(i);
+ moves[i].number = i;
+ moves[i].internalId = i;
+ moves[i].effectIndex = readWord(moveData, 0);
+ moves[i].hitratio = (moveData[5] & 0xFF);
+ moves[i].power = moveData[3] & 0xFF;
+ moves[i].pp = moveData[6] & 0xFF;
+ moves[i].type = Gen4Constants.typeTable[moveData[4] & 0xFF];
+ moves[i].target = readWord(moveData, 8);
+ moves[i].category = Gen4Constants.moveCategoryIndices[moveData[2] & 0xFF];
+ moves[i].priority = moveData[10];
+ int flags = moveData[11] & 0xFF;
+ moves[i].makesContact = (flags & 1) != 0;
+ moves[i].isPunchMove = Gen4Constants.punchMoves.contains(moves[i].number);
+ moves[i].isSoundMove = Gen4Constants.soundMoves.contains(moves[i].number);
+
+ if (i == Moves.swift) {
+ perfectAccuracy = (int)moves[i].hitratio;
+ }
+
+ if (GlobalConstants.normalMultihitMoves.contains(i)) {
+ moves[i].hitCount = 3;
+ } else if (GlobalConstants.doubleHitMoves.contains(i)) {
+ moves[i].hitCount = 2;
+ } else if (i == Moves.tripleKick) {
+ moves[i].hitCount = 2.71; // this assumes the first hit lands
+ }
+
+ int secondaryEffectChance = moveData[7] & 0xFF;
+ loadStatChangesFromEffect(moves[i], secondaryEffectChance);
+ loadStatusFromEffect(moves[i], secondaryEffectChance);
+ loadMiscMoveInfoFromEffect(moves[i], secondaryEffectChance);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void loadStatChangesFromEffect(Move move, int secondaryEffectChance) {
+ switch (move.effectIndex) {
+ case Gen4Constants.noDamageAtkPlusOneEffect:
+ case Gen4Constants.noDamageDefPlusOneEffect:
+ case Gen4Constants.noDamageSpAtkPlusOneEffect:
+ case Gen4Constants.noDamageEvasionPlusOneEffect:
+ case Gen4Constants.noDamageAtkMinusOneEffect:
+ case Gen4Constants.noDamageDefMinusOneEffect:
+ case Gen4Constants.noDamageSpeMinusOneEffect:
+ case Gen4Constants.noDamageAccuracyMinusOneEffect:
+ case Gen4Constants.noDamageEvasionMinusOneEffect:
+ case Gen4Constants.noDamageAtkPlusTwoEffect:
+ case Gen4Constants.noDamageDefPlusTwoEffect:
+ case Gen4Constants.noDamageSpePlusTwoEffect:
+ case Gen4Constants.noDamageSpAtkPlusTwoEffect:
+ case Gen4Constants.noDamageSpDefPlusTwoEffect:
+ case Gen4Constants.noDamageAtkMinusTwoEffect:
+ case Gen4Constants.noDamageDefMinusTwoEffect:
+ case Gen4Constants.noDamageSpeMinusTwoEffect:
+ case Gen4Constants.noDamageSpDefMinusTwoEffect:
+ case Gen4Constants.minimizeEffect:
+ case Gen4Constants.swaggerEffect:
+ case Gen4Constants.defenseCurlEffect:
+ case Gen4Constants.flatterEffect:
+ case Gen4Constants.chargeEffect:
+ case Gen4Constants.noDamageAtkAndDefMinusOneEffect:
+ case Gen4Constants.noDamageDefAndSpDefPlusOneEffect:
+ case Gen4Constants.noDamageAtkAndDefPlusOneEffect:
+ case Gen4Constants.noDamageSpAtkAndSpDefPlusOneEffect:
+ case Gen4Constants.noDamageAtkAndSpePlusOneEffect:
+ case Gen4Constants.noDamageSpAtkMinusTwoEffect:
+ if (move.target == 16) {
+ move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER;
+ } else {
+ move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET;
+ }
+ break;
+
+ case Gen4Constants.damageAtkMinusOneEffect:
+ case Gen4Constants.damageDefMinusOneEffect:
+ case Gen4Constants.damageSpeMinusOneEffect:
+ case Gen4Constants.damageSpAtkMinusOneEffect:
+ case Gen4Constants.damageSpDefMinusOneEffect:
+ case Gen4Constants.damageAccuracyMinusOneEffect:
+ case Gen4Constants.damageSpDefMinusTwoEffect:
+ move.statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET;
+ break;
+
+ case Gen4Constants.damageUserDefPlusOneEffect:
+ case Gen4Constants.damageUserAtkPlusOneEffect:
+ case Gen4Constants.damageUserAllPlusOneEffect:
+ case Gen4Constants.damageUserAtkAndDefMinusOneEffect:
+ case Gen4Constants.damageUserSpAtkMinusTwoEffect:
+ case Gen4Constants.damageUserSpeMinusOneEffect:
+ case Gen4Constants.damageUserDefAndSpDefMinusOneEffect:
+ case Gen4Constants.damageUserSpAtkPlusOneEffect:
+ move.statChangeMoveType = StatChangeMoveType.DAMAGE_USER;
+ break;
+
+ default:
+ // Move does not have a stat-changing effect
+ return;
+ }
+
+ switch (move.effectIndex) {
+ case Gen4Constants.noDamageAtkPlusOneEffect:
+ case Gen4Constants.damageUserAtkPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen4Constants.noDamageDefPlusOneEffect:
+ case Gen4Constants.damageUserDefPlusOneEffect:
+ case Gen4Constants.defenseCurlEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen4Constants.noDamageSpAtkPlusOneEffect:
+ case Gen4Constants.flatterEffect:
+ case Gen4Constants.damageUserSpAtkPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen4Constants.noDamageEvasionPlusOneEffect:
+ case Gen4Constants.minimizeEffect:
+ move.statChanges[0].type = StatChangeType.EVASION;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen4Constants.noDamageAtkMinusOneEffect:
+ case Gen4Constants.damageAtkMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen4Constants.noDamageDefMinusOneEffect:
+ case Gen4Constants.damageDefMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen4Constants.noDamageSpeMinusOneEffect:
+ case Gen4Constants.damageSpeMinusOneEffect:
+ case Gen4Constants.damageUserSpeMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPEED;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen4Constants.noDamageAccuracyMinusOneEffect:
+ case Gen4Constants.damageAccuracyMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.ACCURACY;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen4Constants.noDamageEvasionMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.EVASION;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen4Constants.noDamageAtkPlusTwoEffect:
+ case Gen4Constants.swaggerEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen4Constants.noDamageDefPlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen4Constants.noDamageSpePlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPEED;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen4Constants.noDamageSpAtkPlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen4Constants.noDamageSpDefPlusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[0].stages = 2;
+ break;
+ case Gen4Constants.noDamageAtkMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen4Constants.noDamageDefMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen4Constants.noDamageSpeMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPEED;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen4Constants.noDamageSpDefMinusTwoEffect:
+ case Gen4Constants.damageSpDefMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen4Constants.damageSpAtkMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen4Constants.damageSpDefMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[0].stages = -1;
+ break;
+ case Gen4Constants.damageUserAllPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.ALL;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen4Constants.chargeEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[0].stages = 1;
+ break;
+ case Gen4Constants.damageUserAtkAndDefMinusOneEffect:
+ case Gen4Constants.noDamageAtkAndDefMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = -1;
+ move.statChanges[1].type = StatChangeType.DEFENSE;
+ move.statChanges[1].stages = -1;
+ break;
+ case Gen4Constants.damageUserSpAtkMinusTwoEffect:
+ case Gen4Constants.noDamageSpAtkMinusTwoEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK;
+ move.statChanges[0].stages = -2;
+ break;
+ case Gen4Constants.noDamageDefAndSpDefPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = 1;
+ move.statChanges[1].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[1].stages = 1;
+ break;
+ case Gen4Constants.noDamageAtkAndDefPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = 1;
+ move.statChanges[1].type = StatChangeType.DEFENSE;
+ move.statChanges[1].stages = 1;
+ break;
+ case Gen4Constants.noDamageSpAtkAndSpDefPlusOneEffect:
+ move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK;
+ move.statChanges[0].stages = 1;
+ move.statChanges[1].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[1].stages = 1;
+ break;
+ case Gen4Constants.noDamageAtkAndSpePlusOneEffect:
+ move.statChanges[0].type = StatChangeType.ATTACK;
+ move.statChanges[0].stages = 1;
+ move.statChanges[1].type = StatChangeType.SPEED;
+ move.statChanges[1].stages = 1;
+ break;
+ case Gen4Constants.damageUserDefAndSpDefMinusOneEffect:
+ move.statChanges[0].type = StatChangeType.DEFENSE;
+ move.statChanges[0].stages = -1;
+ move.statChanges[1].type = StatChangeType.SPECIAL_DEFENSE;
+ move.statChanges[1].stages = -1;
+ break;
+ }
+
+ if (move.statChangeMoveType == StatChangeMoveType.DAMAGE_TARGET || move.statChangeMoveType == StatChangeMoveType.DAMAGE_USER) {
+ for (int i = 0; i < move.statChanges.length; i++) {
+ if (move.statChanges[i].type != StatChangeType.NONE) {
+ move.statChanges[i].percentChance = secondaryEffectChance;
+ if (move.statChanges[i].percentChance == 0.0) {
+ move.statChanges[i].percentChance = 100.0;
+ }
+ }
+ }
+ }
+ }
+
+ private void loadStatusFromEffect(Move move, int secondaryEffectChance) {
+ switch (move.effectIndex) {
+ case Gen4Constants.noDamageSleepEffect:
+ case Gen4Constants.toxicEffect:
+ case Gen4Constants.noDamageConfusionEffect:
+ case Gen4Constants.noDamagePoisonEffect:
+ case Gen4Constants.noDamageParalyzeEffect:
+ case Gen4Constants.noDamageBurnEffect:
+ case Gen4Constants.swaggerEffect:
+ case Gen4Constants.flatterEffect:
+ case Gen4Constants.teeterDanceEffect:
+ move.statusMoveType = StatusMoveType.NO_DAMAGE;
+ break;
+
+ case Gen4Constants.damagePoisonEffect:
+ case Gen4Constants.damageBurnEffect:
+ case Gen4Constants.damageFreezeEffect:
+ case Gen4Constants.damageParalyzeEffect:
+ case Gen4Constants.damageConfusionEffect:
+ case Gen4Constants.twineedleEffect:
+ case Gen4Constants.damageBurnAndThawUserEffect:
+ case Gen4Constants.thunderEffect:
+ case Gen4Constants.blazeKickEffect:
+ case Gen4Constants.poisonFangEffect:
+ case Gen4Constants.damagePoisonWithIncreasedCritEffect:
+ case Gen4Constants.flareBlitzEffect:
+ case Gen4Constants.blizzardEffect:
+ case Gen4Constants.voltTackleEffect:
+ case Gen4Constants.bounceEffect:
+ case Gen4Constants.chatterEffect:
+ case Gen4Constants.fireFangEffect:
+ case Gen4Constants.iceFangEffect:
+ case Gen4Constants.thunderFangEffect:
+ move.statusMoveType = StatusMoveType.DAMAGE;
+ break;
+
+ default:
+ // Move does not have a status effect
+ return;
+ }
+
+ switch (move.effectIndex) {
+ case Gen4Constants.noDamageSleepEffect:
+ move.statusType = StatusType.SLEEP;
+ break;
+ case Gen4Constants.damagePoisonEffect:
+ case Gen4Constants.noDamagePoisonEffect:
+ case Gen4Constants.twineedleEffect:
+ case Gen4Constants.damagePoisonWithIncreasedCritEffect:
+ move.statusType = StatusType.POISON;
+ break;
+ case Gen4Constants.damageBurnEffect:
+ case Gen4Constants.damageBurnAndThawUserEffect:
+ case Gen4Constants.noDamageBurnEffect:
+ case Gen4Constants.blazeKickEffect:
+ case Gen4Constants.flareBlitzEffect:
+ case Gen4Constants.fireFangEffect:
+ move.statusType = StatusType.BURN;
+ break;
+ case Gen4Constants.damageFreezeEffect:
+ case Gen4Constants.blizzardEffect:
+ case Gen4Constants.iceFangEffect:
+ move.statusType = StatusType.FREEZE;
+ break;
+ case Gen4Constants.damageParalyzeEffect:
+ case Gen4Constants.noDamageParalyzeEffect:
+ case Gen4Constants.thunderEffect:
+ case Gen4Constants.voltTackleEffect:
+ case Gen4Constants.bounceEffect:
+ case Gen4Constants.thunderFangEffect:
+ move.statusType = StatusType.PARALYZE;
+ break;
+ case Gen4Constants.toxicEffect:
+ case Gen4Constants.poisonFangEffect:
+ move.statusType = StatusType.TOXIC_POISON;
+ break;
+ case Gen4Constants.noDamageConfusionEffect:
+ case Gen4Constants.damageConfusionEffect:
+ case Gen4Constants.swaggerEffect:
+ case Gen4Constants.flatterEffect:
+ case Gen4Constants.teeterDanceEffect:
+ case Gen4Constants.chatterEffect:
+ move.statusType = StatusType.CONFUSION;
+ break;
+ }
+
+ if (move.statusMoveType == StatusMoveType.DAMAGE) {
+ move.statusPercentChance = secondaryEffectChance;
+ if (move.statusPercentChance == 0.0) {
+ if (move.number == Moves.chatter) {
+ move.statusPercentChance = 1.0;
+ } else {
+ move.statusPercentChance = 100.0;
+ }
+ }
+ }
+ }
+
+ private void loadMiscMoveInfoFromEffect(Move move, int secondaryEffectChance) {
+ switch (move.effectIndex) {
+ case Gen4Constants.increasedCritEffect:
+ case Gen4Constants.blazeKickEffect:
+ case Gen4Constants.damagePoisonWithIncreasedCritEffect:
+ move.criticalChance = CriticalChance.INCREASED;
+ break;
+
+ case Gen4Constants.futureSightAndDoomDesireEffect:
+ move.criticalChance = CriticalChance.NONE;
+
+ case Gen4Constants.flinchEffect:
+ case Gen4Constants.snoreEffect:
+ case Gen4Constants.twisterEffect:
+ case Gen4Constants.stompEffect:
+ case Gen4Constants.fakeOutEffect:
+ case Gen4Constants.fireFangEffect:
+ case Gen4Constants.iceFangEffect:
+ case Gen4Constants.thunderFangEffect:
+ move.flinchPercentChance = secondaryEffectChance;
+ break;
+
+ case Gen4Constants.damageAbsorbEffect:
+ case Gen4Constants.dreamEaterEffect:
+ move.absorbPercent = 50;
+ break;
+
+ case Gen4Constants.damageRecoil25PercentEffect:
+ move.recoilPercent = 25;
+ break;
+
+ case Gen4Constants.damageRecoil33PercentEffect:
+ case Gen4Constants.flareBlitzEffect:
+ case Gen4Constants.voltTackleEffect:
+ move.recoilPercent = 33;
+ break;
+
+ case Gen4Constants.damageRecoil50PercentEffect:
+ move.recoilPercent = 50;
+ break;
+
+ case Gen4Constants.bindingEffect:
+ case Gen4Constants.trappingEffect:
+ move.isTrapMove = true;
+ break;
+
+ case Gen4Constants.skullBashEffect:
+ case Gen4Constants.solarbeamEffect:
+ case Gen4Constants.flyEffect:
+ case Gen4Constants.diveEffect:
+ case Gen4Constants.digEffect:
+ case Gen4Constants.bounceEffect:
+ case Gen4Constants.shadowForceEffect:
+ move.isChargeMove = true;
+ break;
+
+ case Gen3Constants.rechargeEffect:
+ move.isRechargeMove = true;
+ break;
+
+ case Gen4Constants.razorWindEffect:
+ move.criticalChance = CriticalChance.INCREASED;
+ move.isChargeMove = true;
+ break;
+
+ case Gen4Constants.skyAttackEffect:
+ move.criticalChance = CriticalChance.INCREASED;
+ move.flinchPercentChance = secondaryEffectChance;
+ move.isChargeMove = true;
+ break;
+ }
+ }
+
+ private void loadPokemonStats() {
+ try {
+ String pstatsnarc = romEntry.getFile("PokemonStats");
+ pokeNarc = this.readNARC(pstatsnarc);
+ String[] pokeNames = readPokemonNames();
+ int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
+ pokes = new Pokemon[Gen4Constants.pokemonCount + formeCount + 1];
+ for (int i = 1; i <= Gen4Constants.pokemonCount; i++) {
+ pokes[i] = new Pokemon();
+ pokes[i].number = i;
+ loadBasicPokeStats(pokes[i], pokeNarc.files.get(i));
+ // Name?
+ pokes[i].name = pokeNames[i];
+ }
+
+ int i = Gen4Constants.pokemonCount + 1;
+ for (int k: Gen4Constants.formeMappings.keySet()) {
+ if (i >= pokes.length) {
+ break;
+ }
+ pokes[i] = new Pokemon();
+ pokes[i].number = i;
+ loadBasicPokeStats(pokes[i], pokeNarc.files.get(k));
+ pokes[i].name = pokeNames[Gen4Constants.formeMappings.get(k).baseForme];
+ pokes[i].baseForme = pokes[Gen4Constants.formeMappings.get(k).baseForme];
+ pokes[i].formeNumber = Gen4Constants.formeMappings.get(k).formeNumber;
+ pokes[i].formeSuffix = Gen4Constants.formeSuffixes.get(k);
+ i = i + 1;
+ }
+
+ populateEvolutions();
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ private void loadBasicPokeStats(Pokemon pkmn, byte[] stats) {
+ pkmn.hp = stats[Gen4Constants.bsHPOffset] & 0xFF;
+ pkmn.attack = stats[Gen4Constants.bsAttackOffset] & 0xFF;
+ pkmn.defense = stats[Gen4Constants.bsDefenseOffset] & 0xFF;
+ pkmn.speed = stats[Gen4Constants.bsSpeedOffset] & 0xFF;
+ pkmn.spatk = stats[Gen4Constants.bsSpAtkOffset] & 0xFF;
+ pkmn.spdef = stats[Gen4Constants.bsSpDefOffset] & 0xFF;
+ // Type
+ pkmn.primaryType = Gen4Constants.typeTable[stats[Gen4Constants.bsPrimaryTypeOffset] & 0xFF];
+ pkmn.secondaryType = Gen4Constants.typeTable[stats[Gen4Constants.bsSecondaryTypeOffset] & 0xFF];
+ // Only one type?
+ if (pkmn.secondaryType == pkmn.primaryType) {
+ pkmn.secondaryType = null;
+ }
+ pkmn.catchRate = stats[Gen4Constants.bsCatchRateOffset] & 0xFF;
+ pkmn.growthCurve = ExpCurve.fromByte(stats[Gen4Constants.bsGrowthCurveOffset]);
+
+ // Abilities
+ pkmn.ability1 = stats[Gen4Constants.bsAbility1Offset] & 0xFF;
+ pkmn.ability2 = stats[Gen4Constants.bsAbility2Offset] & 0xFF;
+
+ // Held Items?
+ int item1 = readWord(stats, Gen4Constants.bsCommonHeldItemOffset);
+ int item2 = readWord(stats, Gen4Constants.bsRareHeldItemOffset);
+
+ if (item1 == item2) {
+ // guaranteed
+ pkmn.guaranteedHeldItem = item1;
+ pkmn.commonHeldItem = 0;
+ pkmn.rareHeldItem = 0;
+ } else {
+ pkmn.guaranteedHeldItem = 0;
+ pkmn.commonHeldItem = item1;
+ pkmn.rareHeldItem = item2;
+ }
+ pkmn.darkGrassHeldItem = -1;
+
+ pkmn.genderRatio = stats[Gen4Constants.bsGenderRatioOffset] & 0xFF;
+
+ int cosmeticForms = Gen4Constants.cosmeticForms.getOrDefault(pkmn.number,0);
+ if (cosmeticForms > 0 && romEntry.romType != Gen4Constants.Type_DP) {
+ pkmn.cosmeticForms = cosmeticForms;
+ }
+ }
+
+ private String[] readPokemonNames() {
+ String[] pokeNames = new String[Gen4Constants.pokemonCount + 1];
+ List<String> nameList = getStrings(romEntry.getInt("PokemonNamesTextOffset"));
+ for (int i = 1; i <= Gen4Constants.pokemonCount; i++) {
+ pokeNames[i] = nameList.get(i);
+ }
+ return pokeNames;
+ }
+
+ @Override
+ protected void savingROM() {
+ savePokemonStats();
+ saveMoves();
+ try {
+ writeARM9(arm9);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ try {
+ writeNARC(romEntry.getFile("Text"), msgNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ try {
+ writeNARC(romEntry.getFile("Scripts"), scriptNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ try {
+ writeNARC(romEntry.getFile("Events"), eventNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void saveMoves() {
+ for (int i = 1; i <= Gen4Constants.moveCount; i++) {
+ byte[] data = moveNarc.files.get(i);
+ writeWord(data, 0, moves[i].effectIndex);
+ data[2] = Gen4Constants.moveCategoryToByte(moves[i].category);
+ data[3] = (byte) moves[i].power;
+ data[4] = Gen4Constants.typeToByte(moves[i].type);
+ int hitratio = (int) Math.round(moves[i].hitratio);
+ if (hitratio < 0) {
+ hitratio = 0;
+ }
+ if (hitratio > 100) {
+ hitratio = 100;
+ }
+ data[5] = (byte) hitratio;
+ data[6] = (byte) moves[i].pp;
+ }
+
+ try {
+ this.writeNARC(romEntry.getFile("MoveData"), moveNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ private void savePokemonStats() {
+ // Update the "a/an X" list too, if it exists
+ List<String> namesList = getStrings(romEntry.getInt("PokemonNamesTextOffset"));
+ int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
+ if (romEntry.getString("HasExtraPokemonNames").equalsIgnoreCase("Yes")) {
+ List<String> namesList2 = getStrings(romEntry.getInt("PokemonNamesTextOffset") + 1);
+ for (int i = 1; i <= Gen4Constants.pokemonCount + formeCount; i++) {
+ if (i > Gen4Constants.pokemonCount) {
+ saveBasicPokeStats(pokes[i], pokeNarc.files.get(i + Gen4Constants.formeOffset));
+ continue;
+ }
+ saveBasicPokeStats(pokes[i], pokeNarc.files.get(i));
+ String oldName = namesList.get(i);
+ namesList.set(i, pokes[i].name);
+ namesList2.set(i, namesList2.get(i).replace(oldName, pokes[i].name));
+ }
+ setStrings(romEntry.getInt("PokemonNamesTextOffset") + 1, namesList2, false);
+ } else {
+ for (int i = 1; i <= Gen4Constants.pokemonCount + formeCount; i++) {
+ if (i > Gen4Constants.pokemonCount) {
+ saveBasicPokeStats(pokes[i], pokeNarc.files.get(i + Gen4Constants.formeOffset));
+ continue;
+ }
+ saveBasicPokeStats(pokes[i], pokeNarc.files.get(i));
+ namesList.set(i, pokes[i].name);
+ }
+ }
+ setStrings(romEntry.getInt("PokemonNamesTextOffset"), namesList, false);
+
+ try {
+ String pstatsnarc = romEntry.getFile("PokemonStats");
+ this.writeNARC(pstatsnarc, pokeNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ writeEvolutions();
+
+ }
+
+ private void saveBasicPokeStats(Pokemon pkmn, byte[] stats) {
+ stats[Gen4Constants.bsHPOffset] = (byte) pkmn.hp;
+ stats[Gen4Constants.bsAttackOffset] = (byte) pkmn.attack;
+ stats[Gen4Constants.bsDefenseOffset] = (byte) pkmn.defense;
+ stats[Gen4Constants.bsSpeedOffset] = (byte) pkmn.speed;
+ stats[Gen4Constants.bsSpAtkOffset] = (byte) pkmn.spatk;
+ stats[Gen4Constants.bsSpDefOffset] = (byte) pkmn.spdef;
+ stats[Gen4Constants.bsPrimaryTypeOffset] = Gen4Constants.typeToByte(pkmn.primaryType);
+ if (pkmn.secondaryType == null) {
+ stats[Gen4Constants.bsSecondaryTypeOffset] = stats[Gen4Constants.bsPrimaryTypeOffset];
+ } else {
+ stats[Gen4Constants.bsSecondaryTypeOffset] = Gen4Constants.typeToByte(pkmn.secondaryType);
+ }
+ stats[Gen4Constants.bsCatchRateOffset] = (byte) pkmn.catchRate;
+ stats[Gen4Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte();
+
+ stats[Gen4Constants.bsAbility1Offset] = (byte) pkmn.ability1;
+ stats[Gen4Constants.bsAbility2Offset] = (byte) pkmn.ability2;
+
+ // Held items
+ if (pkmn.guaranteedHeldItem > 0) {
+ writeWord(stats, Gen4Constants.bsCommonHeldItemOffset, pkmn.guaranteedHeldItem);
+ writeWord(stats, Gen4Constants.bsRareHeldItemOffset, pkmn.guaranteedHeldItem);
+ } else {
+ writeWord(stats, Gen4Constants.bsCommonHeldItemOffset, pkmn.commonHeldItem);
+ writeWord(stats, Gen4Constants.bsRareHeldItemOffset, pkmn.rareHeldItem);
+ }
+ }
+
+ @Override
+ public List<Pokemon> getPokemon() {
+ return pokemonList;
+ }
+
+ @Override
+ public List<Pokemon> getPokemonInclFormes() {
+ return pokemonListInclFormes; // No formes for now
+ }
+
+ @Override
+ public List<Pokemon> getAltFormes() {
+ int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
+ return pokemonListInclFormes.subList(Gen4Constants.pokemonCount + 1, Gen4Constants.pokemonCount + formeCount + 1);
+ }
+
+ @Override
+ public List<MegaEvolution> getMegaEvolutions() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public Pokemon getAltFormeOfPokemon(Pokemon pk, int forme) {
+ int pokeNum = Gen4Constants.getAbsolutePokeNumByBaseForme(pk.number,forme);
+ return pokeNum != 0 ? pokes[pokeNum] : pk;
+ }
+
+ @Override
+ public List<Pokemon> getIrregularFormes() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public boolean hasFunctionalFormes() {
+ return romEntry.romType != Gen4Constants.Type_DP;
+ }
+
+ @Override
+ public List<Pokemon> getStarters() {
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ List<Integer> tailOffsets = RomFunctions.search(arm9, Gen4Constants.hgssStarterCodeSuffix);
+ if (tailOffsets.size() == 1) {
+ // Found starters
+ int starterOffset = tailOffsets.get(0) - 13;
+ int poke1 = readWord(arm9, starterOffset);
+ int poke2 = readWord(arm9, starterOffset + 4);
+ int poke3 = readWord(arm9, starterOffset + 8);
+ return Arrays.asList(pokes[poke1], pokes[poke2], pokes[poke3]);
+ } else {
+ return Arrays.asList(pokes[Species.chikorita], pokes[Species.cyndaquil],
+ pokes[Species.totodile]);
+ }
+ } else {
+ try {
+ byte[] starterData = readOverlay(romEntry.getInt("StarterPokemonOvlNumber"));
+ int poke1 = readWord(starterData, romEntry.getInt("StarterPokemonOffset"));
+ int poke2 = readWord(starterData, romEntry.getInt("StarterPokemonOffset") + 4);
+ int poke3 = readWord(starterData, romEntry.getInt("StarterPokemonOffset") + 8);
+ return Arrays.asList(pokes[poke1], pokes[poke2], pokes[poke3]);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+ }
+
+ @Override
+ public boolean setStarters(List<Pokemon> newStarters) {
+ if (newStarters.size() != 3) {
+ return false;
+ }
+
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ List<Integer> tailOffsets = RomFunctions.search(arm9, Gen4Constants.hgssStarterCodeSuffix);
+ if (tailOffsets.size() == 1) {
+ // Found starters
+ int starterOffset = tailOffsets.get(0) - 13;
+ writeWord(arm9, starterOffset, newStarters.get(0).number);
+ writeWord(arm9, starterOffset + 4, newStarters.get(1).number);
+ writeWord(arm9, starterOffset + 8, newStarters.get(2).number);
+ // Go fix the rival scripts, which rely on fixed pokemon numbers
+ // The logic to be changed each time is roughly:
+ // Set 0x800C = player starter
+ // If(0x800C==152) { trainerbattle rival w/ cynda }
+ // ElseIf(0x800C==155) { trainerbattle rival w/ totodile }
+ // Else { trainerbattle rival w/ chiko }
+ // So we basically have to adjust the 152 and the 155.
+ int[] filesWithRivalScript = Gen4Constants.hgssFilesWithRivalScript;
+ // below code represents a rival script for sure
+ // it means: StoreStarter2 0x800C; If 0x800C 152; CheckLR B_!=
+ // <offset to follow>
+ byte[] magic = Gen4Constants.hgssRivalScriptMagic;
+ NARCArchive scriptNARC = scriptNarc;
+ for (int fileCheck : filesWithRivalScript) {
+ byte[] file = scriptNARC.files.get(fileCheck);
+ List<Integer> rivalOffsets = RomFunctions.search(file, magic);
+ if (rivalOffsets.size() == 1) {
+ // found, adjust
+ int baseOffset = rivalOffsets.get(0);
+ // Replace 152 (chiko) with first starter
+ writeWord(file, baseOffset + 8, newStarters.get(0).number);
+ int jumpAmount = readLong(file, baseOffset + 13);
+ int secondBase = jumpAmount + baseOffset + 17;
+ // TODO find out what this constant 0x11 is and remove
+ // it
+ if (file[secondBase] != 0x11 || (file[secondBase + 4] & 0xFF) != Species.cyndaquil) {
+ // This isn't what we were expecting...
+ } else {
+ // Replace 155 (cynda) with 2nd starter
+ writeWord(file, secondBase + 4, newStarters.get(1).number);
+ }
+ }
+ }
+ // Fix starter text
+ List<String> spStrings = getStrings(romEntry.getInt("StarterScreenTextOffset"));
+ String[] intros = new String[] { "So, you like", "You’ll take", "Do you want" };
+ for (int i = 0; i < 3; i++) {
+ Pokemon newStarter = newStarters.get(i);
+ int color = (i == 0) ? 3 : i;
+ String newStarterDesc = "Professor Elm: " + intros[i] + " \\vFF00\\z000" + color + newStarter.name
+ + "\\vFF00\\z0000,\\nthe " + newStarter.primaryType.camelCase() + "-type Pokémon?";
+ spStrings.set(i + 1, newStarterDesc);
+ String altStarterDesc = "\\vFF00\\z000" + color + newStarter.name + "\\vFF00\\z0000, the "
+ + newStarter.primaryType.camelCase() + "-type Pokémon, is\\nin this Poké Ball!";
+ spStrings.set(i + 4, altStarterDesc);
+ }
+ setStrings(romEntry.getInt("StarterScreenTextOffset"), spStrings);
+
+
+ try {
+ // Fix starter cries
+ byte[] starterPokemonOverlay = readOverlay(romEntry.getInt("StarterPokemonOvlNumber"));
+ String spCriesPrefix = Gen4Constants.starterCriesPrefix;
+ int offset = find(starterPokemonOverlay, spCriesPrefix);
+ if (offset > 0) {
+ offset += spCriesPrefix.length() / 2; // because it was a prefix
+ for (Pokemon newStarter: newStarters) {
+ writeLong(starterPokemonOverlay, offset, newStarter.number);
+ offset += 4;
+ }
+ }
+ writeOverlay(romEntry.getInt("StarterPokemonOvlNumber"), starterPokemonOverlay);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ try {
+ byte[] starterData = readOverlay(romEntry.getInt("StarterPokemonOvlNumber"));
+ writeWord(starterData, romEntry.getInt("StarterPokemonOffset"), newStarters.get(0).number);
+ writeWord(starterData, romEntry.getInt("StarterPokemonOffset") + 4, newStarters.get(1).number);
+ writeWord(starterData, romEntry.getInt("StarterPokemonOffset") + 8, newStarters.get(2).number);
+
+ if (romEntry.romType == Gen4Constants.Type_DP || romEntry.romType == Gen4Constants.Type_Plat) {
+ String starterPokemonGraphicsPrefix = romEntry.getString("StarterPokemonGraphicsPrefix");
+ int offset = find(starterData, starterPokemonGraphicsPrefix);
+ if (offset > 0) {
+
+ // The original subroutine for handling the starter graphics is optimized by the compiler to use
+ // a value as a pointer offset and then adding to that value to get the Pokemon's index.
+ // We will keep this logic, but in order to make place for an extra instruction that will let
+ // us set the Pokemon index to any Gen 4 value we want, we change the base address of the
+ // pointer that the offset is used for; this also requires some changes to the instructions
+ // that utilize this pointer.
+ offset += starterPokemonGraphicsPrefix.length() / 2;
+
+ // Move down a section of instructions to make place for an add instruction that modifies the
+ // pointer. A PC-relative load and a BL have to be slightly modified to point to the correct
+ // thing.
+ writeWord(starterData, offset+0xC, readWord(starterData, offset+0xA));
+ if (offset % 4 == 0) {
+ starterData[offset+0xC] = (byte)(starterData[offset+0xC] - 1);
+ }
+ writeWord(starterData, offset+0xA, readWord(starterData, offset+0x8));
+ starterData[offset+0xA] = (byte)(starterData[offset+0xA] - 1);
+ writeWord(starterData, offset+0x8, readWord(starterData, offset+0x6));
+ writeWord(starterData, offset+0x6, readWord(starterData, offset+0x4));
+ writeWord(starterData, offset+0x4, readWord(starterData, offset+0x2));
+ // This instruction normally uses the value in r0 (0x200) as an offset for an ldr that uses
+ // the pointer as its base address; we change this to not use an offset at all because we
+ // change the instruction before it to add that 0x200 to the base address.
+ writeWord(starterData, offset+0x2, 0x6828);
+ writeWord(starterData, offset, 0x182D);
+
+ offset += 0x16;
+ // Change another ldr to not use any offset since we changed the base address
+ writeWord(starterData, offset, 0x6828);
+
+ offset += 0xA;
+
+ // This is where we write the actual starter numbers, as two adds/subs
+
+ for (int i = 0; i < 3; i++) {
+ // The offset that we want to use for the pointer is 4, then 8, then 0xC.
+ // We take the difference of the Pokemon's index and the offset, because we want to add
+ // (or subtract) that to/from the offset to get the Pokemon's index later.
+ int starterDiff = newStarters.get(i).number - (4*(i+1));
+
+ // Prepare two "add r0, #0x0" instructions where we'll modify the immediate
+ int instr1 = 0x3200;
+ int instr2 = 0x3200;
+
+ if (starterDiff < 0) {
+ // Pokemon's index is below the offset, change to a sub instruction
+ instr1 |= 0x800;
+ starterDiff = Math.abs(starterDiff);
+ } else if (starterDiff > 255) {
+ // Pokemon's index is above (offset + 255), need to utilize the second add instruction
+ instr2 |= 0xFF;
+ starterDiff -= 255;
+ }
+
+ // Modify the first add instruction's immediate value
+ instr1 |= (starterDiff & 0xFF);
+
+ // Change the original offset that's loaded, then move an instruction up one step
+ // and insert our add instructions
+ starterData[offset] = (byte)(4*(i+1));
+ writeWord(starterData, offset+2, readWord(starterData, offset+4));
+ writeWord(starterData, offset+4, instr1);
+ writeWord(starterData, offset+8, instr2);
+
+ // Repeat for each starter
+ offset += 0xE;
+ }
+
+ // Change a loaded value to be 1 instead of 0x81 because we changed the pointer
+ starterData[offset] = 1;
+
+ // Also need to change one usage of the pointer we changed, in the inner function
+ String starterPokemonGraphicsPrefixInner = romEntry.getString("StarterPokemonGraphicsPrefixInner");
+ offset = find(starterData, starterPokemonGraphicsPrefixInner);
+
+ if (offset > 0) {
+ offset += starterPokemonGraphicsPrefixInner.length() / 2;
+ starterData[offset+1] = 0x68;
+ }
+ }
+ }
+
+ writeOverlay(romEntry.getInt("StarterPokemonOvlNumber"), starterData);
+ // Patch DPPt-style rival scripts
+ // these have a series of IfJump commands
+ // following pokemon IDs
+ // the jumps either go to trainer battles, or a HoF times
+ // checker, or the StarterBattle command (Pt only)
+ // the HoF times checker case is for the Fight Area or Survival
+ // Area (depending on version).
+ // the StarterBattle case is for Route 201 in Pt.
+ int[] filesWithRivalScript = (romEntry.romType == Gen4Constants.Type_Plat) ? Gen4Constants.ptFilesWithRivalScript
+ : Gen4Constants.dpFilesWithRivalScript;
+ byte[] magic = Gen4Constants.dpptRivalScriptMagic;
+ NARCArchive scriptNARC = scriptNarc;
+ for (int fileCheck : filesWithRivalScript) {
+ byte[] file = scriptNARC.files.get(fileCheck);
+ List<Integer> rivalOffsets = RomFunctions.search(file, magic);
+ if (rivalOffsets.size() > 0) {
+ for (int baseOffset : rivalOffsets) {
+ // found, check for trainer battle or HoF
+ // check at jump
+ int jumpLoc = baseOffset + magic.length;
+ int jumpTo = readLong(file, jumpLoc) + jumpLoc + 4;
+ // TODO find out what these constants are and remove
+ // them
+ if (readWord(file, jumpTo) != 0xE5 && readWord(file, jumpTo) != 0x28F
+ && (readWord(file, jumpTo) != 0x125 || romEntry.romType != Gen4Constants.Type_Plat)) {
+ continue; // not a rival script
+ }
+ // Replace the two starter-words 387 and 390
+ writeWord(file, baseOffset + 0x8, newStarters.get(0).number);
+ writeWord(file, baseOffset + 0x15, newStarters.get(1).number);
+ }
+ }
+ }
+ // Tag battles with rival or friend
+ // Have their own script magic
+ // 2 for Lucas/Dawn (=4 occurrences), 1 or 2 for Barry
+ byte[] tagBattleMagic = Gen4Constants.dpptTagBattleScriptMagic1;
+ byte[] tagBattleMagic2 = Gen4Constants.dpptTagBattleScriptMagic2;
+ int[] filesWithTagBattleScript = (romEntry.romType == Gen4Constants.Type_Plat) ? Gen4Constants.ptFilesWithTagScript
+ : Gen4Constants.dpFilesWithTagScript;
+ for (int fileCheck : filesWithTagBattleScript) {
+ byte[] file = scriptNARC.files.get(fileCheck);
+ List<Integer> tbOffsets = RomFunctions.search(file, tagBattleMagic);
+ if (tbOffsets.size() > 0) {
+ for (int baseOffset : tbOffsets) {
+ // found, check for second part
+ int secondPartStart = baseOffset + tagBattleMagic.length + 2;
+ if (secondPartStart + tagBattleMagic2.length > file.length) {
+ continue; // match failed
+ }
+ boolean valid = true;
+ for (int spo = 0; spo < tagBattleMagic2.length; spo++) {
+ if (file[secondPartStart + spo] != tagBattleMagic2[spo]) {
+ valid = false;
+ break;
+ }
+ }
+ if (!valid) {
+ continue;
+ }
+ // Make sure the jump following the second
+ // part jumps to a <return> command
+ int jumpLoc = secondPartStart + tagBattleMagic2.length;
+ int jumpTo = readLong(file, jumpLoc) + jumpLoc + 4;
+ // TODO find out what this constant is and remove it
+ if (readWord(file, jumpTo) != 0x1B) {
+ continue; // not a tag battle script
+ }
+ // Replace the two starter-words
+ if (readWord(file, baseOffset + 0x21) == Species.turtwig) {
+ // first starter
+ writeWord(file, baseOffset + 0x21, newStarters.get(0).number);
+ } else {
+ // third starter
+ writeWord(file, baseOffset + 0x21, newStarters.get(2).number);
+ }
+ // second starter
+ writeWord(file, baseOffset + 0xE, newStarters.get(1).number);
+ }
+ }
+ }
+ // Fix starter script text
+ // The starter picking screen
+ List<String> spStrings = getStrings(romEntry.getInt("StarterScreenTextOffset"));
+ // Get pokedex info
+ List<String> pokedexSpeciesStrings = getStrings(romEntry.getInt("PokedexSpeciesTextOffset"));
+ for (int i = 0; i < 3; i++) {
+ Pokemon newStarter = newStarters.get(i);
+ int color = (i == 0) ? 3 : i;
+ String newStarterDesc = "\\vFF00\\z000" + color + pokedexSpeciesStrings.get(newStarter.number)
+ + " " + newStarter.name + "\\vFF00\\z0000!\\nWill you take this Pokémon?";
+ spStrings.set(i + 1, newStarterDesc);
+ }
+ // rewrite starter picking screen
+ setStrings(romEntry.getInt("StarterScreenTextOffset"), spStrings);
+ if (romEntry.romType == Gen4Constants.Type_DP) {
+ // what rival says after we get the Pokemon
+ List<String> lakeStrings = getStrings(romEntry.getInt("StarterLocationTextOffset"));
+ lakeStrings
+ .set(Gen4Constants.dpStarterStringIndex,
+ "\\v0103\\z0000: Fwaaah!\\nYour Pokémon totally rocked!\\pBut mine was way tougher\\nthan yours!\\p...They were other people’s\\nPokémon, though...\\pBut we had to use them...\\nThey won’t mind, will they?\\p");
+ setStrings(romEntry.getInt("StarterLocationTextOffset"), lakeStrings);
+ } else {
+ // what rival says after we get the Pokemon
+ List<String> r201Strings = getStrings(romEntry.getInt("StarterLocationTextOffset"));
+ r201Strings.set(Gen4Constants.ptStarterStringIndex,
+ "\\v0103\\z0000\\z0000: Then, I choose you!\\nI’m picking this one!\\p");
+ setStrings(romEntry.getInt("StarterLocationTextOffset"), r201Strings);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return true;
+ }
+ }
+
+ @Override
+ public boolean supportsStarterHeldItems() {
+ return romEntry.romType == Gen4Constants.Type_DP || romEntry.romType == Gen4Constants.Type_Plat;
+ }
+
+ @Override
+ public List<Integer> getStarterHeldItems() {
+ int starterScriptNumber = romEntry.getInt("StarterPokemonScriptOffset");
+ int starterHeldItemOffset = romEntry.getInt("StarterPokemonHeldItemOffset");
+ byte[] file = scriptNarc.files.get(starterScriptNumber);
+ int item = FileFunctions.read2ByteInt(file, starterHeldItemOffset);
+ return Arrays.asList(item);
+ }
+
+ @Override
+ public void setStarterHeldItems(List<Integer> items) {
+ int starterScriptNumber = romEntry.getInt("StarterPokemonScriptOffset");
+ int starterHeldItemOffset = romEntry.getInt("StarterPokemonHeldItemOffset");
+ byte[] file = scriptNarc.files.get(starterScriptNumber);
+ FileFunctions.write2ByteInt(file, starterHeldItemOffset, items.get(0));
+ }
+
+ @Override
+ public List<Move> getMoves() {
+ return Arrays.asList(moves);
+ }
+
+ @Override
+ public List<EncounterSet> getEncounters(boolean useTimeOfDay) {
+ if (!loadedWildMapNames) {
+ loadWildMapNames();
+ }
+
+ try {
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ return getEncountersHGSS(useTimeOfDay);
+ } else {
+ return getEncountersDPPt(useTimeOfDay);
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ }
+
+ private List<EncounterSet> getEncountersDPPt(boolean useTimeOfDay) throws IOException {
+ // Determine file to use
+ String encountersFile = romEntry.getFile("WildPokemon");
+
+ NARCArchive encounterData = readNARC(encountersFile);
+ List<EncounterSet> encounters = new ArrayList<>();
+ // Credit for
+ // https://github.com/magical/pokemon-encounters/blob/master/nds/encounters-gen4-sinnoh.py
+ // for the structure for this.
+ int c = -1;
+ for (byte[] b : encounterData.files) {
+ c++;
+ if (!wildMapNames.containsKey(c)) {
+ wildMapNames.put(c, "? Unknown ?");
+ }
+ String mapName = wildMapNames.get(c);
+ int grassRate = readLong(b, 0);
+ if (grassRate != 0) {
+ // up to 4
+ List<Encounter> grassEncounters = readEncountersDPPt(b, 4, 12);
+ EncounterSet grass = new EncounterSet();
+ grass.displayName = mapName + " Grass/Cave";
+ grass.encounters = grassEncounters;
+ grass.rate = grassRate;
+ grass.offset = c;
+ encounters.add(grass);
+
+ // Time of day replacements?
+ if (useTimeOfDay) {
+ for (int i = 0; i < 4; i++) {
+ int pknum = readLong(b, 108 + 4 * i);
+ if (pknum >= 1 && pknum <= Gen4Constants.pokemonCount) {
+ Pokemon pk = pokes[pknum];
+ Encounter enc = new Encounter();
+ enc.level = grassEncounters.get(Gen4Constants.dpptAlternateSlots[i + 2]).level;
+ enc.pokemon = pk;
+ grassEncounters.add(enc);
+ }
+ }
+ }
+ // (if useTimeOfDay is off, just override them later)
+
+ // Other conditional replacements (swarm, radar, GBA)
+ EncounterSet conds = new EncounterSet();
+ conds.displayName = mapName + " Swarm/Radar/GBA";
+ conds.rate = grassRate;
+ conds.offset = c;
+ for (int i = 0; i < 20; i++) {
+ if (i >= 2 && i <= 5) {
+ // Time of day slot, handled already
+ continue;
+ }
+ int offs = 100 + i * 4 + (i >= 10 ? 24 : 0);
+ int pknum = readLong(b, offs);
+ if (pknum >= 1 && pknum <= Gen4Constants.pokemonCount) {
+ Pokemon pk = pokes[pknum];
+ Encounter enc = new Encounter();
+ enc.level = grassEncounters.get(Gen4Constants.dpptAlternateSlots[i]).level;
+ enc.pokemon = pk;
+ conds.encounters.add(enc);
+ }
+ }
+ if (conds.encounters.size() > 0) {
+ encounters.add(conds);
+ }
+ }
+
+ // up to 204, 5 sets of "sea" encounters to go
+ int offset = 204;
+ for (int i = 0; i < 5; i++) {
+ int rate = readLong(b, offset);
+ offset += 4;
+ List<Encounter> encountersHere = readSeaEncountersDPPt(b, offset, 5);
+ offset += 40;
+ if (rate == 0 || i == 1) {
+ continue;
+ }
+ EncounterSet other = new EncounterSet();
+ other.displayName = mapName + " " + Gen4Constants.dpptWaterSlotSetNames[i];
+ other.offset = c;
+ other.encounters = encountersHere;
+ other.rate = rate;
+ encounters.add(other);
+ }
+ }
+
+ // Now do the extra encounters (Feebas tiles, honey trees, Great Marsh rotating Pokemon, etc.)
+ String extraEncountersFile = romEntry.getFile("ExtraEncounters");
+ NARCArchive extraEncounterData = readNARC(extraEncountersFile);
+
+ // Feebas tiles
+ byte[] feebasData = extraEncounterData.files.get(0);
+ EncounterSet feebasEncounters = readExtraEncountersDPPt(feebasData, 0, 1);
+ byte[] encounterOverlay = readOverlay(romEntry.getInt("EncounterOvlNumber"));
+ int offset = find(encounterOverlay, Gen4Constants.feebasLevelPrefixDPPt);
+ if (offset > 0) {
+ offset += Gen4Constants.feebasLevelPrefixDPPt.length() / 2; // because it was a prefix
+ for (Encounter enc : feebasEncounters.encounters) {
+ enc.maxLevel = encounterOverlay[offset];
+ enc.level = encounterOverlay[offset + 4];
+ }
+ }
+ feebasEncounters.displayName = "Mt. Coronet Feebas Tiles";
+ encounters.add(feebasEncounters);
+
+ // Honey trees
+ int[] honeyTreeOffsets = romEntry.arrayEntries.get("HoneyTreeOffsets");
+ for (int i = 0; i < honeyTreeOffsets.length; i++) {
+ byte[] honeyTreeData = extraEncounterData.files.get(honeyTreeOffsets[i]);
+ EncounterSet honeyTreeEncounters = readExtraEncountersDPPt(honeyTreeData, 0, 6);
+ offset = find(encounterOverlay, Gen4Constants.honeyTreeLevelPrefixDPPt);
+ if (offset > 0) {
+ offset += Gen4Constants.honeyTreeLevelPrefixDPPt.length() / 2; // because it was a prefix
+
+ // To make different min levels work, we rewrite some assembly code in
+ // setEncountersDPPt, which has the side effect of making reading the min
+ // level easier. In case the original code is still there, just hardcode
+ // the min level used in the vanilla game, since extracting it is hard.
+ byte level;
+ if (encounterOverlay[offset + 46] == 0x0B && encounterOverlay[offset + 47] == 0x2E) {
+ level = 5;
+ } else {
+ level = encounterOverlay[offset + 46];
+ }
+ for (Encounter enc : honeyTreeEncounters.encounters) {
+ enc.maxLevel = encounterOverlay[offset + 102];
+ enc.level = level;
+ }
+ }
+ honeyTreeEncounters.displayName = "Honey Tree Group " + (i + 1);
+ encounters.add(honeyTreeEncounters);
+ }
+
+ // Trophy Garden rotating Pokemon (Mr. Backlot)
+ byte[] trophyGardenData = extraEncounterData.files.get(8);
+ EncounterSet trophyGardenEncounters = readExtraEncountersDPPt(trophyGardenData, 0, 16);
+
+ // Trophy Garden rotating Pokemon get their levels from the regular Trophy Garden grass encounters,
+ // indices 6 and 7. To make the logs nice, read in these encounters for this area and set the level
+ // and maxLevel for the rotating encounters appropriately.
+ int trophyGardenGrassEncounterIndex = Gen4Constants.getTrophyGardenGrassEncounterIndex(romEntry.romType);
+ EncounterSet trophyGardenGrassEncounterSet = encounters.get(trophyGardenGrassEncounterIndex);
+ int level1 = trophyGardenGrassEncounterSet.encounters.get(6).level;
+ int level2 = trophyGardenGrassEncounterSet.encounters.get(7).level;
+ for (Encounter enc : trophyGardenEncounters.encounters) {
+ enc.level = Math.min(level1, level2);
+ if (level1 != level2) {
+ enc.maxLevel = Math.max(level1, level2);
+ }
+ }
+ trophyGardenEncounters.displayName = "Trophy Garden Rotating Pokemon (via Mr. Backlot)";
+ encounters.add(trophyGardenEncounters);
+
+ // Great Marsh rotating Pokemon
+ int[] greatMarshOffsets = new int[]{9, 10};
+ for (int i = 0; i < greatMarshOffsets.length; i++) {
+ byte[] greatMarshData = extraEncounterData.files.get(greatMarshOffsets[i]);
+ EncounterSet greatMarshEncounters = readExtraEncountersDPPt(greatMarshData, 0, 32);
+
+ // Great Marsh rotating Pokemon get their levels from the regular Great Marsh grass encounters,
+ // indices 6 and 7. To make the logs nice, read in these encounters for all areas and set the
+ // level and maxLevel for the rotating encounters appropriately.
+ int level = 100;
+ int maxLevel = 0;
+ List<Integer> marshGrassEncounterIndices = Gen4Constants.getMarshGrassEncounterIndices(romEntry.romType);
+ for (int j = 0; j < marshGrassEncounterIndices.size(); j++) {
+ EncounterSet marshGrassEncounterSet = encounters.get(marshGrassEncounterIndices.get(j));
+ int currentLevel = marshGrassEncounterSet.encounters.get(6).level;
+ if (currentLevel < level) {
+ level = currentLevel;
+ }
+ if (currentLevel > maxLevel) {
+ maxLevel = currentLevel;
+ }
+ currentLevel = marshGrassEncounterSet.encounters.get(7).level;
+ if (currentLevel < level) {
+ level = currentLevel;
+ }
+ if (currentLevel > maxLevel) {
+ maxLevel = currentLevel;
+ }
+ }
+ for (Encounter enc : greatMarshEncounters.encounters) {
+ enc.level = level;
+ enc.maxLevel = maxLevel;
+ }
+ String pokedexStatus = i == 0 ? "(Post-National Dex)" : "(Pre-National Dex)";
+ greatMarshEncounters.displayName = "Great Marsh Rotating Pokemon " + pokedexStatus;
+ encounters.add(greatMarshEncounters);
+ }
+ return encounters;
+ }
+
+ private List<Encounter> readEncountersDPPt(byte[] data, int offset, int amount) {
+ List<Encounter> encounters = new ArrayList<>();
+ for (int i = 0; i < amount; i++) {
+ int level = readLong(data, offset + i * 8);
+ int pokemon = readLong(data, offset + 4 + i * 8);
+ Encounter enc = new Encounter();
+ enc.level = level;
+ enc.pokemon = pokes[pokemon];
+ encounters.add(enc);
+ }
+ return encounters;
+ }
+
+ private List<Encounter> readSeaEncountersDPPt(byte[] data, int offset, int amount) {
+ List<Encounter> encounters = new ArrayList<>();
+ for (int i = 0; i < amount; i++) {
+ int level = readLong(data, offset + i * 8);
+ int pokemon = readLong(data, offset + 4 + i * 8);
+ Encounter enc = new Encounter();
+ enc.level = level >> 8;
+ enc.maxLevel = level & 0xFF;
+ enc.pokemon = pokes[pokemon];
+ encounters.add(enc);
+ }
+ return encounters;
+ }
+
+ private EncounterSet readExtraEncountersDPPt(byte[] data, int offset, int amount) {
+ EncounterSet es = new EncounterSet();
+ es.rate = 1;
+ for (int i = 0; i < amount; i++) {
+ int pokemon = readLong(data, offset + i * 4);
+ Encounter e = new Encounter();
+ e.level = 1;
+ e.pokemon = pokes[pokemon];
+ es.encounters.add(e);
+ }
+ return es;
+ }
+
+ private List<EncounterSet> getEncountersHGSS(boolean useTimeOfDay) throws IOException {
+ String encountersFile = romEntry.getFile("WildPokemon");
+ NARCArchive encounterData = readNARC(encountersFile);
+ List<EncounterSet> encounters = new ArrayList<>();
+ // Credit for
+ // https://github.com/magical/pokemon-encounters/blob/master/nds/encounters-gen4-johto.py
+ // for the structure for this.
+ int[] amounts = new int[] { 0, 5, 2, 5, 5, 5 };
+ int c = -1;
+ for (byte[] b : encounterData.files) {
+ c++;
+ if (!wildMapNames.containsKey(c)) {
+ wildMapNames.put(c, "? Unknown ?");
+ }
+ String mapName = wildMapNames.get(c);
+ int[] rates = new int[6];
+ rates[0] = b[0] & 0xFF;
+ rates[1] = b[1] & 0xFF;
+ rates[2] = b[2] & 0xFF;
+ rates[3] = b[3] & 0xFF;
+ rates[4] = b[4] & 0xFF;
+ rates[5] = b[5] & 0xFF;
+ // Up to 8 after the rates
+ // Grass has to be handled on its own because the levels
+ // are reused for every time of day
+ int[] grassLevels = new int[12];
+ for (int i = 0; i < 12; i++) {
+ grassLevels[i] = b[8 + i] & 0xFF;
+ }
+ // Up to 20 now (12 for levels)
+ Pokemon[][] grassPokes = new Pokemon[3][12];
+ grassPokes[0] = readPokemonHGSS(b, 20, 12);
+ grassPokes[1] = readPokemonHGSS(b, 44, 12);
+ grassPokes[2] = readPokemonHGSS(b, 68, 12);
+ // Up to 92 now (12*2*3 for pokemon)
+ if (rates[0] != 0) {
+ if (!useTimeOfDay) {
+ // Just write "day" encounters
+ List<Encounter> grassEncounters = stitchEncsToLevels(grassPokes[1], grassLevels);
+ EncounterSet grass = new EncounterSet();
+ grass.encounters = grassEncounters;
+ grass.rate = rates[0];
+ grass.displayName = mapName + " Grass/Cave";
+ encounters.add(grass);
+ } else {
+ for (int i = 0; i < 3; i++) {
+ EncounterSet grass = new EncounterSet();
+ grass.encounters = stitchEncsToLevels(grassPokes[i], grassLevels);
+ grass.rate = rates[0];
+ grass.displayName = mapName + " " + Gen4Constants.hgssTimeOfDayNames[i] + " Grass/Cave";
+ encounters.add(grass);
+ }
+ }
+ }
+
+ // Hoenn/Sinnoh Radio
+ EncounterSet radio = readOptionalEncountersHGSS(b, 92, 4);
+ radio.displayName = mapName + " Hoenn/Sinnoh Radio";
+ if (radio.encounters.size() > 0) {
+ encounters.add(radio);
+ }
+
+ // Up to 100 now... 2*2*2 for radio pokemon
+ // Time to handle Surfing, Rock Smash, Rods
+ int offset = 100;
+ for (int i = 1; i < 6; i++) {
+ List<Encounter> encountersHere = readSeaEncountersHGSS(b, offset, amounts[i]);
+ offset += 4 * amounts[i];
+ if (rates[i] != 0) {
+ // Valid area.
+ EncounterSet other = new EncounterSet();
+ other.encounters = encountersHere;
+ other.displayName = mapName + " " + Gen4Constants.hgssNonGrassSetNames[i];
+ other.rate = rates[i];
+ encounters.add(other);
+ }
+ }
+
+ // Swarms
+ EncounterSet swarms = readOptionalEncountersHGSS(b, offset, 2);
+ swarms.displayName = mapName + " Swarms";
+ if (swarms.encounters.size() > 0) {
+ encounters.add(swarms);
+ }
+ EncounterSet nightFishingReplacement = readOptionalEncountersHGSS(b, offset + 4, 1);
+ nightFishingReplacement.displayName = mapName + " Night Fishing Replacement";
+ if (nightFishingReplacement.encounters.size() > 0) {
+ encounters.add(nightFishingReplacement);
+ }
+ EncounterSet fishingSwarms = readOptionalEncountersHGSS(b, offset + 6, 1);
+ fishingSwarms.displayName = mapName + " Fishing Swarm";
+ if (fishingSwarms.encounters.size() > 0) {
+ encounters.add(fishingSwarms);
+ }
+ }
+
+ // Headbutt Encounters
+ String headbuttEncountersFile = romEntry.getFile("HeadbuttPokemon");
+ NARCArchive headbuttEncounterData = readNARC(headbuttEncountersFile);
+ c = -1;
+ for (byte[] b : headbuttEncounterData.files) {
+ c++;
+
+ // Each headbutt encounter file starts with four bytes, which I believe are used
+ // to indicate the number of "normal" and "special" trees that are available in
+ // this area. For areas that don't contain any headbutt encounters, these four
+ // bytes constitute the only four bytes in the file, so we can stop looking at
+ // this file in this case.
+ if (b.length == 4) {
+ continue;
+ }
+
+ String mapName = headbuttMapNames.get(c);
+ EncounterSet headbuttEncounters = readHeadbuttEncountersHGSS(b, 4, 18);
+ headbuttEncounters.displayName = mapName + " Headbutt";
+
+ // Map 24 is an unused version of Route 16, but it still has valid headbutt encounter data.
+ // Avoid adding it to the list of encounters to prevent confusion.
+ if (headbuttEncounters.encounters.size() > 0 && c != 24) {
+ encounters.add(headbuttEncounters);
+ }
+ }
+
+ // Bug Catching Contest Encounters
+ String bccEncountersFile = romEntry.getFile("BCCWilds");
+ byte[] bccEncountersData = readFile(bccEncountersFile);
+ EncounterSet bccEncountersPreNationalDex = readBCCEncountersHGSS(bccEncountersData, 0, 10);
+ bccEncountersPreNationalDex.displayName = "Bug Catching Contest (Pre-National Dex)";
+ if (bccEncountersPreNationalDex.encounters.size() > 0) {
+ encounters.add(bccEncountersPreNationalDex);
+ }
+ EncounterSet bccEncountersPostNationalDexTues = readBCCEncountersHGSS(bccEncountersData, 80, 10);
+ bccEncountersPostNationalDexTues.displayName = "Bug Catching Contest (Post-National Dex, Tuesdays)";
+ if (bccEncountersPostNationalDexTues.encounters.size() > 0) {
+ encounters.add(bccEncountersPostNationalDexTues);
+ }
+ EncounterSet bccEncountersPostNationalDexThurs = readBCCEncountersHGSS(bccEncountersData, 160, 10);
+ bccEncountersPostNationalDexThurs.displayName = "Bug Catching Contest (Post-National Dex, Thursdays)";
+ if (bccEncountersPostNationalDexThurs.encounters.size() > 0) {
+ encounters.add(bccEncountersPostNationalDexThurs);
+ }
+ EncounterSet bccEncountersPostNationalDexSat = readBCCEncountersHGSS(bccEncountersData, 240, 10);
+ bccEncountersPostNationalDexSat.displayName = "Bug Catching Contest (Post-National Dex, Saturdays)";
+ if (bccEncountersPostNationalDexSat.encounters.size() > 0) {
+ encounters.add(bccEncountersPostNationalDexSat);
+ }
+ return encounters;
+ }
+
+ private EncounterSet readOptionalEncountersHGSS(byte[] data, int offset, int amount) {
+ EncounterSet es = new EncounterSet();
+ es.rate = 1;
+ for (int i = 0; i < amount; i++) {
+ int pokemon = readWord(data, offset + i * 2);
+ if (pokemon != 0) {
+ Encounter e = new Encounter();
+ e.level = 1;
+ e.pokemon = pokes[pokemon];
+ es.encounters.add(e);
+ }
+ }
+ return es;
+ }
+
+ private Pokemon[] readPokemonHGSS(byte[] data, int offset, int amount) {
+ Pokemon[] pokesHere = new Pokemon[amount];
+ for (int i = 0; i < amount; i++) {
+ pokesHere[i] = pokes[readWord(data, offset + i * 2)];
+ }
+ return pokesHere;
+ }
+
+ private List<Encounter> readSeaEncountersHGSS(byte[] data, int offset, int amount) {
+ List<Encounter> encounters = new ArrayList<>();
+ for (int i = 0; i < amount; i++) {
+ int level = readWord(data, offset + i * 4);
+ int pokemon = readWord(data, offset + 2 + i * 4);
+ Encounter enc = new Encounter();
+ enc.level = level & 0xFF;
+ enc.maxLevel = level >> 8;
+ enc.pokemon = pokes[pokemon];
+ encounters.add(enc);
+ }
+ return encounters;
+ }
+
+ private EncounterSet readHeadbuttEncountersHGSS(byte[] data, int offset, int amount) {
+ EncounterSet es = new EncounterSet();
+ es.rate = 1;
+ for (int i = 0; i < amount; i++) {
+ int pokemon = readWord(data, offset + i * 4);
+ if (pokemon != 0) {
+ Encounter enc = new Encounter();
+ enc.level = data[offset + 2 + i * 4];
+ enc.maxLevel = data[offset + 3 + i * 4];
+ enc.pokemon = pokes[pokemon];
+ es.encounters.add(enc);
+ }
+ }
+ return es;
+ }
+
+ private EncounterSet readBCCEncountersHGSS(byte[] data, int offset, int amount) {
+ EncounterSet es = new EncounterSet();
+ es.rate = 1;
+ for (int i = 0; i < amount; i++) {
+ int pokemon = readWord(data, offset + i * 8);
+ if (pokemon != 0) {
+ Encounter enc = new Encounter();
+ enc.level = data[offset + 2 + i * 8];
+ enc.maxLevel = data[offset + 3 + i * 8];
+ enc.pokemon = pokes[pokemon];
+ es.encounters.add(enc);
+ }
+ }
+ return es;
+ }
+
+ private List<EncounterSet> readTimeBasedRodEncountersHGSS(byte[] data, int offset, Pokemon replacement, int replacementIndex) {
+ List<EncounterSet> encounters = new ArrayList<>();
+ List<Encounter> rodMorningDayEncounters = readSeaEncountersHGSS(data, offset, 5);
+ EncounterSet rodMorningDay = new EncounterSet();
+ rodMorningDay.encounters = rodMorningDayEncounters;
+ encounters.add(rodMorningDay);
+
+ List<Encounter> rodNightEncounters = new ArrayList<>(rodMorningDayEncounters);
+ Encounter replacedEncounter = cloneEncounterAndReplacePokemon(rodMorningDayEncounters.get(replacementIndex), replacement);
+ rodNightEncounters.set(replacementIndex, replacedEncounter);
+ EncounterSet rodNight = new EncounterSet();
+ rodNight.encounters = rodNightEncounters;
+ encounters.add(rodNight);
+ return encounters;
+ }
+
+ private Encounter cloneEncounterAndReplacePokemon(Encounter enc, Pokemon pkmn) {
+ Encounter clone = new Encounter();
+ clone.level = enc.level;
+ clone.maxLevel = enc.maxLevel;
+ clone.pokemon = pkmn;
+ return clone;
+ }
+
+ @Override
+ public void setEncounters(boolean useTimeOfDay, List<EncounterSet> encounters) {
+ try {
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ setEncountersHGSS(useTimeOfDay, encounters);
+ updatePokedexAreaDataHGSS(encounters);
+ } else {
+ setEncountersDPPt(useTimeOfDay, encounters);
+ updatePokedexAreaDataDPPt(encounters);
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ }
+
+ private void setEncountersDPPt(boolean useTimeOfDay, List<EncounterSet> encounterList) throws IOException {
+ // Determine file to use
+ String encountersFile = romEntry.getFile("WildPokemon");
+ NARCArchive encounterData = readNARC(encountersFile);
+ Iterator<EncounterSet> encounters = encounterList.iterator();
+ // Credit for
+ // https://github.com/magical/pokemon-encounters/blob/master/nds/encounters-gen4-sinnoh.py
+ // for the structure for this.
+ for (byte[] b : encounterData.files) {
+ int grassRate = readLong(b, 0);
+ if (grassRate != 0) {
+ // grass encounters are a-go
+ EncounterSet grass = encounters.next();
+ writeEncountersDPPt(b, 4, grass.encounters, 12);
+
+ // Time of day encounters?
+ int todEncounterSlot = 12;
+ for (int i = 0; i < 4; i++) {
+ int pknum = readLong(b, 108 + 4 * i);
+ if (pknum >= 1 && pknum <= Gen4Constants.pokemonCount) {
+ // Valid time of day slot
+ if (useTimeOfDay) {
+ // Get custom randomized encounter
+ Pokemon pk = grass.encounters.get(todEncounterSlot++).pokemon;
+ writeLong(b, 108 + 4 * i, pk.number);
+ } else {
+ // Copy the original slot's randomized encounter
+ Pokemon pk = grass.encounters.get(Gen4Constants.dpptAlternateSlots[i + 2]).pokemon;
+ writeLong(b, 108 + 4 * i, pk.number);
+ }
+ }
+ }
+
+ // Other conditional encounters?
+ Iterator<Encounter> condEncounters = null;
+ for (int i = 0; i < 20; i++) {
+ if (i >= 2 && i <= 5) {
+ // Time of day slot, handled already
+ continue;
+ }
+ int offs = 100 + i * 4 + (i >= 10 ? 24 : 0);
+ int pknum = readLong(b, offs);
+ if (pknum >= 1 && pknum <= Gen4Constants.pokemonCount) {
+ // This slot is used, grab a replacement.
+ if (condEncounters == null) {
+ // Fetch the set of conditional encounters for this
+ // area now that we know it's necessary and exists.
+ condEncounters = encounters.next().encounters.iterator();
+ }
+ Pokemon pk = condEncounters.next().pokemon;
+ writeLong(b, offs, pk.number);
+ }
+ }
+ }
+ // up to 204, 5 special ones to go
+ // This is for surf, filler, old rod, good rod, super rod
+ // so we skip index 1 (filler)
+ int offset = 204;
+ for (int i = 0; i < 5; i++) {
+ int rate = readLong(b, offset);
+ offset += 4;
+ if (rate == 0 || i == 1) {
+ offset += 40;
+ continue;
+ }
+
+ EncounterSet other = encounters.next();
+ writeSeaEncountersDPPt(b, offset, other.encounters);
+ offset += 40;
+ }
+ }
+
+ // Save
+ writeNARC(encountersFile, encounterData);
+
+ // Now do the extra encounters (Feebas tiles, honey trees, Great Marsh rotating Pokemon, etc.)
+ String extraEncountersFile = romEntry.getFile("ExtraEncounters");
+ NARCArchive extraEncounterData = readNARC(extraEncountersFile);
+
+ // Feebas tiles
+ byte[] feebasData = extraEncounterData.files.get(0);
+ EncounterSet feebasEncounters = encounters.next();
+ byte[] encounterOverlay = readOverlay(romEntry.getInt("EncounterOvlNumber"));
+ int offset = find(encounterOverlay, Gen4Constants.feebasLevelPrefixDPPt);
+ if (offset > 0) {
+ offset += Gen4Constants.feebasLevelPrefixDPPt.length() / 2; // because it was a prefix
+ encounterOverlay[offset] = (byte) feebasEncounters.encounters.get(0).maxLevel;
+ encounterOverlay[offset + 4] = (byte) feebasEncounters.encounters.get(0).level;
+ }
+ writeExtraEncountersDPPt(feebasData, 0, feebasEncounters.encounters);
+
+ // Honey trees
+ int[] honeyTreeOffsets = romEntry.arrayEntries.get("HoneyTreeOffsets");
+ for (int i = 0; i < honeyTreeOffsets.length; i++) {
+ byte[] honeyTreeData = extraEncounterData.files.get(honeyTreeOffsets[i]);
+ EncounterSet honeyTreeEncounters = encounters.next();
+ offset = find(encounterOverlay, Gen4Constants.honeyTreeLevelPrefixDPPt);
+ if (offset > 0) {
+ offset += Gen4Constants.honeyTreeLevelPrefixDPPt.length() / 2; // because it was a prefix
+ int level = honeyTreeEncounters.encounters.get(0).level;
+ int maxLevel = honeyTreeEncounters.encounters.get(0).maxLevel;
+
+ // The original code makes it impossible for certain min levels
+ // from being used in the assembly, but there's also a hardcoded
+ // check for the original level range that we don't want. So we
+ // can use that space to just do "mov r0, level", nop out the rest
+ // of the check, then change "mov r0, r6, #5" to "mov r0, r0, r6".
+ encounterOverlay[offset + 46] = (byte) level;
+ encounterOverlay[offset + 47] = 0x20;
+ encounterOverlay[offset + 48] = 0x00;
+ encounterOverlay[offset + 49] = 0x00;
+ encounterOverlay[offset + 50] = 0x00;
+ encounterOverlay[offset + 51] = 0x00;
+ encounterOverlay[offset + 52] = 0x00;
+ encounterOverlay[offset + 53] = 0x00;
+ encounterOverlay[offset + 54] = (byte) 0x80;
+ encounterOverlay[offset + 55] = 0x19;
+
+ encounterOverlay[offset + 102] = (byte) maxLevel;
+
+ // In the above comment, r6 is a random number between 0 and
+ // (maxLevel - level). To calculate this number, the game rolls
+ // a random number between 0 and 0xFFFF and then divides it by
+ // 0x1746; this produces values between 0 and 10, the original
+ // level range. We need to replace the 0x1746 with our own
+ // constant that has the same effect.
+ int newRange = maxLevel - level;
+ int divisor = (0xFFFF / (newRange + 1)) + 1;
+ FileFunctions.writeFullInt(encounterOverlay, offset + 148, divisor);
+ }
+ writeExtraEncountersDPPt(honeyTreeData, 0, honeyTreeEncounters.encounters);
+ }
+
+ // Trophy Garden rotating Pokemon (Mr. Backlot)
+ byte[] trophyGardenData = extraEncounterData.files.get(8);
+ EncounterSet trophyGardenEncounters = encounters.next();
+
+ // The game will softlock if all the Pokemon here are the same species. As an
+ // emergency mitigation, just randomly pick a different species in case this
+ // happens. This is very unlikely to happen in practice, even with very
+ // restrictive settings, so it should be okay that we're breaking logic here.
+ while (trophyGardenEncounters.encounters.stream().distinct().count() == 1) {
+ trophyGardenEncounters.encounters.get(0).pokemon = randomPokemon();
+ }
+ writeExtraEncountersDPPt(trophyGardenData, 0, trophyGardenEncounters.encounters);
+
+ // Great Marsh rotating Pokemon
+ int[] greatMarshOffsets = new int[]{9, 10};
+ for (int i = 0; i < greatMarshOffsets.length; i++) {
+ byte[] greatMarshData = extraEncounterData.files.get(greatMarshOffsets[i]);
+ EncounterSet greatMarshEncounters = encounters.next();
+ writeExtraEncountersDPPt(greatMarshData, 0, greatMarshEncounters.encounters);
+ }
+
+ // Save
+ writeOverlay(romEntry.getInt("EncounterOvlNumber"), encounterOverlay);
+ writeNARC(extraEncountersFile, extraEncounterData);
+
+ }
+
+ private void writeEncountersDPPt(byte[] data, int offset, List<Encounter> encounters, int enclength) {
+ for (int i = 0; i < enclength; i++) {
+ Encounter enc = encounters.get(i);
+ writeLong(data, offset + i * 8, enc.level);
+ writeLong(data, offset + i * 8 + 4, enc.pokemon.number);
+ }
+ }
+
+ private void writeSeaEncountersDPPt(byte[] data, int offset, List<Encounter> encounters) {
+ int enclength = encounters.size();
+ for (int i = 0; i < enclength; i++) {
+ Encounter enc = encounters.get(i);
+ writeLong(data, offset + i * 8, (enc.level << 8) + enc.maxLevel);
+ writeLong(data, offset + i * 8 + 4, enc.pokemon.number);
+ }
+ }
+
+ private void writeExtraEncountersDPPt(byte[] data, int offset, List<Encounter> encounters) {
+ int enclength = encounters.size();
+ for (int i = 0; i < enclength; i++) {
+ Encounter enc = encounters.get(i);
+ writeLong(data, offset + i * 4, enc.pokemon.number);
+ }
+ }
+
+ private void setEncountersHGSS(boolean useTimeOfDay, List<EncounterSet> encounterList) throws IOException {
+ String encountersFile = romEntry.getFile("WildPokemon");
+ NARCArchive encounterData = readNARC(encountersFile);
+ Iterator<EncounterSet> encounters = encounterList.iterator();
+ // Credit for
+ // https://github.com/magical/pokemon-encounters/blob/master/nds/encounters-gen4-johto.py
+ // for the structure for this.
+ int[] amounts = new int[] { 0, 5, 2, 5, 5, 5 };
+ for (byte[] b : encounterData.files) {
+ int[] rates = new int[6];
+ rates[0] = b[0] & 0xFF;
+ rates[1] = b[1] & 0xFF;
+ rates[2] = b[2] & 0xFF;
+ rates[3] = b[3] & 0xFF;
+ rates[4] = b[4] & 0xFF;
+ rates[5] = b[5] & 0xFF;
+ // Up to 20 after the rates & levels
+ // Grass has to be handled on its own because the levels
+ // are reused for every time of day
+ if (rates[0] != 0) {
+ if (!useTimeOfDay) {
+ // Get a single set of encounters...
+ // Write the encounters we get 3x for morning, day, night
+ EncounterSet grass = encounters.next();
+ writeGrassEncounterLevelsHGSS(b, 8, grass.encounters);
+ writePokemonHGSS(b, 20, grass.encounters);
+ writePokemonHGSS(b, 44, grass.encounters);
+ writePokemonHGSS(b, 68, grass.encounters);
+ } else {
+ EncounterSet grass = encounters.next();
+ writeGrassEncounterLevelsHGSS(b, 8, grass.encounters);
+ writePokemonHGSS(b, 20, grass.encounters);
+ for (int i = 1; i < 3; i++) {
+ grass = encounters.next();
+ writePokemonHGSS(b, 20 + i * 24, grass.encounters);
+ }
+ }
+ }
+
+ // Write radio pokemon
+ writeOptionalEncountersHGSS(b, 92, 4, encounters);
+
+ // Up to 100 now... 2*2*2 for radio pokemon
+ // Write surf, rock smash, and rods
+ int offset = 100;
+ for (int i = 1; i < 6; i++) {
+ if (rates[i] != 0) {
+ // Valid area.
+ EncounterSet other = encounters.next();
+ writeSeaEncountersHGSS(b, offset, other.encounters);
+ }
+ offset += 4 * amounts[i];
+ }
+
+ // Write swarm pokemon
+ writeOptionalEncountersHGSS(b, offset, 2, encounters);
+ writeOptionalEncountersHGSS(b, offset + 4, 1, encounters);
+ writeOptionalEncountersHGSS(b, offset + 6, 1, encounters);
+ }
+
+ // Save
+ writeNARC(encountersFile, encounterData);
+
+ // Write Headbutt encounters
+ String headbuttEncountersFile = romEntry.getFile("HeadbuttPokemon");
+ NARCArchive headbuttEncounterData = readNARC(headbuttEncountersFile);
+ int c = -1;
+ for (byte[] b : headbuttEncounterData.files) {
+ c++;
+
+ // In getEncountersHGSS, we ignored maps with no headbutt encounter data,
+ // and we also ignored map 24 for being unused. We need to ignore them
+ // here as well to keep encounters.next() in sync with the correct file.
+ if (b.length == 4 || c == 24) {
+ continue;
+ }
+
+ EncounterSet headbutt = encounters.next();
+ writeHeadbuttEncountersHGSS(b, 4, headbutt.encounters);
+ }
+
+ // Save
+ writeNARC(headbuttEncountersFile, headbuttEncounterData);
+
+ // Write Bug Catching Contest encounters
+ String bccEncountersFile = romEntry.getFile("BCCWilds");
+ byte[] bccEncountersData = readFile(bccEncountersFile);
+ EncounterSet bccEncountersPreNationalDex = encounters.next();
+ writeBCCEncountersHGSS(bccEncountersData, 0, bccEncountersPreNationalDex.encounters);
+ EncounterSet bccEncountersPostNationalDexTues = encounters.next();
+ writeBCCEncountersHGSS(bccEncountersData, 80, bccEncountersPostNationalDexTues.encounters);
+ EncounterSet bccEncountersPostNationalDexThurs = encounters.next();
+ writeBCCEncountersHGSS(bccEncountersData, 160, bccEncountersPostNationalDexThurs.encounters);
+ EncounterSet bccEncountersPostNationalDexSat = encounters.next();
+ writeBCCEncountersHGSS(bccEncountersData, 240, bccEncountersPostNationalDexSat.encounters);
+
+ // Save
+ writeFile(bccEncountersFile, bccEncountersData);
+ }
+
+ private void writeOptionalEncountersHGSS(byte[] data, int offset, int amount, Iterator<EncounterSet> encounters) {
+ Iterator<Encounter> eIter = null;
+ for (int i = 0; i < amount; i++) {
+ int origPokemon = readWord(data, offset + i * 2);
+ if (origPokemon != 0) {
+ // Need an encounter set, yes.
+ if (eIter == null) {
+ eIter = encounters.next().encounters.iterator();
+ }
+ Encounter here = eIter.next();
+ writeWord(data, offset + i * 2, here.pokemon.number);
+ }
+ }
+
+ }
+
+ private void writeGrassEncounterLevelsHGSS(byte[] data, int offset, List<Encounter> encounters) {
+ int enclength = encounters.size();
+ for (int i = 0; i < enclength; i++) {
+ data[offset + i] = (byte) encounters.get(i).level;
+ }
+
+ }
+
+ private void writePokemonHGSS(byte[] data, int offset, List<Encounter> encounters) {
+ int enclength = encounters.size();
+ for (int i = 0; i < enclength; i++) {
+ writeWord(data, offset + i * 2, encounters.get(i).pokemon.number);
+ }
+
+ }
+
+ private void writeSeaEncountersHGSS(byte[] data, int offset, List<Encounter> encounters) {
+ int enclength = encounters.size();
+ for (int i = 0; i < enclength; i++) {
+ Encounter enc = encounters.get(i);
+ data[offset + i * 4] = (byte) enc.level;
+ data[offset + i * 4 + 1] = (byte) enc.maxLevel;
+ writeWord(data, offset + i * 4 + 2, enc.pokemon.number);
+ }
+
+ }
+
+ private void writeHeadbuttEncountersHGSS(byte[] data, int offset, List<Encounter> encounters) {
+ int enclength = encounters.size();
+ for (int i = 0; i < enclength; i++) {
+ Encounter enc = encounters.get(i);
+ writeWord(data, offset + i * 4, enc.pokemon.number);
+ data[offset + 2 + i * 4] = (byte) enc.level;
+ data[offset + 3 + i * 4] = (byte) enc.maxLevel;
+ }
+ }
+
+ private void writeBCCEncountersHGSS(byte[] data, int offset, List<Encounter> encounters) {
+ int enclength = encounters.size();
+ for (int i = 0; i < enclength; i++) {
+ Encounter enc = encounters.get(i);
+ writeWord(data, offset + i * 8, enc.pokemon.number);
+ data[offset + 2 + i * 8] = (byte) enc.level;
+ data[offset + 3 + i * 8] = (byte) enc.maxLevel;
+ }
+ }
+
+ private List<Encounter> stitchEncsToLevels(Pokemon[] pokemon, int[] levels) {
+ List<Encounter> encounters = new ArrayList<>();
+ for (int i = 0; i < pokemon.length; i++) {
+ Encounter enc = new Encounter();
+ enc.level = levels[i];
+ enc.pokemon = pokemon[i];
+ encounters.add(enc);
+ }
+ return encounters;
+ }
+
+ private void loadWildMapNames() {
+ try {
+ wildMapNames = new HashMap<>();
+ headbuttMapNames = new HashMap<>();
+ byte[] internalNames = this.readFile(romEntry.getFile("MapTableFile"));
+ int numMapHeaders = internalNames.length / 16;
+ int baseMHOffset = romEntry.getInt("MapTableARM9Offset");
+ List<String> allMapNames = getStrings(romEntry.getInt("MapNamesTextOffset"));
+ int mapNameIndexSize = romEntry.getInt("MapTableNameIndexSize");
+ for (int map = 0; map < numMapHeaders; map++) {
+ int baseOffset = baseMHOffset + map * 24;
+ int mapNameIndex = (mapNameIndexSize == 2) ? readWord(arm9, baseOffset + 18)
+ : (arm9[baseOffset + 18] & 0xFF);
+ String mapName = allMapNames.get(mapNameIndex);
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ int wildSet = arm9[baseOffset] & 0xFF;
+ if (wildSet != 255) {
+ wildMapNames.put(wildSet, mapName);
+ }
+ headbuttMapNames.put(map, mapName);
+ } else {
+ int wildSet = readWord(arm9, baseOffset + 14);
+ if (wildSet != 65535) {
+ wildMapNames.put(wildSet, mapName);
+ }
+ }
+ }
+ loadedWildMapNames = true;
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ private void updatePokedexAreaDataDPPt(List<EncounterSet> encounters) throws IOException {
+ String encountersFile = romEntry.getFile("WildPokemon");
+ NARCArchive encounterData = readNARC(encountersFile);
+
+ // Initialize empty area data
+ Set[][] dungeonAreaData = new Set[Gen4Constants.pokemonCount + 1][3];
+ Set[] dungeonSpecialPreNationalData = new Set[Gen4Constants.pokemonCount + 1];
+ Set[] dungeonSpecialPostNationalData = new Set[Gen4Constants.pokemonCount + 1];
+ Set[][] overworldAreaData = new Set[Gen4Constants.pokemonCount + 1][3];
+ Set[] overworldSpecialPreNationalData = new Set[Gen4Constants.pokemonCount + 1];
+ Set[] overworldSpecialPostNationalData = new Set[Gen4Constants.pokemonCount + 1];
+
+ for (int pk = 1; pk <= Gen4Constants.pokemonCount; pk++) {
+ for (int time = 0; time < 3; time++) {
+ dungeonAreaData[pk][time] = new TreeSet<>();
+ overworldAreaData[pk][time] = new TreeSet<>();
+ }
+ dungeonSpecialPreNationalData[pk] = new TreeSet<>();
+ dungeonSpecialPostNationalData[pk] = new TreeSet<>();
+ overworldSpecialPreNationalData[pk] = new TreeSet<>();
+ overworldSpecialPostNationalData[pk] = new TreeSet<>();
+ }
+
+ for (int c = 0; c < encounterData.files.size(); c++) {
+ Set<Integer>[][] target;
+ Set<Integer>[] specialTarget;
+ int index;
+ if (Gen4Constants.dpptOverworldDexMaps[c] != -1) {
+ target = overworldAreaData;
+ specialTarget = overworldSpecialPostNationalData;
+ index = Gen4Constants.dpptOverworldDexMaps[c];
+ } else if (Gen4Constants.dpptDungeonDexMaps[c] != -1) {
+ target = dungeonAreaData;
+ specialTarget = dungeonSpecialPostNationalData;
+ index = Gen4Constants.dpptDungeonDexMaps[c];
+ } else {
+ continue;
+ }
+
+ byte[] b = encounterData.files.get(c);
+
+ int grassRate = readLong(b, 0);
+ if (grassRate != 0) {
+ // up to 4
+ List<Encounter> grassEncounters = readEncountersDPPt(b, 4, 12);
+
+ for (int i = 0; i < 12; i++) {
+ int pknum = grassEncounters.get(i).pokemon.number;
+ if (i == 2 || i == 3) {
+ // morning only - time of day data for day/night for
+ // these slots
+ target[pknum][0].add(index);
+ } else {
+ // all times of day
+ target[pknum][0].add(index);
+ target[pknum][1].add(index);
+ target[pknum][2].add(index);
+ }
+ }
+
+ // time of day data for slots 2 and 3 day/night
+ for (int i = 0; i < 4; i++) {
+ int pknum = readLong(b, 108 + 4 * i);
+ if (pknum >= 1 && pknum <= Gen4Constants.pokemonCount) {
+ target[pknum][i > 1 ? 2 : 1].add(index);
+ }
+ }
+
+ // For Swarm/Radar/GBA encounters, only Poke Radar encounters appear in the dex
+ for (int i = 6; i < 10; i++) {
+ int offs = 100 + i * 4;
+ int pknum = readLong(b, offs);
+ if (pknum >= 1 && pknum <= Gen4Constants.pokemonCount) {
+ specialTarget[pknum].add(index);
+ }
+ }
+ }
+
+ // up to 204, 5 sets of "sea" encounters to go
+ int offset = 204;
+ for (int i = 0; i < 5; i++) {
+ int rate = readLong(b, offset);
+ offset += 4;
+ List<Encounter> encountersHere = readSeaEncountersDPPt(b, offset, 5);
+ offset += 40;
+ if (rate == 0 || i == 1) {
+ continue;
+ }
+ for (Encounter enc : encountersHere) {
+ target[enc.pokemon.number][0].add(index);
+ target[enc.pokemon.number][1].add(index);
+ target[enc.pokemon.number][2].add(index);
+ }
+ }
+ }
+
+ // Handle the "special" encounters that aren't in the encounter GARC
+ for (EncounterSet es : encounters) {
+ if (es.displayName.contains("Mt. Coronet Feebas Tiles")) {
+ for (Encounter enc : es.encounters) {
+ dungeonSpecialPreNationalData[enc.pokemon.number].add(Gen4Constants.dpptMtCoronetDexIndex);
+ dungeonSpecialPostNationalData[enc.pokemon.number].add(Gen4Constants.dpptMtCoronetDexIndex);
+ }
+ } else if (es.displayName.contains("Honey Tree Group 1") || es.displayName.contains("Honey Tree Group 2")) {
+ for (Encounter enc : es.encounters) {
+ dungeonSpecialPreNationalData[enc.pokemon.number].add(Gen4Constants.dpptFloaromaMeadowDexIndex);
+ dungeonSpecialPostNationalData[enc.pokemon.number].add(Gen4Constants.dpptFloaromaMeadowDexIndex);
+ overworldSpecialPreNationalData[enc.pokemon.number].addAll(Gen4Constants.dpptOverworldHoneyTreeDexIndicies);
+ overworldSpecialPostNationalData[enc.pokemon.number].addAll(Gen4Constants.dpptOverworldHoneyTreeDexIndicies);
+ }
+ } else if (es.displayName.contains("Trophy Garden Rotating Pokemon")) {
+ for (Encounter enc : es.encounters) {
+ dungeonSpecialPostNationalData[enc.pokemon.number].add(Gen4Constants.dpptTrophyGardenDexIndex);
+ }
+ } else if (es.displayName.contains("Great Marsh Rotating Pokemon (Post-National Dex)")) {
+ for (Encounter enc : es.encounters) {
+ dungeonSpecialPostNationalData[enc.pokemon.number].add(Gen4Constants.dpptGreatMarshDexIndex);
+ }
+ } else if (es.displayName.contains("Great Marsh Rotating Pokemon (Pre-National Dex)")) {
+ for (Encounter enc : es.encounters) {
+ dungeonSpecialPreNationalData[enc.pokemon.number].add(Gen4Constants.dpptGreatMarshDexIndex);
+ }
+ }
+ }
+
+ // Write new area data to its file
+ // Area data format credit to Ganix
+ String pokedexAreaDataFile = romEntry.getFile("PokedexAreaData");
+ NARCArchive pokedexAreaData = readNARC(pokedexAreaDataFile);
+ int dungeonDataIndex = romEntry.getInt("PokedexAreaDataDungeonIndex");
+ int dungeonSpecialPreNationalDataIndex = romEntry.getInt("PokedexAreaDataDungeonSpecialPreNationalIndex");
+ int dungeonSpecialPostNationalDataIndex = romEntry.getInt("PokedexAreaDataDungeonSpecialPostNationalIndex");
+ int overworldDataIndex = romEntry.getInt("PokedexAreaDataOverworldIndex");
+ int overworldSpecialPreNationalDataIndex = romEntry.getInt("PokedexAreaDataOverworldSpecialPreNationalIndex");
+ int overworldSpecialPostNationalDataIndex = romEntry.getInt("PokedexAreaDataOverworldSpecialPostNationalIndex");
+ for (int pk = 1; pk <= Gen4Constants.pokemonCount; pk++) {
+ for (int time = 0; time < 3; time++) {
+ pokedexAreaData.files.set(dungeonDataIndex + pk + time * Gen4Constants.pokedexAreaDataSize,
+ makePokedexAreaDataFile(dungeonAreaData[pk][time]));
+ pokedexAreaData.files.set(overworldDataIndex + pk + time * Gen4Constants.pokedexAreaDataSize,
+ makePokedexAreaDataFile(overworldAreaData[pk][time]));
+ }
+ pokedexAreaData.files.set(dungeonSpecialPreNationalDataIndex + pk,
+ makePokedexAreaDataFile(dungeonSpecialPreNationalData[pk]));
+ pokedexAreaData.files.set(dungeonSpecialPostNationalDataIndex + pk,
+ makePokedexAreaDataFile(dungeonSpecialPostNationalData[pk]));
+ pokedexAreaData.files.set(overworldSpecialPreNationalDataIndex + pk,
+ makePokedexAreaDataFile(overworldSpecialPreNationalData[pk]));
+ pokedexAreaData.files.set(overworldSpecialPostNationalDataIndex + pk,
+ makePokedexAreaDataFile(overworldSpecialPostNationalData[pk]));
+ }
+ writeNARC(pokedexAreaDataFile, pokedexAreaData);
+ }
+
+ private void updatePokedexAreaDataHGSS(List<EncounterSet> encounters) throws IOException {
+ String encountersFile = romEntry.getFile("WildPokemon");
+ NARCArchive encounterData = readNARC(encountersFile);
+
+ // Initialize empty area data
+ Set[][] dungeonAreaData = new Set[Gen4Constants.pokemonCount + 1][3];
+ Set[][] overworldAreaData = new Set[Gen4Constants.pokemonCount + 1][3];
+ Set[] dungeonSpecialData = new Set[Gen4Constants.pokemonCount + 1];
+ Set[] overworldSpecialData = new Set[Gen4Constants.pokemonCount + 1];
+
+ for (int pk = 1; pk <= Gen4Constants.pokemonCount; pk++) {
+ for (int time = 0; time < 3; time++) {
+ dungeonAreaData[pk][time] = new TreeSet<>();
+ overworldAreaData[pk][time] = new TreeSet<>();
+ }
+ dungeonSpecialData[pk] = new TreeSet<>();
+ overworldSpecialData[pk] = new TreeSet<>();
+ }
+
+ for (int c = 0; c < encounterData.files.size(); c++) {
+ Set<Integer>[][] target;
+ Set<Integer>[] specialTarget;
+ int index;
+ if (Gen4Constants.hgssOverworldDexMaps[c] != -1) {
+ target = overworldAreaData;
+ specialTarget = overworldSpecialData;
+ index = Gen4Constants.hgssOverworldDexMaps[c];
+ } else if (Gen4Constants.hgssDungeonDexMaps[c] != -1) {
+ target = dungeonAreaData;
+ specialTarget = dungeonSpecialData;
+ index = Gen4Constants.hgssDungeonDexMaps[c];
+ } else {
+ continue;
+ }
+
+ byte[] b = encounterData.files.get(c);
+ int[] amounts = new int[]{0, 5, 2, 5, 5, 5};
+ int[] rates = new int[6];
+ rates[0] = b[0] & 0xFF;
+ rates[1] = b[1] & 0xFF;
+ rates[2] = b[2] & 0xFF;
+ rates[3] = b[3] & 0xFF;
+ rates[4] = b[4] & 0xFF;
+ rates[5] = b[5] & 0xFF;
+ // Up to 20 now (12 for levels)
+ if (rates[0] != 0) {
+ for (int time = 0; time < 3; time++) {
+ Pokemon[] pokes = readPokemonHGSS(b, 20 + time * 24, 12);
+ for (Pokemon pk : pokes) {
+ target[pk.number][time].add(index);
+ }
+ }
+ }
+
+ // Hoenn/Sinnoh Radio
+ EncounterSet radio = readOptionalEncountersHGSS(b, 92, 4);
+ for (Encounter enc : radio.encounters) {
+ specialTarget[enc.pokemon.number].add(index);
+ }
+
+ // Up to 100 now... 2*2*2 for radio pokemon
+ // Handle surf, rock smash, and old rod
+ int offset = 100;
+ for (int i = 1; i < 4; i++) {
+ List<Encounter> encountersHere = readSeaEncountersHGSS(b, offset, amounts[i]);
+ offset += 4 * amounts[i];
+ if (rates[i] != 0) {
+ // Valid area.
+ for (Encounter enc : encountersHere) {
+ target[enc.pokemon.number][0].add(index);
+ target[enc.pokemon.number][1].add(index);
+ target[enc.pokemon.number][2].add(index);
+ }
+ }
+ }
+
+ // Handle good and super rod, because they can get an encounter slot replaced by the night fishing replacement
+ Pokemon nightFishingReplacement = pokes[readWord(b, 192)];
+ if (rates[4] != 0) {
+ List<EncounterSet> goodRodEncounters =
+ readTimeBasedRodEncountersHGSS(b, offset, nightFishingReplacement, Gen4Constants.hgssGoodRodReplacementIndex);
+ for (Encounter enc : goodRodEncounters.get(0).encounters) {
+ target[enc.pokemon.number][0].add(index);
+ target[enc.pokemon.number][1].add(index);
+ }
+ for (Encounter enc : goodRodEncounters.get(1).encounters) {
+ target[enc.pokemon.number][2].add(index);
+ }
+ }
+ if (rates[5] != 0) {
+ List<EncounterSet> superRodEncounters =
+ readTimeBasedRodEncountersHGSS(b, offset + 20, nightFishingReplacement, Gen4Constants.hgssSuperRodReplacementIndex);
+ for (Encounter enc : superRodEncounters.get(0).encounters) {
+ target[enc.pokemon.number][0].add(index);
+ target[enc.pokemon.number][1].add(index);
+ }
+ for (Encounter enc : superRodEncounters.get(1).encounters) {
+ target[enc.pokemon.number][2].add(index);
+ }
+ }
+ }
+
+ // Handle headbutt encounters too (only doing it like this because reading the encounters from the ROM is really annoying)
+ EncounterSet firstHeadbuttEncounter = encounters.stream().filter(es -> es.displayName.contains("Route 1 Headbutt")).findFirst().orElse(null);
+ int startingHeadbuttOffset = encounters.indexOf(firstHeadbuttEncounter);
+ if (startingHeadbuttOffset != -1) {
+ for (int i = 0; i < Gen4Constants.hgssHeadbuttOverworldDexMaps.length; i++) {
+ EncounterSet es = encounters.get(startingHeadbuttOffset + i);
+ for (Encounter enc : es.encounters) {
+ if (Gen4Constants.hgssHeadbuttOverworldDexMaps[i] != -1) {
+ overworldSpecialData[enc.pokemon.number].add(Gen4Constants.hgssHeadbuttOverworldDexMaps[i]);
+ } else if (Gen4Constants.hgssHeadbuttDungeonDexMaps[i] != -1) {
+ dungeonSpecialData[enc.pokemon.number].add(Gen4Constants.hgssHeadbuttDungeonDexMaps[i]);
+ }
+ }
+ }
+ }
+
+ // Write new area data to its file
+ // Area data format credit to Ganix
+ String pokedexAreaDataFile = romEntry.getFile("PokedexAreaData");
+ NARCArchive pokedexAreaData = readNARC(pokedexAreaDataFile);
+ int dungeonDataIndex = romEntry.getInt("PokedexAreaDataDungeonIndex");
+ int overworldDataIndex = romEntry.getInt("PokedexAreaDataOverworldIndex");
+ int dungeonSpecialIndex = romEntry.getInt("PokedexAreaDataDungeonSpecialIndex");
+ int overworldSpecialDataIndex = romEntry.getInt("PokedexAreaDataOverworldSpecialIndex");
+ for (int pk = 1; pk <= Gen4Constants.pokemonCount; pk++) {
+ for (int time = 0; time < 3; time++) {
+ pokedexAreaData.files.set(dungeonDataIndex + pk + time * Gen4Constants.pokedexAreaDataSize,
+ makePokedexAreaDataFile(dungeonAreaData[pk][time]));
+ pokedexAreaData.files.set(overworldDataIndex + pk + time * Gen4Constants.pokedexAreaDataSize,
+ makePokedexAreaDataFile(overworldAreaData[pk][time]));
+ }
+ pokedexAreaData.files.set(dungeonSpecialIndex + pk, makePokedexAreaDataFile(dungeonSpecialData[pk]));
+ pokedexAreaData.files.set(overworldSpecialDataIndex + pk, makePokedexAreaDataFile(overworldSpecialData[pk]));
+ }
+ writeNARC(pokedexAreaDataFile, pokedexAreaData);
+ }
+
+ private byte[] makePokedexAreaDataFile(Set<Integer> data) {
+ byte[] output = new byte[data.size() * 4 + 4];
+ int idx = 0;
+ for (Integer obj : data) {
+ int areaIndex = obj;
+ this.writeLong(output, idx, areaIndex);
+ idx += 4;
+ }
+ return output;
+ }
+
+ @Override
+ public List<Trainer> getTrainers() {
+ List<Trainer> allTrainers = new ArrayList<>();
+ try {
+ NARCArchive trainers = this.readNARC(romEntry.getFile("TrainerData"));
+ NARCArchive trpokes = this.readNARC(romEntry.getFile("TrainerPokemon"));
+ List<String> tclasses = this.getTrainerClassNames();
+ List<String> tnames = this.getTrainerNames();
+ int trainernum = trainers.files.size();
+ for (int i = 1; i < trainernum; i++) {
+ // Trainer entries are 20 bytes
+ // Team flags; 1 byte; 0x01 = custom moves, 0x02 = held item
+ // Class; 1 byte
+ // 1 byte not used
+ // Number of pokemon in team; 1 byte
+ // Items; 2 bytes each, 4 item slots
+ // AI Flags; 2 byte
+ // 2 bytes not used
+ // Battle Mode; 1 byte; 0 means single, 1 means double.
+ // 3 bytes not used
+ byte[] trainer = trainers.files.get(i);
+ byte[] trpoke = trpokes.files.get(i);
+ Trainer tr = new Trainer();
+ tr.poketype = trainer[0] & 0xFF;
+ tr.trainerclass = trainer[1] & 0xFF;
+ tr.index = i;
+ int numPokes = trainer[3] & 0xFF;
+ int pokeOffs = 0;
+ tr.fullDisplayName = tclasses.get(tr.trainerclass) + " " + tnames.get(i - 1);
+ for (int poke = 0; poke < numPokes; poke++) {
+ // Structure is
+ // IV SB LV LV SP SP FRM FRM
+ // (HI HI)
+ // (M1 M1 M2 M2 M3 M3 M4 M4)
+ // where SB = 0 0 Ab Ab 0 0 G G
+ // IV is a "difficulty" level between 0 and 255 to represent 0 to 31 IVs.
+ // These IVs affect all attributes. For the vanilla games, the
+ // vast majority of trainers have 0 IVs; Elite Four members will
+ // have 30 IVs.
+ // Ab Ab = ability number, 0 for first ability, 2 for second [HGSS only]
+ // G G affect the gender somehow. 0 appears to mean "most common
+ // gender for the species".
+ int difficulty = trpoke[pokeOffs] & 0xFF;
+ int level = trpoke[pokeOffs + 2] & 0xFF;
+ int species = (trpoke[pokeOffs + 4] & 0xFF) + ((trpoke[pokeOffs + 5] & 0x01) << 8);
+ int formnum = (trpoke[pokeOffs + 5] >> 2);
+ TrainerPokemon tpk = new TrainerPokemon();
+ tpk.level = level;
+ tpk.pokemon = pokes[species];
+ tpk.IVs = (difficulty * 31) / 255;
+ int abilitySlot = (trpoke[pokeOffs + 1] >>> 4) & 0xF;
+ if (abilitySlot == 0) {
+ // All Gen 4 games represent the first ability as ability 0.
+ abilitySlot = 1;
+ }
+ tpk.abilitySlot = abilitySlot;
+ tpk.forme = formnum;
+ tpk.formeSuffix = Gen4Constants.getFormeSuffixByBaseForme(species,formnum);
+ pokeOffs += 6;
+ if (tr.pokemonHaveItems()) {
+ tpk.heldItem = readWord(trpoke, pokeOffs);
+ pokeOffs += 2;
+ }
+ if (tr.pokemonHaveCustomMoves()) {
+ for (int move = 0; move < 4; move++) {
+ tpk.moves[move] = readWord(trpoke, pokeOffs + (move*2));
+ }
+ pokeOffs += 8;
+ }
+ // Plat/HGSS have another random pokeOffs +=2 here.
+ if (romEntry.romType != Gen4Constants.Type_DP) {
+ pokeOffs += 2;
+ }
+ tr.pokemon.add(tpk);
+ }
+ allTrainers.add(tr);
+ }
+ if (romEntry.romType == Gen4Constants.Type_DP) {
+ Gen4Constants.tagTrainersDP(allTrainers);
+ Gen4Constants.setMultiBattleStatusDP(allTrainers);
+ } else if (romEntry.romType == Gen4Constants.Type_Plat) {
+ Gen4Constants.tagTrainersPt(allTrainers);
+ Gen4Constants.setMultiBattleStatusPt(allTrainers);
+ } else {
+ Gen4Constants.tagTrainersHGSS(allTrainers);
+ Gen4Constants.setMultiBattleStatusHGSS(allTrainers);
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ return allTrainers;
+ }
+
+ @Override
+ public List<Integer> getMainPlaythroughTrainers() {
+ return new ArrayList<>(); // Not implemented
+ }
+
+ @Override
+ public List<Integer> getEliteFourTrainers(boolean isChallengeMode) {
+ return Arrays.stream(romEntry.arrayEntries.get("EliteFourIndices")).boxed().collect(Collectors.toList());
+ }
+
+ @Override
+ public List<Integer> getEvolutionItems() {
+ return Gen4Constants.evolutionItems;
+ }
+
+ @Override
+ public void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode) {
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ fixAbilitySlotValuesForHGSS(trainerData);
+ }
+ Iterator<Trainer> allTrainers = trainerData.iterator();
+ try {
+ NARCArchive trainers = this.readNARC(romEntry.getFile("TrainerData"));
+ NARCArchive trpokes = new NARCArchive();
+
+ // Get current movesets in case we need to reset them for certain
+ // trainer mons.
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+
+ // empty entry
+ trpokes.files.add(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 });
+ int trainernum = trainers.files.size();
+ for (int i = 1; i < trainernum; i++) {
+ byte[] trainer = trainers.files.get(i);
+ Trainer tr = allTrainers.next();
+ // preserve original poketype
+ trainer[0] = (byte) tr.poketype;
+ int numPokes = tr.pokemon.size();
+ trainer[3] = (byte) numPokes;
+
+ if (doubleBattleMode) {
+ if (!tr.skipImportant()) {
+ // If we set this flag for partner trainers (e.g., Cheryl), then the double wild battles
+ // will turn into trainer battles with glitchy trainers.
+ boolean excludedPartnerTrainer = romEntry.romType != Gen4Constants.Type_HGSS &&
+ Gen4Constants.partnerTrainerIndices.contains(tr.index);
+ if (trainer[16] == 0 && !excludedPartnerTrainer) {
+ trainer[16] |= 3;
+ }
+ }
+ }
+
+ int bytesNeeded = 6 * numPokes;
+ if (romEntry.romType != Gen4Constants.Type_DP) {
+ bytesNeeded += 2 * numPokes;
+ }
+ if (tr.pokemonHaveCustomMoves()) {
+ bytesNeeded += 8 * numPokes; // 2 bytes * 4 moves
+ }
+ if (tr.pokemonHaveItems()) {
+ bytesNeeded += 2 * numPokes;
+ }
+ byte[] trpoke = new byte[bytesNeeded];
+ int pokeOffs = 0;
+ Iterator<TrainerPokemon> tpokes = tr.pokemon.iterator();
+ for (int poke = 0; poke < numPokes; poke++) {
+ TrainerPokemon tp = tpokes.next();
+ int ability = tp.abilitySlot << 4;
+ if (tp.abilitySlot == 1) {
+ // All Gen 4 games represent the first ability as ability 0.
+ ability = 0;
+ }
+ // Add 1 to offset integer division truncation
+ int difficulty = Math.min(255, 1 + (tp.IVs * 255) / 31);
+ writeWord(trpoke, pokeOffs, difficulty | ability << 8);
+ writeWord(trpoke, pokeOffs + 2, tp.level);
+ writeWord(trpoke, pokeOffs + 4, tp.pokemon.number);
+ trpoke[pokeOffs + 5] |= (tp.forme << 2);
+ pokeOffs += 6;
+ if (tr.pokemonHaveItems()) {
+ writeWord(trpoke, pokeOffs, tp.heldItem);
+ pokeOffs += 2;
+ }
+ if (tr.pokemonHaveCustomMoves()) {
+ if (tp.resetMoves) {
+ int[] pokeMoves = RomFunctions.getMovesAtLevel(getAltFormeOfPokemon(tp.pokemon, tp.forme).number, movesets, tp.level);
+ for (int m = 0; m < 4; m++) {
+ writeWord(trpoke, pokeOffs + m * 2, pokeMoves[m]);
+ }
+ } else {
+ writeWord(trpoke, pokeOffs, tp.moves[0]);
+ writeWord(trpoke, pokeOffs + 2, tp.moves[1]);
+ writeWord(trpoke, pokeOffs + 4, tp.moves[2]);
+ writeWord(trpoke, pokeOffs + 6, tp.moves[3]);
+ }
+ pokeOffs += 8;
+ }
+ // Plat/HGSS have another random pokeOffs +=2 here.
+ if (romEntry.romType != Gen4Constants.Type_DP) {
+ pokeOffs += 2;
+ }
+ }
+ trpokes.files.add(trpoke);
+ }
+ this.writeNARC(romEntry.getFile("TrainerData"), trainers);
+ this.writeNARC(romEntry.getFile("TrainerPokemon"), trpokes);
+
+ // In Gen 4, the game prioritizes showing the special double battle intro over almost any
+ // other kind of intro. Since the trainer music is tied to the intro, this results in the
+ // vast majority of "special" trainers losing their intro and music in double battle mode.
+ // To fix this, the below code patches the executable to skip the case for the special
+ // double battle intro (by changing a beq to an unconditional branch); this slightly breaks
+ // battles that are double battles in the original game, but the trade-off is worth it.
+
+ // Then, also patch various subroutines that control the "Trainer Eye" event and text boxes
+ // related to this in order to make double battles work on all trainers
+ if (doubleBattleMode) {
+ String doubleBattleFixPrefix = Gen4Constants.getDoubleBattleFixPrefix(romEntry.romType);
+ int offset = find(arm9, doubleBattleFixPrefix);
+ if (offset > 0) {
+ offset += doubleBattleFixPrefix.length() / 2; // because it was a prefix
+ arm9[offset] = (byte) 0xE0;
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ String doubleBattleFlagReturnPrefix = romEntry.getString("DoubleBattleFlagReturnPrefix");
+ String doubleBattleWalkingPrefix1 = romEntry.getString("DoubleBattleWalkingPrefix1");
+ String doubleBattleWalkingPrefix2 = romEntry.getString("DoubleBattleWalkingPrefix2");
+ String doubleBattleTextBoxPrefix = romEntry.getString("DoubleBattleTextBoxPrefix");
+
+ // After getting the double battle flag, return immediately instead of converting it to a 1 for
+ // non-zero values/0 for zero
+ offset = find(arm9, doubleBattleFlagReturnPrefix);
+ if (offset > 0) {
+ offset += doubleBattleFlagReturnPrefix.length() / 2; // because it was a prefix
+ writeWord(arm9, offset, 0xBD08);
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ // Instead of doing "double trainer walk" for nonzero values, do it only for value == 2
+ offset = find(arm9, doubleBattleWalkingPrefix1);
+ if (offset > 0) {
+ offset += doubleBattleWalkingPrefix1.length() / 2; // because it was a prefix
+ arm9[offset] = (byte) 0x2; // cmp r0, #0x2
+ arm9[offset+3] = (byte) 0xD0; // beq DOUBLE_TRAINER_WALK
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ // Instead of checking if the value was exactly 1 after checking that it was nonzero, check that it's
+ // 2 again lol
+ offset = find(arm9, doubleBattleWalkingPrefix2);
+ if (offset > 0) {
+ offset += doubleBattleWalkingPrefix2.length() / 2; // because it was a prefix
+ arm9[offset] = (byte) 0x2;
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ // Once again, compare a value to 2 instead of just checking that it's nonzero
+ offset = find(arm9, doubleBattleTextBoxPrefix);
+ if (offset > 0) {
+ offset += doubleBattleTextBoxPrefix.length() / 2; // because it was a prefix
+ writeWord(arm9, offset, 0x46C0);
+ writeWord(arm9, offset+2, 0x2802);
+ arm9[offset+5] = (byte) 0xD0;
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ // This NARC has some data that controls how text boxes are handled at the end of a trainer battle.
+ // Changing this byte from 4 -> 0 makes it check if the "double battle" flag is exactly 2 instead of
+ // checking "flag & 2", which makes the single trainer double battles use the single battle
+ // handling (since we set their flag to 3 instead of 2)
+ NARCArchive battleSkillSubSeq = readNARC(romEntry.getFile("BattleSkillSubSeq"));
+ byte[] trainerEndFile = battleSkillSubSeq.files.get(romEntry.getInt("TrainerEndFileNumber"));
+ trainerEndFile[romEntry.getInt("TrainerEndTextBoxOffset")] = 0;
+ writeNARC(romEntry.getFile("BattleSkillSubSeq"), battleSkillSubSeq);
+
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ }
+
+ // Note: This method is here to avoid bloating AbstractRomHandler with special-case logic.
+ // It only works here because nothing in AbstractRomHandler cares about the abilitySlot at
+ // the moment; if that changes, then this should be moved there instead.
+ private void fixAbilitySlotValuesForHGSS(List<Trainer> trainers) {
+ for (Trainer tr : trainers) {
+ if (tr.pokemon.size() > 0) {
+ TrainerPokemon lastPokemon = tr.pokemon.get(tr.pokemon.size() - 1);
+ int lastAbilitySlot = lastPokemon.abilitySlot;
+ for (int i = 0; i < tr.pokemon.size(); i++) {
+ // HGSS has a nasty bug where if a single Pokemon with an abilitySlot of 2
+ // appears on the trainer's team, then all Pokemon that appear after it in
+ // the trpoke data will *also* use their second ability in-game, regardless
+ // of what their abilitySlot is set to. This can mess with the rival's
+ // starter carrying forward their ability, and can also cause sensible items
+ // to behave incorrectly. To fix this, we just make sure everything on a
+ // Trainer's team uses the same abilitySlot. The choice to copy the last
+ // Pokemon's abilitySlot is arbitrary, but allows us to avoid any special-
+ // casing involving the rival's starter, since it always appears last.
+ tr.pokemon.get(i).abilitySlot = lastAbilitySlot;
+ }
+ }
+ }
+ }
+
+ @Override
+ public List<Pokemon> bannedForWildEncounters() {
+ // Ban Unown in DPPt because you can't get certain letters outside of Solaceon Ruins.
+ // Ban Unown in HGSS because they don't show up unless you complete a puzzle in the Ruins of Alph.
+ return new ArrayList<>(Collections.singletonList(pokes[Species.unown]));
+ }
+
+ @Override
+ public List<Pokemon> getBannedFormesForTrainerPokemon() {
+ List<Pokemon> banned = new ArrayList<>();
+ if (romEntry.romType != Gen4Constants.Type_DP) {
+ Pokemon giratinaOrigin = this.getAltFormeOfPokemon(pokes[Species.giratina], 1);
+ if (giratinaOrigin != null) {
+ // Ban Giratina-O for trainers in Gen 4, since he just instantly transforms
+ // back to Altered Forme if he's not holding the Griseous Orb.
+ banned.add(giratinaOrigin);
+ }
+ }
+ return banned;
+ }
+
+ @Override
+ public Map<Integer, List<MoveLearnt>> getMovesLearnt() {
+ Map<Integer, List<MoveLearnt>> movesets = new TreeMap<>();
+ try {
+ NARCArchive movesLearnt = this.readNARC(romEntry.getFile("PokemonMovesets"));
+ int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
+ for (int i = 1; i <= Gen4Constants.pokemonCount + formeCount; i++) {
+ Pokemon pkmn = pokes[i];
+ byte[] rom;
+ if (i > Gen4Constants.pokemonCount) {
+ rom = movesLearnt.files.get(i + Gen4Constants.formeOffset);
+ } else {
+ rom = movesLearnt.files.get(i);
+ }
+ int moveDataLoc = 0;
+ List<MoveLearnt> learnt = new ArrayList<>();
+ while ((rom[moveDataLoc] & 0xFF) != 0xFF || (rom[moveDataLoc + 1] & 0xFF) != 0xFF) {
+ int move = (rom[moveDataLoc] & 0xFF);
+ int level = (rom[moveDataLoc + 1] & 0xFE) >> 1;
+ if ((rom[moveDataLoc + 1] & 0x01) == 0x01) {
+ move += 256;
+ }
+ MoveLearnt ml = new MoveLearnt();
+ ml.level = level;
+ ml.move = move;
+ learnt.add(ml);
+ moveDataLoc += 2;
+ }
+ movesets.put(pkmn.number, learnt);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return movesets;
+ }
+
+ @Override
+ public void setMovesLearnt(Map<Integer, List<MoveLearnt>> movesets) {
+ // int[] extraLearnSets = new int[] { 7, 13, 13 };
+ // Build up a new NARC
+ NARCArchive movesLearnt = new NARCArchive();
+ // The blank moveset
+ byte[] blankSet = new byte[] { (byte) 0xFF, (byte) 0xFF, 0, 0 };
+ movesLearnt.files.add(blankSet);
+ int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
+ for (int i = 1; i <= Gen4Constants.pokemonCount + formeCount; i++) {
+ if (i == Gen4Constants.pokemonCount + 1) {
+ for (int j = 0; j < Gen4Constants.formeOffset; j++) {
+ movesLearnt.files.add(blankSet);
+ }
+ }
+ Pokemon pkmn = pokes[i];
+ List<MoveLearnt> learnt = movesets.get(pkmn.number);
+ int sizeNeeded = learnt.size() * 2 + 2;
+ if ((sizeNeeded % 4) != 0) {
+ sizeNeeded += 2;
+ }
+ byte[] moveset = new byte[sizeNeeded];
+ int j = 0;
+ for (; j < learnt.size(); j++) {
+ MoveLearnt ml = learnt.get(j);
+ moveset[j * 2] = (byte) (ml.move & 0xFF);
+ int levelPart = (ml.level << 1) & 0xFE;
+ if (ml.move > 255) {
+ levelPart++;
+ }
+ moveset[j * 2 + 1] = (byte) levelPart;
+ }
+ moveset[j * 2] = (byte) 0xFF;
+ moveset[j * 2 + 1] = (byte) 0xFF;
+ movesLearnt.files.add(moveset);
+ }
+ //for (int j = 0; j < extraLearnSets[romEntry.romType]; j++) {
+ // movesLearnt.files.add(blankSet);
+ //}
+ // Save
+ try {
+ this.writeNARC(romEntry.getFile("PokemonMovesets"), movesLearnt);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ @Override
+ public Map<Integer, List<Integer>> getEggMoves() {
+ Map<Integer, List<Integer>> eggMoves = new TreeMap<>();
+ try {
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ NARCArchive eggMoveNARC = this.readNARC(romEntry.getFile("EggMoves"));
+ byte[] eggMoveData = eggMoveNARC.files.get(0);
+ eggMoves = readEggMoves(eggMoveData, 0);
+ } else {
+ byte[] fieldOvl = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ int offset = find(fieldOvl, Gen4Constants.dpptEggMoveTablePrefix);
+ if (offset > 0) {
+ offset += Gen4Constants.dpptEggMoveTablePrefix.length() / 2; // because it was a prefix
+ eggMoves = readEggMoves(fieldOvl, offset);
+ }
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ return eggMoves;
+ }
+
+ @Override
+ public void setEggMoves(Map<Integer, List<Integer>> eggMoves) {
+ try {
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ NARCArchive eggMoveNARC = this.readNARC(romEntry.getFile("EggMoves"));
+ byte[] eggMoveData = eggMoveNARC.files.get(0);
+ writeEggMoves(eggMoves, eggMoveData, 0);
+ eggMoveNARC.files.set(0, eggMoveData);
+ this.writeNARC(romEntry.getFile("EggMoves"), eggMoveNARC);
+ } else {
+ byte[] fieldOvl = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ int offset = find(fieldOvl, Gen4Constants.dpptEggMoveTablePrefix);
+ if (offset > 0) {
+ offset += Gen4Constants.dpptEggMoveTablePrefix.length() / 2; // because it was a prefix
+ writeEggMoves(eggMoves, fieldOvl, offset);
+ this.writeOverlay(romEntry.getInt("FieldOvlNumber"), fieldOvl);
+ }
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private Map<Integer, List<Integer>> readEggMoves(byte[] data, int startingOffset) {
+ Map<Integer, List<Integer>> eggMoves = new TreeMap<>();
+ int currentOffset = startingOffset;
+ int currentSpecies = 0;
+ List<Integer> currentMoves = new ArrayList<>();
+ int val = FileFunctions.read2ByteInt(data, currentOffset);
+
+ // Egg move data is stored exactly like in Gen 3, so check egg_moves.h in the
+ // Gen 3 decomps for more info on how this algorithm works.
+ while (val != 0xFFFF) {
+ if (val > 20000) {
+ int species = val - 20000;
+ if (currentMoves.size() > 0) {
+ eggMoves.put(currentSpecies, currentMoves);
+ }
+ currentSpecies = species;
+ currentMoves = new ArrayList<>();
+ } else {
+ currentMoves.add(val);
+ }
+ currentOffset += 2;
+ val = FileFunctions.read2ByteInt(data, currentOffset);
+ }
+
+ // Need to make sure the last entry gets recorded too
+ if (currentMoves.size() > 0) {
+ eggMoves.put(currentSpecies, currentMoves);
+ }
+
+ return eggMoves;
+ }
+
+ private void writeEggMoves(Map<Integer, List<Integer>> eggMoves, byte[] data, int startingOffset) {
+ int currentOffset = startingOffset;
+ for (int species : eggMoves.keySet()) {
+ FileFunctions.write2ByteInt(data, currentOffset, species + 20000);
+ currentOffset += 2;
+ for (int move : eggMoves.get(species)) {
+ FileFunctions.write2ByteInt(data, currentOffset, move);
+ currentOffset += 2;
+ }
+ }
+ }
+
+ private static class ScriptEntry {
+ private int scriptFile;
+ private int scriptOffset;
+
+ public ScriptEntry(int scriptFile, int scriptOffset) {
+ this.scriptFile = scriptFile;
+ this.scriptOffset = scriptOffset;
+ }
+ }
+
+ private static class TextEntry {
+ private int textIndex;
+ private int stringNumber;
+
+ public TextEntry(int textIndex, int stringNumber) {
+ this.textIndex = textIndex;
+ this.stringNumber = stringNumber;
+ }
+ }
+
+ private static class StaticPokemon {
+ protected ScriptEntry[] speciesEntries;
+ protected ScriptEntry[] formeEntries;
+ protected ScriptEntry[] levelEntries;
+
+ public StaticPokemon() {
+ this.speciesEntries = new ScriptEntry[0];
+ this.formeEntries = new ScriptEntry[0];
+ this.levelEntries = new ScriptEntry[0];
+ }
+
+ public Pokemon getPokemon(Gen4RomHandler parent, NARCArchive scriptNARC) {
+ return parent.pokes[parent.readWord(scriptNARC.files.get(speciesEntries[0].scriptFile), speciesEntries[0].scriptOffset)];
+ }
+
+ public void setPokemon(Gen4RomHandler parent, NARCArchive scriptNARC, Pokemon pkmn) {
+ int value = pkmn.number;
+ for (int i = 0; i < speciesEntries.length; i++) {
+ byte[] file = scriptNARC.files.get(speciesEntries[i].scriptFile);
+ parent.writeWord(file, speciesEntries[i].scriptOffset, value);
+ }
+ }
+
+ public int getForme(NARCArchive scriptNARC) {
+ if (formeEntries.length == 0) {
+ return 0;
+ }
+ byte[] file = scriptNARC.files.get(formeEntries[0].scriptFile);
+ return file[formeEntries[0].scriptOffset];
+ }
+
+ public void setForme(NARCArchive scriptNARC, int forme) {
+ for (int i = 0; i < formeEntries.length; i++) {
+ byte[] file = scriptNARC.files.get(formeEntries[i].scriptFile);
+ file[formeEntries[i].scriptOffset] = (byte) forme;
+ }
+ }
+
+ public int getLevelCount() {
+ return levelEntries.length;
+ }
+
+ public int getLevel(NARCArchive scriptNARC, int i) {
+ if (levelEntries.length <= i) {
+ return 1;
+ }
+ byte[] file = scriptNARC.files.get(levelEntries[i].scriptFile);
+ return file[levelEntries[i].scriptOffset];
+ }
+
+ public void setLevel(NARCArchive scriptNARC, int level, int i) {
+ if (levelEntries.length > i) { // Might not have a level entry e.g., it's an egg
+ byte[] file = scriptNARC.files.get(levelEntries[i].scriptFile);
+ file[levelEntries[i].scriptOffset] = (byte) level;
+ }
+ }
+ }
+
+ private static class StaticPokemonGameCorner extends StaticPokemon {
+ private TextEntry[] textEntries;
+
+ public StaticPokemonGameCorner() {
+ super();
+ this.textEntries = new TextEntry[0];
+ }
+
+ @Override
+ public void setPokemon(Gen4RomHandler parent, NARCArchive scriptNARC, Pokemon pkmn) {
+ super.setPokemon(parent, scriptNARC, pkmn);
+ for (TextEntry textEntry : textEntries) {
+ List<String> strings = parent.getStrings(textEntry.textIndex);
+ String originalString = strings.get(textEntry.stringNumber);
+ // For JP, the first thing after the name is "\x0001". For non-JP, it's "\v0203"
+ int postNameIndex = originalString.indexOf("\\");
+ String newString = pkmn.name.toUpperCase() + originalString.substring(postNameIndex);
+ strings.set(textEntry.stringNumber, newString);
+ parent.setStrings(textEntry.textIndex, strings);
+ }
+ }
+ }
+
+ private static class RoamingPokemon {
+ private int[] speciesCodeOffsets;
+ private int[] levelCodeOffsets;
+ private ScriptEntry[] speciesScriptOffsets;
+ private ScriptEntry[] genderOffsets;
+
+ public RoamingPokemon() {
+ this.speciesCodeOffsets = new int[0];
+ this.levelCodeOffsets = new int[0];
+ this.speciesScriptOffsets = new ScriptEntry[0];
+ this.genderOffsets = new ScriptEntry[0];
+ }
+
+ public Pokemon getPokemon(Gen4RomHandler parent) {
+ int species = parent.readWord(parent.arm9, speciesCodeOffsets[0]);
+ return parent.pokes[species];
+ }
+
+ public void setPokemon(Gen4RomHandler parent, NARCArchive scriptNARC, Pokemon pkmn) {
+ int value = pkmn.number;
+ for (int speciesCodeOffset : speciesCodeOffsets) {
+ parent.writeWord(parent.arm9, speciesCodeOffset, value);
+ }
+ for (ScriptEntry speciesScriptOffset : speciesScriptOffsets) {
+ byte[] file = scriptNARC.files.get(speciesScriptOffset.scriptFile);
+ parent.writeWord(file, speciesScriptOffset.scriptOffset, value);
+ }
+ int gender = 0; // male (works for genderless Pokemon too)
+ if (pkmn.genderRatio == 0xFE) {
+ gender = 1; // female
+ }
+ for (ScriptEntry genderOffset : genderOffsets) {
+ byte[] file = scriptNARC.files.get(genderOffset.scriptFile);
+ parent.writeWord(file, genderOffset.scriptOffset, gender);
+ }
+ }
+
+ public int getLevel(Gen4RomHandler parent) {
+ if (levelCodeOffsets.length == 0) {
+ return 1;
+ }
+ return parent.arm9[levelCodeOffsets[0]];
+ }
+
+ public void setLevel(Gen4RomHandler parent, int level) {
+ for (int levelCodeOffset : levelCodeOffsets) {
+ parent.arm9[levelCodeOffset] = (byte) level;
+ }
+ }
+ }
+
+ @Override
+ public List<StaticEncounter> getStaticPokemon() {
+ List<StaticEncounter> sp = new ArrayList<>();
+ if (!romEntry.staticPokemonSupport) {
+ return sp;
+ }
+ try {
+ int[] staticEggOffsets = new int[0];
+ if (romEntry.arrayEntries.containsKey("StaticEggPokemonOffsets")) {
+ staticEggOffsets = romEntry.arrayEntries.get("StaticEggPokemonOffsets");
+ }
+ NARCArchive scriptNARC = scriptNarc;
+ for (int i = 0; i < romEntry.staticPokemon.size(); i++) {
+ int currentOffset = i;
+ StaticPokemon statP = romEntry.staticPokemon.get(i);
+ StaticEncounter se = new StaticEncounter();
+ Pokemon newPK = statP.getPokemon(this, scriptNARC);
+ newPK = getAltFormeOfPokemon(newPK, statP.getForme(scriptNARC));
+ se.pkmn = newPK;
+ se.level = statP.getLevel(scriptNARC, 0);
+ se.isEgg = Arrays.stream(staticEggOffsets).anyMatch(x-> x == currentOffset);
+ for (int levelEntry = 1; levelEntry < statP.getLevelCount(); levelEntry++) {
+ StaticEncounter linkedStatic = new StaticEncounter();
+ linkedStatic.pkmn = newPK;
+ linkedStatic.level = statP.getLevel(scriptNARC, levelEntry);
+ se.linkedEncounters.add(linkedStatic);
+ }
+ sp.add(se);
+ }
+ if (romEntry.arrayEntries.containsKey("StaticPokemonTrades")) {
+ NARCArchive tradeNARC = this.readNARC(romEntry.getFile("InGameTrades"));
+ int[] trades = romEntry.arrayEntries.get("StaticPokemonTrades");
+ int[] scripts = romEntry.arrayEntries.get("StaticPokemonTradeScripts");
+ int[] scriptOffsets = romEntry.arrayEntries.get("StaticPokemonTradeLevelOffsets");
+ for (int i = 0; i < trades.length; i++) {
+ int tradeNum = trades[i];
+ byte[] scriptFile = scriptNARC.files.get(scripts[i]);
+ int level = scriptFile[scriptOffsets[i]];
+ StaticEncounter se = new StaticEncounter(pokes[readLong(tradeNARC.files.get(tradeNum), 0)]);
+ se.level = level;
+ sp.add(se);
+ }
+ }
+ if (romEntry.getInt("MysteryEggOffset") > 0) {
+ byte[] ovOverlay = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ StaticEncounter se = new StaticEncounter(pokes[ovOverlay[romEntry.getInt("MysteryEggOffset")] & 0xFF]);
+ se.isEgg = true;
+ sp.add(se);
+ }
+ if (romEntry.getInt("FossilTableOffset") > 0) {
+ byte[] ftData = arm9;
+ int baseOffset = romEntry.getInt("FossilTableOffset");
+ int fossilLevelScriptNum = romEntry.getInt("FossilLevelScriptNumber");
+ byte[] fossilLevelScript = scriptNARC.files.get(fossilLevelScriptNum);
+ int level = fossilLevelScript[romEntry.getInt("FossilLevelOffset")];
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ ftData = readOverlay(romEntry.getInt("FossilTableOvlNumber"));
+ }
+ // read the 7 Fossil Pokemon
+ for (int f = 0; f < Gen4Constants.fossilCount; f++) {
+ StaticEncounter se = new StaticEncounter(pokes[readWord(ftData, baseOffset + 2 + f * 4)]);
+ se.level = level;
+ sp.add(se);
+ }
+ }
+
+ if (roamerRandomizationEnabled) {
+ getRoamers(sp);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return sp;
+ }
+
+ @Override
+ public boolean setStaticPokemon(List<StaticEncounter> staticPokemon) {
+ if (!romEntry.staticPokemonSupport) {
+ return false;
+ }
+ int sptsize = romEntry.arrayEntries.containsKey("StaticPokemonTrades") ? romEntry.arrayEntries
+ .get("StaticPokemonTrades").length : 0;
+ int meggsize = romEntry.getInt("MysteryEggOffset") > 0 ? 1 : 0;
+ int fossilsize = romEntry.getInt("FossilTableOffset") > 0 ? 7 : 0;
+ if (staticPokemon.size() != romEntry.staticPokemon.size() + sptsize + meggsize + fossilsize + romEntry.roamingPokemon.size()) {
+ return false;
+ }
+ try {
+ Iterator<StaticEncounter> statics = staticPokemon.iterator();
+ NARCArchive scriptNARC = scriptNarc;
+ for (StaticPokemon statP : romEntry.staticPokemon) {
+ StaticEncounter se = statics.next();
+ statP.setPokemon(this, scriptNARC, se.pkmn);
+ statP.setForme(scriptNARC, se.pkmn.formeNumber);
+ statP.setLevel(scriptNARC, se.level, 0);
+ for (int i = 0; i < se.linkedEncounters.size(); i++) {
+ StaticEncounter linkedStatic = se.linkedEncounters.get(i);
+ statP.setLevel(scriptNARC, linkedStatic.level, i + 1);
+ }
+ }
+ if (romEntry.arrayEntries.containsKey("StaticPokemonTrades")) {
+ NARCArchive tradeNARC = this.readNARC(romEntry.getFile("InGameTrades"));
+ int[] trades = romEntry.arrayEntries.get("StaticPokemonTrades");
+ int[] scripts = romEntry.arrayEntries.get("StaticPokemonTradeScripts");
+ int[] scriptOffsets = romEntry.arrayEntries.get("StaticPokemonTradeLevelOffsets");
+ for (int i = 0; i < trades.length; i++) {
+ int tradeNum = trades[i];
+ StaticEncounter se = statics.next();
+ Pokemon thisTrade = se.pkmn;
+ List<Integer> possibleAbilities = new ArrayList<>();
+ possibleAbilities.add(thisTrade.ability1);
+ if (thisTrade.ability2 > 0) {
+ possibleAbilities.add(thisTrade.ability2);
+ }
+ if (thisTrade.ability3 > 0) {
+ possibleAbilities.add(thisTrade.ability3);
+ }
+
+ // Write species and ability
+ writeLong(tradeNARC.files.get(tradeNum), 0, thisTrade.number);
+ writeLong(tradeNARC.files.get(tradeNum), 0x1C,
+ possibleAbilities.get(this.random.nextInt(possibleAbilities.size())));
+
+ // Write level to script file
+ byte[] scriptFile = scriptNARC.files.get(scripts[i]);
+ scriptFile[scriptOffsets[i]] = (byte) se.level;
+
+ // If it's Kenya, write new species name to text file
+ if (i == 1) {
+ Map<String, String> replacements = new TreeMap<>();
+ replacements.put(pokes[Species.spearow].name.toUpperCase(), se.pkmn.name);
+ replaceAllStringsInEntry(romEntry.getInt("KenyaTextOffset"), replacements);
+ }
+ }
+ writeNARC(romEntry.getFile("InGameTrades"), tradeNARC);
+ }
+ if (romEntry.getInt("MysteryEggOffset") > 0) {
+ // Same overlay as MT moves
+ // Truncate the pokemon# to 1byte, unless it's 0
+ int pokenum = statics.next().pkmn.number;
+ if (pokenum > 255) {
+ pokenum = this.random.nextInt(255) + 1;
+ }
+ byte[] ovOverlay = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ ovOverlay[romEntry.getInt("MysteryEggOffset")] = (byte) pokenum;
+ writeOverlay(romEntry.getInt("FieldOvlNumber"), ovOverlay);
+ }
+ if (romEntry.getInt("FossilTableOffset") > 0) {
+ int baseOffset = romEntry.getInt("FossilTableOffset");
+ int fossilLevelScriptNum = romEntry.getInt("FossilLevelScriptNumber");
+ byte[] fossilLevelScript = scriptNARC.files.get(fossilLevelScriptNum);
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ byte[] ftData = readOverlay(romEntry.getInt("FossilTableOvlNumber"));
+ for (int f = 0; f < Gen4Constants.fossilCount; f++) {
+ StaticEncounter se = statics.next();
+ int pokenum = se.pkmn.number;
+ writeWord(ftData, baseOffset + 2 + f * 4, pokenum);
+ fossilLevelScript[romEntry.getInt("FossilLevelOffset")] = (byte) se.level;
+ }
+ writeOverlay(romEntry.getInt("FossilTableOvlNumber"), ftData);
+ } else {
+ // write to arm9
+ for (int f = 0; f < Gen4Constants.fossilCount; f++) {
+ StaticEncounter se = statics.next();
+ int pokenum = se.pkmn.number;
+ writeWord(arm9, baseOffset + 2 + f * 4, pokenum);
+ fossilLevelScript[romEntry.getInt("FossilLevelOffset")] = (byte) se.level;
+ }
+ }
+ }
+ if (roamerRandomizationEnabled) {
+ setRoamers(statics);
+ }
+ if (romEntry.romType == Gen4Constants.Type_Plat) {
+ patchDistortionWorldGroundCheck();
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return true;
+ }
+
+ private void getRoamers(List<StaticEncounter> statics) {
+ if (romEntry.romType == Gen4Constants.Type_DP) {
+ int offset = romEntry.getInt("RoamingPokemonFunctionStartOffset");
+ if (readWord(arm9, offset + 44) != 0) {
+ // In the original code, the code at this offset would be performing a shift to put
+ // Cresselia's constant in r7. After applying the patch, this is now a nop, since
+ // we just pc-relative load it instead. So if a nop isn't here, apply the patch.
+ applyDiamondPearlRoamerPatch();
+ }
+ } else if (romEntry.romType == Gen4Constants.Type_Plat || romEntry.romType == Gen4Constants.Type_HGSS) {
+ int firstSpeciesOffset = romEntry.roamingPokemon.get(0).speciesCodeOffsets[0];
+ if (arm9.length < firstSpeciesOffset || readWord(arm9, firstSpeciesOffset) == 0) {
+ // Either the arm9 hasn't been extended, or the patch hasn't been written
+ int extendBy = romEntry.getInt("Arm9ExtensionSize");
+ arm9 = extendARM9(arm9, extendBy, romEntry.getString("TCMCopyingPrefix"), Gen4Constants.arm9Offset);
+ genericIPSPatch(arm9, "NewRoamerSubroutineTweak");
+ }
+ }
+ for (int i = 0; i < romEntry.roamingPokemon.size(); i++) {
+ RoamingPokemon roamer = romEntry.roamingPokemon.get(i);
+ StaticEncounter se = new StaticEncounter();
+ se.pkmn = roamer.getPokemon(this);
+ se.level = roamer.getLevel(this);
+ statics.add(se);
+ }
+ }
+
+ private void setRoamers(Iterator<StaticEncounter> statics) {
+ for (int i = 0; i < romEntry.roamingPokemon.size(); i++) {
+ RoamingPokemon roamer = romEntry.roamingPokemon.get(i);
+ StaticEncounter roamerEncounter = statics.next();
+ roamer.setPokemon(this, scriptNarc, roamerEncounter.pkmn);
+ roamer.setLevel(this, roamerEncounter.level);
+ }
+ }
+
+ private void applyDiamondPearlRoamerPatch() {
+ int offset = romEntry.getInt("RoamingPokemonFunctionStartOffset");
+
+ // The original code had an entry for Darkrai; its species ID is pc-relative loaded. Since this
+ // entry is clearly unused, just replace Darkrai's species ID constant with Cresselia's, since
+ // in the original code, her ID is computed as 0x7A << 0x2
+ FileFunctions.writeFullInt(arm9, offset + 244, Species.cresselia);
+
+ // Now write a pc-relative load to our new constant over where Cresselia's ID is normally mov'd
+ // into r7 and shifted.
+ arm9[offset + 42] = 0x32;
+ arm9[offset + 43] = 0x4F;
+ arm9[offset + 44] = 0x00;
+ arm9[offset + 45] = 0x00;
+ }
+
+ private void patchDistortionWorldGroundCheck() throws IOException {
+ byte[] fieldOverlay = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ int offset = find(fieldOverlay, Gen4Constants.distortionWorldGroundCheckPrefix);
+ if (offset > 0) {
+ offset += Gen4Constants.distortionWorldGroundCheckPrefix.length() / 2; // because it was a prefix
+
+ // We're now looking at a jump table in the field overlay that determines which intro graphic the game
+ // should display when encountering a Pokemon that does *not* have a special intro. The Giratina fight
+ // in the Distortion World uses ground type 23, and that particular ground type never initializes the
+ // variable that determines which graphic to use. As a result, if Giratina is replaced with a Pokemon
+ // that lacks a special intro, the game will use an uninitialized value for the intro graphic and crash.
+ // The below code simply patches the jump table entry for ground type 23 to take the same branch that
+ // regular grass encounters take, ensuring the intro graphic variable is initialized.
+ fieldOverlay[offset + (2 * 23)] = 0x30;
+ writeOverlay(romEntry.getInt("FieldOvlNumber"), fieldOverlay);
+ }
+ }
+
+ @Override
+ public List<Integer> getTMMoves() {
+ String tmDataPrefix;
+ if (romEntry.romType == Gen4Constants.Type_DP || romEntry.romType == Gen4Constants.Type_Plat) {
+ tmDataPrefix = Gen4Constants.dpptTMDataPrefix;
+ } else {
+ tmDataPrefix = Gen4Constants.hgssTMDataPrefix;
+ }
+ int offset = find(arm9, tmDataPrefix);
+ if (offset > 0) {
+ offset += tmDataPrefix.length() / 2; // because it was a prefix
+ List<Integer> tms = new ArrayList<>();
+ for (int i = 0; i < Gen4Constants.tmCount; i++) {
+ tms.add(readWord(arm9, offset + i * 2));
+ }
+ return tms;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public List<Integer> getHMMoves() {
+ String tmDataPrefix;
+ if (romEntry.romType == Gen4Constants.Type_DP || romEntry.romType == Gen4Constants.Type_Plat) {
+ tmDataPrefix = Gen4Constants.dpptTMDataPrefix;
+ } else {
+ tmDataPrefix = Gen4Constants.hgssTMDataPrefix;
+ }
+ int offset = find(arm9, tmDataPrefix);
+ if (offset > 0) {
+ offset += tmDataPrefix.length() / 2; // because it was a prefix
+ offset += Gen4Constants.tmCount * 2; // TM data
+ List<Integer> hms = new ArrayList<>();
+ for (int i = 0; i < Gen4Constants.hmCount; i++) {
+ hms.add(readWord(arm9, offset + i * 2));
+ }
+ return hms;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void setTMMoves(List<Integer> moveIndexes) {
+ List<Integer> oldMoveIndexes = this.getTMMoves();
+ String tmDataPrefix;
+ if (romEntry.romType == Gen4Constants.Type_DP || romEntry.romType == Gen4Constants.Type_Plat) {
+ tmDataPrefix = Gen4Constants.dpptTMDataPrefix;
+ } else {
+ tmDataPrefix = Gen4Constants.hgssTMDataPrefix;
+ }
+ int offset = find(arm9, tmDataPrefix);
+ if (offset > 0) {
+ offset += tmDataPrefix.length() / 2; // because it was a prefix
+ for (int i = 0; i < Gen4Constants.tmCount; i++) {
+ writeWord(arm9, offset + i * 2, moveIndexes.get(i));
+ }
+
+ // Update TM item descriptions
+ List<String> itemDescriptions = getStrings(romEntry.getInt("ItemDescriptionsTextOffset"));
+ List<String> moveDescriptions = getStrings(romEntry.getInt("MoveDescriptionsTextOffset"));
+ int textCharsPerLine = Gen4Constants.getTextCharsPerLine(romEntry.romType);
+ // TM01 is item 328 and so on
+ for (int i = 0; i < Gen4Constants.tmCount; i++) {
+ // Rewrite 5-line move descs into 3-line item descs
+ itemDescriptions.set(i + Gen4Constants.tmItemOffset, RomFunctions.rewriteDescriptionForNewLineSize(
+ moveDescriptions.get(moveIndexes.get(i)), "\\n", textCharsPerLine, ssd));
+ }
+ // Save the new item descriptions
+ setStrings(romEntry.getInt("ItemDescriptionsTextOffset"), itemDescriptions);
+ // Palettes update
+ String baseOfPalettes = Gen4Constants.pthgssItemPalettesPrefix;
+ if (romEntry.romType == Gen4Constants.Type_DP) {
+ baseOfPalettes = Gen4Constants.dpItemPalettesPrefix;
+ }
+ int offsPals = find(arm9, baseOfPalettes);
+ if (offsPals > 0) {
+ // Write pals
+ for (int i = 0; i < Gen4Constants.tmCount; i++) {
+ Move m = this.moves[moveIndexes.get(i)];
+ int pal = this.typeTMPaletteNumber(m.type);
+ writeWord(arm9, offsPals + i * 8 + 2, pal);
+ }
+ }
+ // if we can't update the palettes, it's not a big deal...
+
+ // Update TM Text
+ for (int i = 0; i < Gen4Constants.tmCount; i++) {
+ int oldMoveIndex = oldMoveIndexes.get(i);
+ int newMoveIndex = moveIndexes.get(i);
+ int tmNumber = i + 1;
+
+ if (romEntry.tmTexts.containsKey(tmNumber)) {
+ List<TextEntry> textEntries = romEntry.tmTexts.get(tmNumber);
+ Set<Integer> textFiles = new HashSet<>();
+ for (TextEntry textEntry : textEntries) {
+ textFiles.add(textEntry.textIndex);
+ }
+ String oldMoveName = moves[oldMoveIndex].name;
+ String newMoveName = moves[newMoveIndex].name;
+ if (romEntry.romType == Gen4Constants.Type_HGSS && oldMoveIndex == Moves.roar) {
+ // It's somewhat dumb to even be bothering with this, but it's too silly not to do
+ oldMoveName = oldMoveName.toUpperCase();
+ newMoveName = newMoveName.toUpperCase();
+ }
+ Map<String, String> replacements = new TreeMap<>();
+ replacements.put(oldMoveName, newMoveName);
+ for (int textFile : textFiles) {
+ replaceAllStringsInEntry(textFile, replacements);
+ }
+ }
+
+ if (romEntry.tmTextsGameCorner.containsKey(tmNumber)) {
+ TextEntry textEntry = romEntry.tmTextsGameCorner.get(tmNumber);
+ setBottomScreenTMText(textEntry.textIndex, textEntry.stringNumber, newMoveIndex);
+ }
+
+ if (romEntry.tmScriptOffsetsFrontier.containsKey(tmNumber)) {
+ int scriptFile = romEntry.getInt("FrontierScriptNumber");
+ byte[] frontierScript = scriptNarc.files.get(scriptFile);
+ int scriptOffset = romEntry.tmScriptOffsetsFrontier.get(tmNumber);
+ writeWord(frontierScript, scriptOffset, newMoveIndex);
+ scriptNarc.files.set(scriptFile, frontierScript);
+ }
+
+ if (romEntry.tmTextsFrontier.containsKey(tmNumber)) {
+ int textOffset = romEntry.getInt("MiscUITextOffset");
+ int stringNumber = romEntry.tmTextsFrontier.get(tmNumber);
+ setBottomScreenTMText(textOffset, stringNumber, newMoveIndex);
+ }
+ }
+ }
+ }
+
+ private void setBottomScreenTMText(int textOffset, int stringNumber, int newMoveIndex) {
+ List<String> strings = getStrings(textOffset);
+ String originalString = strings.get(stringNumber);
+
+ // The first thing after the name is "\n".
+ int postNameIndex = originalString.indexOf("\\");
+ String originalName = originalString.substring(0, postNameIndex);
+
+ // Some languages (like English) write the name in ALL CAPS, others don't.
+ // Check if the original is ALL CAPS and then match it for consistency.
+ boolean isAllCaps = originalName.equals(originalName.toUpperCase());
+ String newName = moves[newMoveIndex].name;
+ if (isAllCaps) {
+ newName = newName.toUpperCase();
+ }
+ String newString = newName + originalString.substring(postNameIndex);
+ strings.set(stringNumber, newString);
+ setStrings(textOffset, strings);
+ }
+
+ private static RomFunctions.StringSizeDeterminer ssd = new RomFunctions.StringLengthSD();
+
+ @Override
+ public int getTMCount() {
+ return Gen4Constants.tmCount;
+ }
+
+ @Override
+ public int getHMCount() {
+ return Gen4Constants.hmCount;
+ }
+
+ @Override
+ public Map<Pokemon, boolean[]> getTMHMCompatibility() {
+ Map<Pokemon, boolean[]> compat = new TreeMap<>();
+ int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
+ for (int i = 1; i <= Gen4Constants.pokemonCount + formeCount; i++) {
+ byte[] data;
+ if (i > Gen4Constants.pokemonCount) {
+ data = pokeNarc.files.get(i + Gen4Constants.formeOffset);
+ } else {
+ data = pokeNarc.files.get(i);
+ }
+ Pokemon pkmn = pokes[i];
+ boolean[] flags = new boolean[Gen4Constants.tmCount + Gen4Constants.hmCount + 1];
+ for (int j = 0; j < 13; j++) {
+ readByteIntoFlags(data, flags, j * 8 + 1, Gen4Constants.bsTMHMCompatOffset + j);
+ }
+ compat.put(pkmn, flags);
+ }
+ return compat;
+ }
+
+ @Override
+ public void setTMHMCompatibility(Map<Pokemon, boolean[]> compatData) {
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ byte[] data = pokeNarc.files.get(pkmn.number);
+ for (int j = 0; j < 13; j++) {
+ data[Gen4Constants.bsTMHMCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasMoveTutors() {
+ return romEntry.romType != Gen4Constants.Type_DP;
+ }
+
+ @Override
+ public List<Integer> getMoveTutorMoves() {
+ if (!hasMoveTutors()) {
+ return new ArrayList<>();
+ }
+ int baseOffset = romEntry.getInt("MoveTutorMovesOffset");
+ int amount = romEntry.getInt("MoveTutorCount");
+ int bytesPer = romEntry.getInt("MoveTutorBytesCount");
+ List<Integer> mtMoves = new ArrayList<>();
+ try {
+ byte[] mtFile = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ for (int i = 0; i < amount; i++) {
+ mtMoves.add(readWord(mtFile, baseOffset + i * bytesPer));
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return mtMoves;
+ }
+
+ @Override
+ public void setMoveTutorMoves(List<Integer> moves) {
+ if (!hasMoveTutors()) {
+ return;
+ }
+ int baseOffset = romEntry.getInt("MoveTutorMovesOffset");
+ int amount = romEntry.getInt("MoveTutorCount");
+ int bytesPer = romEntry.getInt("MoveTutorBytesCount");
+ if (moves.size() != amount) {
+ return;
+ }
+ try {
+ byte[] mtFile = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ for (int i = 0; i < amount; i++) {
+ writeWord(mtFile, baseOffset + i * bytesPer, moves.get(i));
+ }
+ writeOverlay(romEntry.getInt("FieldOvlNumber"), mtFile);
+
+ // In HGSS, Headbutt is the last tutor move, but the tutor teaches it
+ // to you via a hardcoded script rather than looking at this data
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ setHGSSHeadbuttTutor(moves.get(moves.size() - 1));
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void setHGSSHeadbuttTutor(int headbuttReplacement) {
+ byte[] ilexForestScripts = scriptNarc.files.get(Gen4Constants.ilexForestScriptFile);
+ for (int offset : Gen4Constants.headbuttTutorScriptOffsets) {
+ writeWord(ilexForestScripts, offset, headbuttReplacement);
+ }
+
+ String replacementName = moves[headbuttReplacement].name;
+ Map<String, String> replacements = new TreeMap<>();
+ replacements.put(moves[Moves.headbutt].name, replacementName);
+ replaceAllStringsInEntry(Gen4Constants.ilexForestStringsFile, replacements);
+ }
+
+ @Override
+ public Map<Pokemon, boolean[]> getMoveTutorCompatibility() {
+ if (!hasMoveTutors()) {
+ return new TreeMap<>();
+ }
+ Map<Pokemon, boolean[]> compat = new TreeMap<>();
+ int amount = romEntry.getInt("MoveTutorCount");
+ int baseOffset = romEntry.getInt("MoveTutorCompatOffset");
+ int bytesPer = romEntry.getInt("MoveTutorCompatBytesCount");
+ try {
+ byte[] mtcFile;
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ mtcFile = readFile(romEntry.getFile("MoveTutorCompat"));
+ } else {
+ mtcFile = readOverlay(romEntry.getInt("MoveTutorCompatOvlNumber"));
+ }
+ int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
+ for (int i = 1; i <= Gen4Constants.pokemonCount + formeCount; i++) {
+ Pokemon pkmn = pokes[i];
+ boolean[] flags = new boolean[amount + 1];
+ for (int j = 0; j < bytesPer; j++) {
+ if (i > Gen4Constants.pokemonCount) {
+ readByteIntoFlags(mtcFile, flags, j * 8 + 1, baseOffset + (i - 1) * bytesPer + j);
+ } else {
+ readByteIntoFlags(mtcFile, flags, j * 8 + 1, baseOffset + (i - 1) * bytesPer + j);
+ }
+ }
+ compat.put(pkmn, flags);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return compat;
+ }
+
+ @Override
+ public void setMoveTutorCompatibility(Map<Pokemon, boolean[]> compatData) {
+ if (!hasMoveTutors()) {
+ return;
+ }
+ int amount = romEntry.getInt("MoveTutorCount");
+ int baseOffset = romEntry.getInt("MoveTutorCompatOffset");
+ int bytesPer = romEntry.getInt("MoveTutorCompatBytesCount");
+ try {
+ byte[] mtcFile;
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ mtcFile = readFile(romEntry.getFile("MoveTutorCompat"));
+ } else {
+ mtcFile = readOverlay(romEntry.getInt("MoveTutorCompatOvlNumber"));
+ }
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ for (int j = 0; j < bytesPer; j++) {
+ int offsHere = baseOffset + (pkmn.number - 1) * bytesPer + j;
+ if (j * 8 + 8 <= amount) {
+ // entirely new byte
+ mtcFile[offsHere] = getByteFromFlags(flags, j * 8 + 1);
+ } else if (j * 8 < amount) {
+ // need some of the original byte
+ int newByte = getByteFromFlags(flags, j * 8 + 1) & 0xFF;
+ int oldByteParts = (mtcFile[offsHere] >>> (8 - amount + j * 8)) << (8 - amount + j * 8);
+ mtcFile[offsHere] = (byte) (newByte | oldByteParts);
+ }
+ // else do nothing to the byte
+ }
+ }
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ writeFile(romEntry.getFile("MoveTutorCompat"), mtcFile);
+ } else {
+ writeOverlay(romEntry.getInt("MoveTutorCompatOvlNumber"), mtcFile);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ 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<Integer> 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);
+ }
+ }
+
+ private boolean lastStringsCompressed = false;
+
+ private List<String> getStrings(int index) {
+ PokeTextData pt = new PokeTextData(msgNarc.files.get(index));
+ pt.decrypt();
+ lastStringsCompressed = pt.compressFlag;
+ return new ArrayList<>(pt.strlist);
+ }
+
+ private void setStrings(int index, List<String> newStrings) {
+ setStrings(index, newStrings, false);
+ }
+
+ private void setStrings(int index, List<String> newStrings, boolean compressed) {
+ byte[] rawUnencrypted = TextToPoke.MakeFile(newStrings, compressed);
+
+ // make new encrypted name set
+ PokeTextData encrypt = new PokeTextData(rawUnencrypted);
+ encrypt.SetKey(0xD00E);
+ encrypt.encrypt();
+
+ // rewrite
+ msgNarc.files.set(index, encrypt.get());
+ }
+
+ @Override
+ public String getROMName() {
+ return "Pokemon " + romEntry.name;
+ }
+
+ @Override
+ public String getROMCode() {
+ return romEntry.romCode;
+ }
+
+ @Override
+ public String getSupportLevel() {
+ return romEntry.staticPokemonSupport ? "Complete" : "No Static Pokemon";
+ }
+
+ @Override
+ public boolean hasTimeBasedEncounters() {
+ // dppt technically do but we ignore them completely
+ return romEntry.romType == Gen4Constants.Type_HGSS;
+ }
+
+ @Override
+ public boolean hasWildAltFormes() {
+ return false;
+ }
+
+ @Override
+ public boolean canChangeStaticPokemon() {
+ return romEntry.staticPokemonSupport;
+ }
+
+ @Override
+ public boolean hasStaticAltFormes() {
+ return false;
+ }
+
+ @Override
+ public boolean hasMainGameLegendaries() {
+ return true;
+ }
+
+ @Override
+ public List<Integer> getMainGameLegendaries() {
+ return Arrays.stream(romEntry.arrayEntries.get("MainGameLegendaries")).boxed().collect(Collectors.toList());
+ }
+
+ @Override
+ public List<Integer> getSpecialMusicStatics() {
+ return Arrays.stream(romEntry.arrayEntries.get("SpecialMusicStatics")).boxed().collect(Collectors.toList());
+ }
+
+ @Override
+ public List<TotemPokemon> getTotemPokemon() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setTotemPokemon(List<TotemPokemon> totemPokemon) {
+
+ }
+
+ @Override
+ public boolean hasStarterAltFormes() {
+ return false;
+ }
+
+ @Override
+ public int starterCount() {
+ return 3;
+ }
+
+ @Override
+ public Map<Integer, StatChange> getUpdatedPokemonStats(int generation) {
+ return GlobalConstants.getStatChanges(generation);
+ }
+
+ private void populateEvolutions() {
+ for (Pokemon pkmn : pokes) {
+ if (pkmn != null) {
+ pkmn.evolutionsFrom.clear();
+ pkmn.evolutionsTo.clear();
+ }
+ }
+
+ // Read NARC
+ try {
+ NARCArchive evoNARC = readNARC(romEntry.getFile("PokemonEvolutions"));
+ for (int i = 1; i <= Gen4Constants.pokemonCount; i++) {
+ Pokemon pk = pokes[i];
+ byte[] evoEntry = evoNARC.files.get(i);
+ for (int evo = 0; evo < 7; evo++) {
+ int method = readWord(evoEntry, evo * 6);
+ int species = readWord(evoEntry, evo * 6 + 4);
+ if (method >= 1 && method <= Gen4Constants.evolutionMethodCount && species >= 1) {
+ EvolutionType et = EvolutionType.fromIndex(4, method);
+ int extraInfo = readWord(evoEntry, evo * 6 + 2);
+ Evolution evol = new Evolution(pokes[i], pokes[species], true, et, extraInfo);
+ if (!pk.evolutionsFrom.contains(evol)) {
+ pk.evolutionsFrom.add(evol);
+ pokes[species].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 writeEvolutions() {
+ try {
+ NARCArchive evoNARC = readNARC(romEntry.getFile("PokemonEvolutions"));
+ for (int i = 1; i <= Gen4Constants.pokemonCount; i++) {
+ byte[] evoEntry = evoNARC.files.get(i);
+ Pokemon pk = pokes[i];
+ if (pk.number == Species.nincada) {
+ writeShedinjaEvolution();
+ }
+ int evosWritten = 0;
+ for (Evolution evo : pk.evolutionsFrom) {
+ writeWord(evoEntry, evosWritten * 6, evo.type.toIndex(4));
+ writeWord(evoEntry, evosWritten * 6 + 2, evo.extraInfo);
+ writeWord(evoEntry, evosWritten * 6 + 4, evo.to.number);
+ evosWritten++;
+ if (evosWritten == 7) {
+ break;
+ }
+ }
+ while (evosWritten < 7) {
+ writeWord(evoEntry, evosWritten * 6, 0);
+ writeWord(evoEntry, evosWritten * 6 + 2, 0);
+ writeWord(evoEntry, evosWritten * 6 + 4, 0);
+ evosWritten++;
+ }
+ }
+ writeNARC(romEntry.getFile("PokemonEvolutions"), evoNARC);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void writeShedinjaEvolution() {
+ 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 extraEvolution = nincada.evolutionsFrom.get(1).to;
+
+ // In all the Gen 4 games, the game is hardcoded to check for
+ // the LEVEL_IS_EXTRA evolution method; if it the Pokemon has it,
+ // then a harcoded Shedinja is generated after every evolution
+ // by using the following instructions:
+ // mov r0, #0x49
+ // lsl r0, r0, #2
+ // The below code tweaks this instruction to load the species ID of Nincada's
+ // new extra evolution into r0 using an 8-bit addition. Since Gen 4 has fewer
+ // than 510 species in it, this will always succeed.
+ int offset = find(arm9, Gen4Constants.shedinjaSpeciesLocator);
+ if (offset > 0) {
+ int lowByte, highByte;
+ if (extraEvolution.number < 256) {
+ lowByte = extraEvolution.number;
+ highByte = 0;
+ } else {
+ lowByte = 255;
+ highByte = extraEvolution.number - 255;
+ }
+
+ // mov r0, lowByte
+ // add r0, r0, highByte
+ arm9[offset] = (byte) lowByte;
+ arm9[offset + 1] = 0x20;
+ arm9[offset + 2] = (byte) highByte;
+ arm9[offset + 3] = 0x30;
+ }
+ }
+
+ @Override
+ public void removeImpossibleEvolutions(Settings settings) {
+ boolean changeMoveEvos = !(settings.getMovesetsMod() == Settings.MovesetsMod.UNCHANGED);
+
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+ Set<Evolution> extraEvolutions = new HashSet<>();
+ for (Pokemon pkmn : pokes) {
+ if (pkmn != null) {
+ extraEvolutions.clear();
+ for (Evolution evo : pkmn.evolutionsFrom) {
+ // new 160 other impossible evolutions:
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ // beauty milotic
+ if (evo.type == EvolutionType.LEVEL_HIGH_BEAUTY) {
+ // Replace w/ level 35
+ evo.type = EvolutionType.LEVEL;
+ evo.extraInfo = 35;
+ addEvoUpdateLevel(impossibleEvolutionUpdates, evo);
+ }
+ // mt.coronet (magnezone/probopass)
+ if (evo.type == EvolutionType.LEVEL_ELECTRIFIED_AREA) {
+ // Replace w/ level 40
+ evo.type = EvolutionType.LEVEL;
+ evo.extraInfo = 40;
+ addEvoUpdateLevel(impossibleEvolutionUpdates, evo);
+ }
+ // moss rock (leafeon)
+ if (evo.type == EvolutionType.LEVEL_MOSS_ROCK) {
+ // Replace w/ leaf stone
+ evo.type = EvolutionType.STONE;
+ evo.extraInfo = Items.leafStone;
+ addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames.get(evo.extraInfo));
+ }
+ // icy rock (glaceon)
+ if (evo.type == EvolutionType.LEVEL_ICY_ROCK) {
+ // Replace w/ dawn stone
+ evo.type = EvolutionType.STONE;
+ evo.extraInfo = Items.dawnStone;
+ addEvoUpdateStone(impossibleEvolutionUpdates, evo, itemNames.get(evo.extraInfo));
+ }
+ }
+ 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);
+ }
+ }
+ }
+ 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);
+
+ // Reduce the amount of happiness required to evolve.
+ int offset = find(arm9, Gen4Constants.friendshipValueForEvoLocator);
+ if (offset > 0) {
+ // Amount of required happiness for HAPPINESS evolutions.
+ if (arm9[offset] == (byte)220) {
+ arm9[offset] = (byte)160;
+ }
+ // Amount of required happiness for HAPPINESS_DAY evolutions.
+ if (arm9[offset + 22] == (byte)220) {
+ arm9[offset + 22] = (byte)160;
+ }
+ // Amount of required happiness for HAPPINESS_NIGHT evolutions.
+ if (arm9[offset + 44] == (byte)220) {
+ arm9[offset + 44] = (byte)160;
+ }
+ }
+
+ 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<Evolution> 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);
+ }
+ }
+ }
+ pkmn.evolutionsFrom.addAll(extraEvolutions);
+ for (Evolution ev : extraEvolutions) {
+ ev.to.evolutionsTo.add(ev);
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public boolean hasShopRandomization() {
+ return true;
+ }
+
+ @Override
+ public Map<Integer, Shop> getShopItems() {
+ List<String> shopNames = Gen4Constants.getShopNames(romEntry.romType);
+ List<Integer> mainGameShops = Arrays.stream(romEntry.arrayEntries.get("MainGameShops")).boxed().collect(Collectors.toList());
+ List<Integer> skipShops = Arrays.stream(romEntry.arrayEntries.get("SkipShops")).boxed().collect(Collectors.toList());
+ int shopCount = romEntry.getInt("ShopCount");
+ Map<Integer, Shop> shopItemsMap = new TreeMap<>();
+ String shopDataPrefix = romEntry.getString("ShopDataPrefix");
+ int offset = find(arm9,shopDataPrefix);
+ offset += shopDataPrefix.length() / 2;
+
+ for (int i = 0; i < shopCount; i++) {
+ if (!skipShops.contains(i)) {
+ List<Integer> items = new ArrayList<>();
+ int val = (FileFunctions.read2ByteInt(arm9, offset));
+ while ((val & 0xFFFF) != 0xFFFF) {
+ if (val != 0) {
+ items.add(val);
+ }
+ offset += 2;
+ val = (FileFunctions.read2ByteInt(arm9, offset));
+ }
+ offset += 2;
+ Shop shop = new Shop();
+ shop.items = items;
+ shop.name = shopNames.get(i);
+ shop.isMainGame = mainGameShops.contains(i);
+ shopItemsMap.put(i, shop);
+ } else {
+ while ((FileFunctions.read2ByteInt(arm9, offset) & 0xFFFF) != 0xFFFF) {
+ offset += 2;
+ }
+ offset += 2;
+ }
+ }
+ return shopItemsMap;
+ }
+
+ @Override
+ public void setShopItems(Map<Integer, Shop> shopItems) {
+ int shopCount = romEntry.getInt("ShopCount");
+ String shopDataPrefix = romEntry.getString("ShopDataPrefix");
+ int offset = find(arm9,shopDataPrefix);
+ offset += shopDataPrefix.length() / 2;
+
+ for (int i = 0; i < shopCount; i++) {
+ Shop thisShop = shopItems.get(i);
+ if (thisShop == null || thisShop.items == null) {
+ while ((FileFunctions.read2ByteInt(arm9, offset) & 0xFFFF) != 0xFFFF) {
+ offset += 2;
+ }
+ offset += 2;
+ continue;
+ }
+ Iterator<Integer> iterItems = thisShop.items.iterator();
+ int val = (FileFunctions.read2ByteInt(arm9, offset));
+ while ((val & 0xFFFF) != 0xFFFF) {
+ if (val != 0) {
+ FileFunctions.write2ByteInt(arm9,offset,iterItems.next());
+ }
+ offset += 2;
+ val = (FileFunctions.read2ByteInt(arm9, offset));
+ }
+ offset += 2;
+ }
+ }
+
+ @Override
+ public void setShopPrices() {
+ try {
+ // In Diamond and Pearl, item IDs 112 through 134 are unused. In Platinum and HGSS, item ID 112 is used for
+ // the Griseous Orb. So we need to skip through the unused IDs at different points depending on the game.
+ int startOfUnusedIDs = romEntry.romType == Gen4Constants.Type_DP ? 112 : 113;
+ NARCArchive itemPriceNarc = this.readNARC(romEntry.getFile("ItemData"));
+ int itemID = 1;
+ for (int i = 1; i < itemPriceNarc.files.size(); i++) {
+ writeWord(itemPriceNarc.files.get(i),0,Gen4Constants.balancedItemPrices.get(itemID) * 10);
+ itemID++;
+ if (itemID == startOfUnusedIDs) {
+ itemID = 135;
+ }
+ }
+ writeNARC(romEntry.getFile("ItemData"),itemPriceNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public List<PickupItem> getPickupItems() {
+ List<PickupItem> pickupItems = new ArrayList<>();
+ try {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("BattleOvlNumber"));
+ if (pickupItemsTableOffset == 0) {
+ int offset = find(battleOverlay, Gen4Constants.pickupTableLocator);
+ if (offset > 0) {
+ pickupItemsTableOffset = offset;
+ }
+ }
+
+ // If we haven't found the pickup table for this ROM already, find it.
+ if (rarePickupItemsTableOffset == 0) {
+ int offset = find(battleOverlay, Gen4Constants.rarePickupTableLocator);
+ if (offset > 0) {
+ rarePickupItemsTableOffset = offset;
+ }
+ }
+
+ // Assuming we've found the pickup table, extract the items out of it.
+ if (pickupItemsTableOffset > 0 && rarePickupItemsTableOffset > 0) {
+ for (int i = 0; i < Gen4Constants.numberOfCommonPickupItems; i++) {
+ int itemOffset = pickupItemsTableOffset + (2 * i);
+ int item = FileFunctions.read2ByteInt(battleOverlay, itemOffset);
+ PickupItem pickupItem = new PickupItem(item);
+ pickupItems.add(pickupItem);
+ }
+ for (int i = 0; i < Gen4Constants.numberOfRarePickupItems; i++) {
+ int itemOffset = rarePickupItemsTableOffset + (2 * i);
+ int item = FileFunctions.read2ByteInt(battleOverlay, 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;
+ }
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return pickupItems;
+ }
+
+ @Override
+ public void setPickupItems(List<PickupItem> pickupItems) {
+ try {
+ if (pickupItemsTableOffset > 0 && rarePickupItemsTableOffset > 0) {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("BattleOvlNumber"));
+ Iterator<PickupItem> itemIterator = pickupItems.iterator();
+ for (int i = 0; i < Gen4Constants.numberOfCommonPickupItems; i++) {
+ int itemOffset = pickupItemsTableOffset + (2 * i);
+ int item = itemIterator.next().item;
+ FileFunctions.write2ByteInt(battleOverlay, itemOffset, item);
+ }
+ for (int i = 0; i < Gen4Constants.numberOfRarePickupItems; i++) {
+ int itemOffset = rarePickupItemsTableOffset + (2 * i);
+ int item = itemIterator.next().item;
+ FileFunctions.write2ByteInt(battleOverlay, itemOffset, item);
+ }
+ writeOverlay(romEntry.getInt("BattleOvlNumber"), battleOverlay);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public boolean canChangeTrainerText() {
+ return true;
+ }
+
+ @Override
+ public List<String> getTrainerNames() {
+ List<String> tnames = new ArrayList<>(getStrings(romEntry.getInt("TrainerNamesTextOffset")));
+ tnames.remove(0); // blank one
+ for (int i = 0; i < tnames.size(); i++) {
+ if (tnames.get(i).contains("\\and")) {
+ tnames.set(i, tnames.get(i).replace("\\and", "&"));
+ }
+ }
+ return tnames;
+ }
+
+ @Override
+ public int maxTrainerNameLength() {
+ return 10;// based off the english ROMs fixed
+ }
+
+ @Override
+ public void setTrainerNames(List<String> trainerNames) {
+ List<String> oldTNames = getStrings(romEntry.getInt("TrainerNamesTextOffset"));
+ List<String> newTNames = new ArrayList<>(trainerNames);
+ for (int i = 0; i < newTNames.size(); i++) {
+ if (newTNames.get(i).contains("&")) {
+ newTNames.set(i, newTNames.get(i).replace("&", "\\and"));
+ }
+ }
+ newTNames.add(0, oldTNames.get(0)); // the 0-entry, preserve it
+
+ // rewrite, only compressed if they were compressed before
+ setStrings(romEntry.getInt("TrainerNamesTextOffset"), newTNames, lastStringsCompressed);
+
+ }
+
+ @Override
+ public TrainerNameMode trainerNameMode() {
+ return TrainerNameMode.MAX_LENGTH;
+ }
+
+ @Override
+ public List<Integer> getTCNameLengthsByTrainer() {
+ // not needed
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<String> getTrainerClassNames() {
+ return getStrings(romEntry.getInt("TrainerClassesTextOffset"));
+ }
+
+ @Override
+ public void setTrainerClassNames(List<String> trainerClassNames) {
+ setStrings(romEntry.getInt("TrainerClassesTextOffset"), trainerClassNames);
+ }
+
+ @Override
+ public int maxTrainerClassNameLength() {
+ return 12;// based off the english ROMs
+ }
+
+ @Override
+ public boolean fixedTrainerClassNamesLength() {
+ return false;
+ }
+
+ @Override
+ public List<Integer> getDoublesTrainerClasses() {
+ int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses");
+ List<Integer> doubles = new ArrayList<>();
+ for (int tClass : doublesClasses) {
+ doubles.add(tClass);
+ }
+ return doubles;
+ }
+
+ @Override
+ public String getDefaultExtension() {
+ return "nds";
+ }
+
+ @Override
+ public int abilitiesPerPokemon() {
+ return 2;
+ }
+
+ @Override
+ public int highestAbilityIndex() {
+ return Gen4Constants.highestAbilityIndex;
+ }
+
+ @Override
+ public int internalStringLength(String string) {
+ return string.length();
+ }
+
+ @Override
+ public void randomizeIntroPokemon() {
+ try {
+ if (romEntry.romType == Gen4Constants.Type_DP || romEntry.romType == Gen4Constants.Type_Plat) {
+ Pokemon introPokemon = randomPokemon();
+ while (introPokemon.genderRatio == 0xFE) {
+ // This is a female-only Pokemon. Gen 4 has an annoying quirk where female-only Pokemon *need*
+ // to pass a special parameter into the function that loads Pokemon sprites; the game will
+ // softlock on native hardware otherwise. The way the compiler has optimized the intro Pokemon
+ // code makes it very hard to modify, so passing in this special parameter is difficult. Rather
+ // than attempt to patch this code, just reroll until it isn't female-only.
+ introPokemon = randomPokemon();
+ }
+ byte[] introOverlay = readOverlay(romEntry.getInt("IntroOvlNumber"));
+ for (String prefix : Gen4Constants.dpptIntroPrefixes) {
+ int offset = find(introOverlay, prefix);
+ if (offset > 0) {
+ offset += prefix.length() / 2; // because it was a prefix
+ writeWord(introOverlay, offset, introPokemon.number);
+ }
+ }
+ writeOverlay(romEntry.getInt("IntroOvlNumber"), introOverlay);
+ } else if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ // Modify the sprite used for Ethan/Lyra's Marill
+ int marillReplacement = this.random.nextInt(548) + 297;
+ while (Gen4Constants.hgssBannedOverworldPokemon.contains(marillReplacement)) {
+ marillReplacement = this.random.nextInt(548) + 297;
+ }
+
+ byte[] fieldOverlay = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ String prefix = Gen4Constants.lyraEthanMarillSpritePrefix;
+ int offset = find(fieldOverlay, prefix);
+ if (offset > 0) {
+ offset += prefix.length() / 2; // because it was a prefix
+ writeWord(fieldOverlay, offset, marillReplacement);
+ if (Gen4Constants.hgssBigOverworldPokemon.contains(marillReplacement)) {
+ // Write the constant to indicate it's big (0x208 | (20 << 10))
+ writeWord(fieldOverlay, offset + 2, 0x5208);
+ } else {
+ // Write the constant to indicate it's normal-sized (0x227 | (19 << 10))
+ writeWord(fieldOverlay, offset + 2, 0x4E27);
+ }
+ }
+ writeOverlay(romEntry.getInt("FieldOvlNumber"), fieldOverlay);
+
+ // Now modify the Marill's cry in every script it appears in to ensure consistency
+ int marillReplacementId = Gen4Constants.convertOverworldSpriteToSpecies(marillReplacement);
+ for (ScriptEntry entry : romEntry.marillCryScriptEntries) {
+ byte[] script = scriptNarc.files.get(entry.scriptFile);
+ writeWord(script, entry.scriptOffset, marillReplacementId);
+ scriptNarc.files.set(entry.scriptFile, script);
+ }
+
+ // Modify the text too for additional consistency
+ int[] textOffsets = romEntry.arrayEntries.get("MarillTextFiles");
+ String originalSpeciesString = pokes[Species.marill].name.toUpperCase();
+ String newSpeciesString = pokes[marillReplacementId].name;
+ Map<String, String> replacements = new TreeMap<>();
+ replacements.put(originalSpeciesString, newSpeciesString);
+ for (int i = 0; i < textOffsets.length; i++) {
+ replaceAllStringsInEntry(textOffsets[i], replacements);
+ }
+
+ // Lastly, modify the catching tutorial to use the new Pokemon if we're capable of doing so
+ if (romEntry.tweakFiles.containsKey("NewCatchingTutorialSubroutineTweak")) {
+ String catchingTutorialMonTablePrefix = romEntry.getString("CatchingTutorialMonTablePrefix");
+ offset = find(arm9, catchingTutorialMonTablePrefix);
+ if (offset > 0) {
+ offset += catchingTutorialMonTablePrefix.length() / 2; // because it was a prefix
+
+ // As part of our catching tutorial patch, the player Pokemon's ID is just pc-relative
+ // loaded, and offset is now pointing to it.
+ writeWord(arm9, offset, marillReplacementId);
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public ItemList getAllowedItems() {
+ return allowedItems;
+ }
+
+ @Override
+ public ItemList getNonBadItems() {
+ return nonBadItems;
+ }
+
+ @Override
+ public List<Integer> getUniqueNoSellItems() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getRegularShopItems() {
+ return Gen4Constants.regularShopItems;
+ }
+
+ @Override
+ public List<Integer> getOPShopItems() {
+ return Gen4Constants.opShopItems;
+ }
+
+ @Override
+ public String[] getItemNames() {
+ return itemNames.toArray(new String[0]);
+ }
+
+ @Override
+ public String abilityName(int number) {
+ return abilityNames.get(number);
+ }
+
+ @Override
+ public Map<Integer, List<Integer>> getAbilityVariations() {
+ return Gen4Constants.abilityVariations;
+ }
+
+ @Override
+ public List<Integer> getUselessAbilities() {
+ return new ArrayList<>(Gen4Constants.uselessAbilities);
+ }
+
+ @Override
+ public int getAbilityForTrainerPokemon(TrainerPokemon tp) {
+ // In Gen 4, alt formes for Trainer Pokemon use the base forme's ability
+ Pokemon pkmn = tp.pokemon;
+ while (pkmn.baseForme != null) {
+ pkmn = pkmn.baseForme;
+ }
+
+ if (romEntry.romType == Gen4Constants.Type_DP || romEntry.romType == Gen4Constants.Type_Plat) {
+ // In DPPt, Trainer Pokemon *always* use the first Ability, no matter what
+ return pkmn.ability1;
+ } else {
+ // In HGSS, Trainer Pokemon can specify which ability they want to use.
+ return tp.abilitySlot == 2 ? pkmn.ability2 : pkmn.ability1;
+ }
+ }
+
+ @Override
+ public boolean hasMegaEvolutions() {
+ return false;
+ }
+
+ private List<Integer> getFieldItems() {
+ List<Integer> fieldItems = new ArrayList<>();
+ // normal items
+ int scriptFile = romEntry.getInt("ItemBallsScriptOffset");
+ byte[] itemScripts = scriptNarc.files.get(scriptFile);
+ int offset = 0;
+ int skipTableOffset = 0;
+ int[] skipTable = romEntry.arrayEntries.get("ItemBallsSkip");
+ int setVar = romEntry.romType == Gen4Constants.Type_HGSS ? Gen4Constants.hgssSetVarScript
+ : Gen4Constants.dpptSetVarScript;
+ while (true) {
+ int part1 = readWord(itemScripts, offset);
+ if (part1 == Gen4Constants.scriptListTerminator) {
+ // done
+ break;
+ }
+ int offsetInFile = readRelativePointer(itemScripts, offset);
+ offset += 4;
+ if (skipTableOffset < skipTable.length && (skipTable[skipTableOffset] == (offset / 4) - 1)) {
+ skipTableOffset++;
+ continue;
+ }
+ int command = readWord(itemScripts, offsetInFile);
+ int variable = readWord(itemScripts, offsetInFile + 2);
+ if (command == setVar && variable == Gen4Constants.itemScriptVariable) {
+ int item = readWord(itemScripts, offsetInFile + 4);
+ fieldItems.add(item);
+ }
+
+ }
+
+ // hidden items
+ int hiTableOffset = romEntry.getInt("HiddenItemTableOffset");
+ int hiTableLimit = romEntry.getInt("HiddenItemCount");
+ for (int i = 0; i < hiTableLimit; i++) {
+ int item = readWord(arm9, hiTableOffset + i * 8);
+ fieldItems.add(item);
+ }
+
+ return fieldItems;
+ }
+
+ private void setFieldItems(List<Integer> fieldItems) {
+ Iterator<Integer> iterItems = fieldItems.iterator();
+
+ // normal items
+ int scriptFile = romEntry.getInt("ItemBallsScriptOffset");
+ byte[] itemScripts = scriptNarc.files.get(scriptFile);
+ int offset = 0;
+ int skipTableOffset = 0;
+ int[] skipTable = romEntry.arrayEntries.get("ItemBallsSkip");
+ int setVar = romEntry.romType == Gen4Constants.Type_HGSS ? Gen4Constants.hgssSetVarScript
+ : Gen4Constants.dpptSetVarScript;
+ while (true) {
+ int part1 = readWord(itemScripts, offset);
+ if (part1 == Gen4Constants.scriptListTerminator) {
+ // done
+ break;
+ }
+ int offsetInFile = readRelativePointer(itemScripts, offset);
+ offset += 4;
+ if (skipTableOffset < skipTable.length && (skipTable[skipTableOffset] == (offset / 4) - 1)) {
+ skipTableOffset++;
+ continue;
+ }
+ int command = readWord(itemScripts, offsetInFile);
+ int variable = readWord(itemScripts, offsetInFile + 2);
+ if (command == setVar && variable == Gen4Constants.itemScriptVariable) {
+ int item = iterItems.next();
+ writeWord(itemScripts, offsetInFile + 4, item);
+ }
+ }
+
+ // hidden items
+ int hiTableOffset = romEntry.getInt("HiddenItemTableOffset");
+ int hiTableLimit = romEntry.getInt("HiddenItemCount");
+ for (int i = 0; i < hiTableLimit; i++) {
+ int item = iterItems.next();
+ writeWord(arm9, hiTableOffset + i * 8, item);
+ }
+ }
+
+ @Override
+ public List<Integer> getRequiredFieldTMs() {
+ if (romEntry.romType == Gen4Constants.Type_DP) {
+ return Gen4Constants.dpRequiredFieldTMs;
+ } else if (romEntry.romType == Gen4Constants.Type_Plat) {
+ // same as DP just we have to keep the weather TMs
+ return Gen4Constants.ptRequiredFieldTMs;
+ }
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getCurrentFieldTMs() {
+ List<Integer> fieldItems = this.getFieldItems();
+ List<Integer> fieldTMs = new ArrayList<>();
+
+ for (int item : fieldItems) {
+ if (Gen4Constants.allowedItems.isTM(item)) {
+ fieldTMs.add(item - Gen4Constants.tmItemOffset + 1);
+ }
+ }
+
+ return fieldTMs;
+ }
+
+ @Override
+ public void setFieldTMs(List<Integer> fieldTMs) {
+ List<Integer> fieldItems = this.getFieldItems();
+ int fiLength = fieldItems.size();
+ Iterator<Integer> iterTMs = fieldTMs.iterator();
+
+ for (int i = 0; i < fiLength; i++) {
+ int oldItem = fieldItems.get(i);
+ if (Gen4Constants.allowedItems.isTM(oldItem)) {
+ int newItem = iterTMs.next() + Gen4Constants.tmItemOffset - 1;
+ fieldItems.set(i, newItem);
+ }
+ }
+
+ this.setFieldItems(fieldItems);
+ }
+
+ @Override
+ public List<Integer> getRegularFieldItems() {
+ List<Integer> fieldItems = this.getFieldItems();
+ List<Integer> fieldRegItems = new ArrayList<>();
+
+ for (int item : fieldItems) {
+ if (Gen4Constants.allowedItems.isAllowed(item) && !(Gen4Constants.allowedItems.isTM(item))) {
+ fieldRegItems.add(item);
+ }
+ }
+
+ return fieldRegItems;
+ }
+
+ @Override
+ public void setRegularFieldItems(List<Integer> items) {
+ List<Integer> fieldItems = this.getFieldItems();
+ int fiLength = fieldItems.size();
+ Iterator<Integer> iterNewItems = items.iterator();
+
+ for (int i = 0; i < fiLength; i++) {
+ int oldItem = fieldItems.get(i);
+ if (!(Gen4Constants.allowedItems.isTM(oldItem)) && Gen4Constants.allowedItems.isAllowed(oldItem)) {
+ int newItem = iterNewItems.next();
+ fieldItems.set(i, newItem);
+ }
+ }
+
+ this.setFieldItems(fieldItems);
+ }
+
+ @Override
+ public List<IngameTrade> getIngameTrades() {
+ List<IngameTrade> trades = new ArrayList<>();
+ try {
+ NARCArchive tradeNARC = this.readNARC(romEntry.getFile("InGameTrades"));
+ int[] spTrades = new int[0];
+ if (romEntry.arrayEntries.containsKey("StaticPokemonTrades")) {
+ spTrades = romEntry.arrayEntries.get("StaticPokemonTrades");
+ }
+ List<String> tradeStrings = getStrings(romEntry.getInt("IngameTradesTextOffset"));
+ int tradeCount = tradeNARC.files.size();
+ for (int i = 0; i < tradeCount; i++) {
+ boolean isSP = false;
+ for (int spTrade : spTrades) {
+ if (spTrade == i) {
+ isSP = true;
+ break;
+ }
+ }
+ if (isSP) {
+ continue;
+ }
+ byte[] tfile = tradeNARC.files.get(i);
+ IngameTrade trade = new IngameTrade();
+ trade.nickname = tradeStrings.get(i);
+ trade.givenPokemon = pokes[readLong(tfile, 0)];
+ trade.ivs = new int[6];
+ for (int iv = 0; iv < 6; iv++) {
+ trade.ivs[iv] = readLong(tfile, 4 + iv * 4);
+ }
+ trade.otId = readWord(tfile, 0x20);
+ trade.otName = tradeStrings.get(i + tradeCount);
+ trade.item = readLong(tfile, 0x3C);
+ trade.requestedPokemon = pokes[readLong(tfile, 0x4C)];
+ trades.add(trade);
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ return trades;
+ }
+
+ @Override
+ public void setIngameTrades(List<IngameTrade> trades) {
+ int tradeOffset = 0;
+ List<IngameTrade> oldTrades = this.getIngameTrades();
+ try {
+ NARCArchive tradeNARC = this.readNARC(romEntry.getFile("InGameTrades"));
+ int[] spTrades = new int[0];
+ if (romEntry.arrayEntries.containsKey("StaticPokemonTrades")) {
+ spTrades = romEntry.arrayEntries.get("StaticPokemonTrades");
+ }
+ List<String> tradeStrings = getStrings(romEntry.getInt("IngameTradesTextOffset"));
+ int tradeCount = tradeNARC.files.size();
+ for (int i = 0; i < tradeCount; i++) {
+ boolean isSP = false;
+ for (int spTrade : spTrades) {
+ if (spTrade == i) {
+ isSP = true;
+ break;
+ }
+ }
+ if (isSP) {
+ continue;
+ }
+ byte[] tfile = tradeNARC.files.get(i);
+ IngameTrade trade = trades.get(tradeOffset++);
+ tradeStrings.set(i, trade.nickname);
+ tradeStrings.set(i + tradeCount, trade.otName);
+ writeLong(tfile, 0, trade.givenPokemon.number);
+ for (int iv = 0; iv < 6; iv++) {
+ writeLong(tfile, 4 + iv * 4, trade.ivs[iv]);
+ }
+ writeWord(tfile, 0x20, trade.otId);
+ writeLong(tfile, 0x3C, trade.item);
+ writeLong(tfile, 0x4C, trade.requestedPokemon.number);
+ if (tfile.length > 0x50) {
+ writeLong(tfile, 0x50, 0); // disable gender
+ }
+ }
+ this.writeNARC(romEntry.getFile("InGameTrades"), tradeNARC);
+ this.setStrings(romEntry.getInt("IngameTradesTextOffset"), tradeStrings);
+ // update what the people say when they talk to you
+ if (romEntry.arrayEntries.containsKey("IngameTradePersonTextOffsets")) {
+ int[] textOffsets = romEntry.arrayEntries.get("IngameTradePersonTextOffsets");
+ for (int trade = 0; trade < textOffsets.length; trade++) {
+ if (textOffsets[trade] > 0) {
+ if (trade >= oldTrades.size() || trade >= trades.size()) {
+ break;
+ }
+ IngameTrade oldTrade = oldTrades.get(trade);
+ IngameTrade newTrade = trades.get(trade);
+ Map<String, String> replacements = new TreeMap<>();
+ replacements.put(oldTrade.givenPokemon.name.toUpperCase(), newTrade.givenPokemon.name);
+ if (oldTrade.requestedPokemon != newTrade.requestedPokemon) {
+ replacements.put(oldTrade.requestedPokemon.name.toUpperCase(), newTrade.requestedPokemon.name);
+ }
+ replaceAllStringsInEntry(textOffsets[trade], replacements);
+ // hgss override for one set of strings that appears 2x
+ if (romEntry.romType == Gen4Constants.Type_HGSS && trade == 6) {
+ replaceAllStringsInEntry(textOffsets[trade] + 1, replacements);
+ }
+ }
+ }
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ }
+
+ private void replaceAllStringsInEntry(int entry, Map<String, String> replacements) {
+ // This function currently only replaces move and Pokemon names, and we don't want them
+ // split across multiple lines if there is a space.
+ replacements.replaceAll((key, oldValue) -> oldValue.replace(' ', '_'));
+ int lineLength = Gen4Constants.getTextCharsPerLine(romEntry.romType);
+ List<String> strings = this.getStrings(entry);
+ for (int strNum = 0; strNum < strings.size(); strNum++) {
+ String oldString = strings.get(strNum);
+ boolean needsReplacement = false;
+ for (Map.Entry<String, String> replacement : replacements.entrySet()) {
+ if (oldString.contains(replacement.getKey())) {
+ needsReplacement = true;
+ break;
+ }
+ }
+ if (needsReplacement) {
+ String newString = RomFunctions.formatTextWithReplacements(oldString, replacements, "\\n", "\\l", "\\p",
+ lineLength, ssd);
+ newString = newString.replace('_', ' ');
+ strings.set(strNum, newString);
+ }
+ }
+ this.setStrings(entry, strings);
+ }
+
+ @Override
+ public boolean hasDVs() {
+ return false;
+ }
+
+ @Override
+ public int generationOfPokemon() {
+ return 4;
+ }
+
+ @Override
+ public void removeEvosForPokemonPool() {
+ // slightly more complicated than gen2/3
+ // we have to update a "baby table" too
+ List<Pokemon> pokemonIncluded = this.mainPokemonList;
+ Set<Evolution> keepEvos = new HashSet<>();
+ for (Pokemon pk : pokes) {
+ if (pk != null) {
+ keepEvos.clear();
+ for (Evolution evol : pk.evolutionsFrom) {
+ if (pokemonIncluded.contains(evol.from) && pokemonIncluded.contains(evol.to)) {
+ keepEvos.add(evol);
+ } else {
+ evol.to.evolutionsTo.remove(evol);
+ }
+ }
+ pk.evolutionsFrom.retainAll(keepEvos);
+ }
+ }
+
+ try {
+ byte[] babyPokes = readFile(romEntry.getFile("BabyPokemon"));
+ // baby pokemon
+ for (int i = 1; i <= Gen4Constants.pokemonCount; 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(babyPokes, i * 2, baby.number);
+ }
+ // finish up
+ writeFile(romEntry.getFile("BabyPokemon"), babyPokes);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public boolean supportsFourStartingMoves() {
+ return true;
+ }
+
+ @Override
+ public List<Integer> getFieldMoves() {
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ return Gen4Constants.hgssFieldMoves;
+ } else {
+ return Gen4Constants.dpptFieldMoves;
+ }
+ }
+
+ @Override
+ public List<Integer> getEarlyRequiredHMMoves() {
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ return Gen4Constants.hgssEarlyRequiredHMMoves;
+ } else {
+ return Gen4Constants.dpptEarlyRequiredHMMoves;
+ }
+ }
+
+ @Override
+ public int miscTweaksAvailable() {
+ int available = MiscTweak.LOWER_CASE_POKEMON_NAMES.getValue();
+ available |= MiscTweak.RANDOMIZE_CATCHING_TUTORIAL.getValue();
+ available |= MiscTweak.UPDATE_TYPE_EFFECTIVENESS.getValue();
+ if (romEntry.tweakFiles.get("FastestTextTweak") != null) {
+ available |= MiscTweak.FASTEST_TEXT.getValue();
+ }
+ available |= MiscTweak.BAN_LUCKY_EGG.getValue();
+ if (romEntry.tweakFiles.get("NationalDexAtStartTweak") != null) {
+ available |= MiscTweak.NATIONAL_DEX_AT_START.getValue();
+ }
+ available |= MiscTweak.RUN_WITHOUT_RUNNING_SHOES.getValue();
+ available |= MiscTweak.FASTER_HP_AND_EXP_BARS.getValue();
+ if (romEntry.tweakFiles.get("FastDistortionWorldTweak") != null) {
+ available |= MiscTweak.FAST_DISTORTION_WORLD.getValue();
+ }
+ if (romEntry.romType == Gen4Constants.Type_Plat || romEntry.romType == Gen4Constants.Type_HGSS) {
+ available |= MiscTweak.UPDATE_ROTOM_FORME_TYPING.getValue();
+ }
+ return available;
+ }
+
+ @Override
+ public void applyMiscTweak(MiscTweak tweak) {
+ if (tweak == MiscTweak.LOWER_CASE_POKEMON_NAMES) {
+ applyCamelCaseNames();
+ } else if (tweak == MiscTweak.RANDOMIZE_CATCHING_TUTORIAL) {
+ randomizeCatchingTutorial();
+ } else 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.NATIONAL_DEX_AT_START) {
+ patchForNationalDex();
+ } else if (tweak == MiscTweak.RUN_WITHOUT_RUNNING_SHOES) {
+ applyRunWithoutRunningShoesPatch();
+ } else if (tweak == MiscTweak.FASTER_HP_AND_EXP_BARS) {
+ patchFasterBars();
+ } else if (tweak == MiscTweak.UPDATE_TYPE_EFFECTIVENESS) {
+ updateTypeEffectiveness();
+ } else if (tweak == MiscTweak.FAST_DISTORTION_WORLD) {
+ applyFastDistortionWorld();
+ } else if (tweak == MiscTweak.UPDATE_ROTOM_FORME_TYPING) {
+ updateRotomFormeTyping();
+ }
+ }
+
+ @Override
+ public boolean isEffectivenessUpdated() {
+ return effectivenessUpdated;
+ }
+
+ private void randomizeCatchingTutorial() {
+ int opponentOffset = romEntry.getInt("CatchingTutorialOpponentMonOffset");
+
+ if (romEntry.tweakFiles.containsKey("NewCatchingTutorialSubroutineTweak")) {
+ String catchingTutorialMonTablePrefix = romEntry.getString("CatchingTutorialMonTablePrefix");
+ int offset = find(arm9, catchingTutorialMonTablePrefix);
+ if (offset > 0) {
+ offset += catchingTutorialMonTablePrefix.length() / 2; // because it was a prefix
+
+ // The player's mon is randomized as part of randomizing Lyra/Ethan's Pokemon (see
+ // randomizeIntroPokemon), so we just care about the enemy mon. As part of our catching
+ // tutorial patch, the player and enemy species IDs are pc-relative loaded, with the
+ // enemy ID occurring right after the player ID (which is what offset is pointing to).
+ Pokemon opponent = randomPokemonLimited(Integer.MAX_VALUE, false);
+ writeWord(arm9, offset + 4, opponent.number);
+ }
+ } else if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ // For non-US HGSS, just handle it in the old-school way. Can randomize both Pokemon, but both limited to 1-255
+ // Make sure to raise the level of Lyra/Ethan's Pokemon to 10 to prevent softlocks
+ int playerOffset = romEntry.getInt("CatchingTutorialPlayerMonOffset");
+ int levelOffset = romEntry.getInt("CatchingTutorialPlayerLevelOffset");
+ Pokemon opponent = randomPokemonLimited(255, false);
+ Pokemon player = randomPokemonLimited(255, false);
+ if (opponent != null && player != null) {
+ arm9[opponentOffset] = (byte) opponent.number;
+ arm9[playerOffset] = (byte) player.number;
+ arm9[levelOffset] = 10;
+ }
+ } else {
+ // DPPt only supports randomizing the opponent, but enough space for any mon
+ Pokemon opponent = randomPokemonLimited(Integer.MAX_VALUE, false);
+
+ if (opponent != null) {
+ writeLong(arm9, opponentOffset, opponent.number);
+ }
+ }
+
+ }
+
+ private void applyFastestText() {
+ genericIPSPatch(arm9, "FastestTextTweak");
+ }
+
+ private void patchForNationalDex() {
+ byte[] pokedexScript = scriptNarc.files.get(romEntry.getInt("NationalDexScriptOffset"));
+
+ if (romEntry.romType == Gen4Constants.Type_HGSS) {
+ // Our patcher breaks if the output file is larger than the input file. For HGSS, we want
+ // to expand the script by four bytes to add an instruction to enable the national dex. Thus,
+ // the IPS patch was created with us adding four 0x00 bytes to the end of the script in mind.
+ byte[] expandedPokedexScript = new byte[pokedexScript.length + 4];
+ System.arraycopy(pokedexScript, 0, expandedPokedexScript, 0, pokedexScript.length);
+ pokedexScript = expandedPokedexScript;
+ }
+ genericIPSPatch(pokedexScript, "NationalDexAtStartTweak");
+ scriptNarc.files.set(romEntry.getInt("NationalDexScriptOffset"), pokedexScript);
+ }
+
+ private void applyRunWithoutRunningShoesPatch() {
+ String prefix = Gen4Constants.getRunWithoutRunningShoesPrefix(romEntry.romType);
+ int offset = find(arm9, prefix);
+ if (offset != 0) {
+ // The prefix starts 0xE bytes from what we want to patch because what comes
+ // between is region and revision dependent. To start running, the game checks:
+ // 1. That you're holding the B button
+ // 2. That the FLAG_SYS_B_DASH flag is set (aka, you've acquired Running Shoes)
+ // For #2, if the flag is unset, it jumps to a different part of the
+ // code to make you walk instead. This simply nops out this jump so the
+ // game stops caring about the FLAG_SYS_B_DASH flag entirely.
+ writeWord(arm9,offset + 0xE, 0);
+ }
+ }
+
+ private void patchFasterBars() {
+ // To understand what this code is patching, take a look at the CalcNewBarValue
+ // and MoveBattleBar functions in this file from the Emerald decompilation:
+ // https://github.com/pret/pokeemerald/blob/master/src/battle_interface.c
+ // The code in Gen 4 is almost identical outside of one single constant; the
+ // reason the bars scroll slower is because Gen 4 runs at 30 FPS instead of 60.
+ try {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("BattleOvlNumber"));
+ int offset = find(battleOverlay, Gen4Constants.hpBarSpeedPrefix);
+ if (offset > 0) {
+ offset += Gen4Constants.hpBarSpeedPrefix.length() / 2; // because it was a prefix
+ // For the HP bar, the original game passes 1 for the toAdd parameter of CalcNewBarValue.
+ // We want to pass 2 instead, so we simply change the mov instruction at offset.
+ battleOverlay[offset] = 0x02;
+ }
+
+ offset = find(battleOverlay, Gen4Constants.expBarSpeedPrefix);
+ if (offset > 0) {
+ offset += Gen4Constants.expBarSpeedPrefix.length() / 2; // because it was a prefix
+ // For the EXP bar, the original game passes expFraction for the toAdd parameter. The
+ // game calculates expFraction by doing a division, and to do *that*, it has to load
+ // receivedValue into r0 so it can call the division function with it as the first
+ // parameter. It gets the value from r6 like so:
+ // add r0, r6, #0
+ // Since we ultimately want toAdd (and thus expFraction) to be doubled, we can double
+ // receivedValue when it gets loaded into r0 by tweaking the add to be:
+ // add r0, r6, r6
+ battleOverlay[offset] = (byte) 0xB0;
+ battleOverlay[offset + 1] = 0x19;
+ }
+
+ offset = find(battleOverlay, Gen4Constants.bothBarsSpeedPrefix);
+ if (offset > 0) {
+ offset += Gen4Constants.bothBarsSpeedPrefix.length() / 2; // because it was a prefix
+ // For both HP and EXP bars, a different set of logic is used when the maxValue has
+ // fewer pixels than the whole bar; this logic ignores the toAdd parameter entirely and
+ // calculates its *own* toAdd by doing maxValue << 8 / scale. If we instead do
+ // maxValue << 9, the new toAdd becomes doubled as well.
+ battleOverlay[offset] = 0x40;
+ }
+
+ writeOverlay(romEntry.getInt("BattleOvlNumber"), battleOverlay);
+
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void updateTypeEffectiveness() {
+ try {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("BattleOvlNumber"));
+ int typeEffectivenessTableOffset = find(battleOverlay, Gen4Constants.typeEffectivenessTableLocator);
+ if (typeEffectivenessTableOffset > 0) {
+ List<TypeRelationship> typeEffectivenessTable = readTypeEffectivenessTable(battleOverlay, typeEffectivenessTableOffset);
+ log("--Updating Type Effectiveness--");
+ for (TypeRelationship relationship : typeEffectivenessTable) {
+ // Change Ghost 0.5x against Steel to Ghost 1x to Steel
+ if (relationship.attacker == Type.GHOST && relationship.defender == Type.STEEL) {
+ relationship.effectiveness = Effectiveness.NEUTRAL;
+ log("Replaced: Ghost not very effective vs Steel => Ghost neutral vs Steel");
+ }
+
+ // Change Dark 0.5x against Steel to Dark 1x to Steel
+ else if (relationship.attacker == Type.DARK && relationship.defender == Type.STEEL) {
+ relationship.effectiveness = Effectiveness.NEUTRAL;
+ log("Replaced: Dark not very effective vs Steel => Dark neutral vs Steel");
+ }
+ }
+ logBlankLine();
+ writeTypeEffectivenessTable(typeEffectivenessTable, battleOverlay, typeEffectivenessTableOffset);
+ writeOverlay(romEntry.getInt("BattleOvlNumber"), battleOverlay);
+ effectivenessUpdated = true;
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private List<TypeRelationship> readTypeEffectivenessTable(byte[] battleOverlay, int typeEffectivenessTableOffset) {
+ List<TypeRelationship> typeEffectivenessTable = new ArrayList<>();
+ int currentOffset = typeEffectivenessTableOffset;
+ int attackingType = battleOverlay[currentOffset];
+ // 0xFE marks the end of the table *not* affected by Foresight, while 0xFF marks
+ // the actual end of the table. Since we don't care about Ghost immunities at all,
+ // just stop once we reach the Foresight section.
+ while (attackingType != (byte) 0xFE) {
+ int defendingType = battleOverlay[currentOffset + 1];
+ int effectivenessInternal = battleOverlay[currentOffset + 2];
+ Type attacking = Gen4Constants.typeTable[attackingType];
+ Type defending = Gen4Constants.typeTable[defendingType];
+ Effectiveness effectiveness = null;
+ switch (effectivenessInternal) {
+ case 20:
+ effectiveness = Effectiveness.DOUBLE;
+ break;
+ case 10:
+ effectiveness = Effectiveness.NEUTRAL;
+ break;
+ case 5:
+ effectiveness = Effectiveness.HALF;
+ break;
+ case 0:
+ effectiveness = Effectiveness.ZERO;
+ break;
+ }
+ if (effectiveness != null) {
+ TypeRelationship relationship = new TypeRelationship(attacking, defending, effectiveness);
+ typeEffectivenessTable.add(relationship);
+ }
+ currentOffset += 3;
+ attackingType = battleOverlay[currentOffset];
+ }
+ return typeEffectivenessTable;
+ }
+
+ private void writeTypeEffectivenessTable(List<TypeRelationship> typeEffectivenessTable, byte[] battleOverlay,
+ int typeEffectivenessTableOffset) {
+ int currentOffset = typeEffectivenessTableOffset;
+ for (TypeRelationship relationship : typeEffectivenessTable) {
+ battleOverlay[currentOffset] = Gen4Constants.typeToByte(relationship.attacker);
+ battleOverlay[currentOffset + 1] = Gen4Constants.typeToByte(relationship.defender);
+ byte effectivenessInternal = 0;
+ switch (relationship.effectiveness) {
+ case DOUBLE:
+ effectivenessInternal = 20;
+ break;
+ case NEUTRAL:
+ effectivenessInternal = 10;
+ break;
+ case HALF:
+ effectivenessInternal = 5;
+ break;
+ case ZERO:
+ effectivenessInternal = 0;
+ break;
+ }
+ battleOverlay[currentOffset + 2] = effectivenessInternal;
+ currentOffset += 3;
+ }
+ }
+
+ private void applyFastDistortionWorld() {
+ byte[] spearPillarPortalScript = scriptNarc.files.get(Gen4Constants.ptSpearPillarPortalScriptFile);
+ byte[] expandedSpearPillarPortalScript = new byte[spearPillarPortalScript.length + 12];
+ System.arraycopy(spearPillarPortalScript, 0, expandedSpearPillarPortalScript, 0, spearPillarPortalScript.length);
+ spearPillarPortalScript = expandedSpearPillarPortalScript;
+ genericIPSPatch(spearPillarPortalScript, "FastDistortionWorldTweak");
+ scriptNarc.files.set(Gen4Constants.ptSpearPillarPortalScriptFile, spearPillarPortalScript);
+ }
+
+ private void updateRotomFormeTyping() {
+ pokes[Species.Gen4Formes.rotomH].secondaryType = Type.FIRE;
+ pokes[Species.Gen4Formes.rotomW].secondaryType = Type.WATER;
+ pokes[Species.Gen4Formes.rotomFr].secondaryType = Type.ICE;
+ pokes[Species.Gen4Formes.rotomFa].secondaryType = Type.FLYING;
+ pokes[Species.Gen4Formes.rotomM].secondaryType = Type.GRASS;
+ }
+
+ @Override
+ public void enableGuaranteedPokemonCatching() {
+ try {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("BattleOvlNumber"));
+ int offset = find(battleOverlay, Gen4Constants.perfectOddsBranchLocator);
+ if (offset > 0) {
+ // In Cmd_handleballthrow (name taken from pokeemerald decomp), the middle of the function checks
+ // if the odds of catching a Pokemon is greater than 254; if it is, then the Pokemon is automatically
+ // caught. In ASM, this is represented by:
+ // cmp r1, #0xFF
+ // bcc oddsLessThanOrEqualTo254
+ // The below code just nops these two instructions so that we *always* act like our odds are 255,
+ // and Pokemon are automatically caught no matter what.
+ battleOverlay[offset] = 0x00;
+ battleOverlay[offset + 1] = 0x00;
+ battleOverlay[offset + 2] = 0x00;
+ battleOverlay[offset + 3] = 0x00;
+ writeOverlay(romEntry.getInt("BattleOvlNumber"), battleOverlay);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public void applyCorrectStaticMusic(Map<Integer,Integer> specialMusicStaticChanges) {
+ List<Integer> replaced = new ArrayList<>();
+ String newIndexToMusicPrefix;
+ int newIndexToMusicPoolOffset;
+
+ switch(romEntry.romType) {
+ case Gen4Constants.Type_DP:
+ case Gen4Constants.Type_Plat:
+ int extendBy = romEntry.getInt("Arm9ExtensionSize");
+ arm9 = extendARM9(arm9, extendBy, romEntry.getString("TCMCopyingPrefix"), Gen4Constants.arm9Offset);
+ genericIPSPatch(arm9, "NewIndexToMusicTweak");
+
+ newIndexToMusicPrefix = romEntry.getString("NewIndexToMusicPrefix");
+ newIndexToMusicPoolOffset = find(arm9, newIndexToMusicPrefix);
+ newIndexToMusicPoolOffset += newIndexToMusicPrefix.length() / 2;
+
+ for (int oldStatic: specialMusicStaticChanges.keySet()) {
+ int i = newIndexToMusicPoolOffset;
+ int index = readWord(arm9, i);
+ while (index != oldStatic || replaced.contains(i)) {
+ i += 4;
+ index = readWord(arm9, i);
+ }
+ writeWord(arm9, i, specialMusicStaticChanges.get(oldStatic));
+ replaced.add(i);
+ }
+ break;
+ case Gen4Constants.Type_HGSS:
+ newIndexToMusicPrefix = romEntry.getString("IndexToMusicPrefix");
+ newIndexToMusicPoolOffset = find(arm9, newIndexToMusicPrefix);
+
+ if (newIndexToMusicPoolOffset > 0) {
+ newIndexToMusicPoolOffset += newIndexToMusicPrefix.length() / 2;
+
+ for (int oldStatic: specialMusicStaticChanges.keySet()) {
+ int i = newIndexToMusicPoolOffset;
+ int indexEtc = readWord(arm9, i);
+ int index = indexEtc & 0x3FF;
+ while (index != oldStatic || replaced.contains(i)) {
+ i += 2;
+ indexEtc = readWord(arm9, i);
+ index = indexEtc & 0x3FF;
+ }
+ int newIndexEtc = specialMusicStaticChanges.get(oldStatic) | (indexEtc & 0xFC00);
+ writeWord(arm9, i, newIndexEtc);
+ replaced.add(i);
+ }
+ }
+ break;
+ }
+ }
+
+ @Override
+ public boolean hasStaticMusicFix() {
+ return romEntry.tweakFiles.get("NewIndexToMusicTweak") != null || romEntry.romType == Gen4Constants.Type_HGSS;
+ }
+
+ private boolean genericIPSPatch(byte[] data, String ctName) {
+ String patchName = romEntry.tweakFiles.get(ctName);
+ if (patchName == null) {
+ return false;
+ }
+
+ try {
+ FileFunctions.applyPatch(data, patchName);
+ return true;
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private Pokemon randomPokemonLimited(int maxValue, boolean blockNonMales) {
+ checkPokemonRestrictions();
+ List<Pokemon> validPokemon = new ArrayList<>();
+ for (Pokemon pk : this.mainPokemonList) {
+ if (pk.number <= maxValue && (!blockNonMales || pk.genderRatio <= 0xFD)) {
+ validPokemon.add(pk);
+ }
+ }
+ if (validPokemon.size() == 0) {
+ return null;
+ } else {
+ return validPokemon.get(random.nextInt(validPokemon.size()));
+ }
+ }
+
+ private void computeCRC32sForRom() throws IOException {
+ this.actualOverlayCRC32s = new HashMap<>();
+ this.actualFileCRC32s = new HashMap<>();
+ this.actualArm9CRC32 = FileFunctions.getCRC32(arm9);
+ for (int overlayNumber : romEntry.overlayExpectedCRC32s.keySet()) {
+ byte[] overlay = readOverlay(overlayNumber);
+ long crc32 = FileFunctions.getCRC32(overlay);
+ this.actualOverlayCRC32s.put(overlayNumber, crc32);
+ }
+ 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() {
+ if (romEntry.arm9ExpectedCRC32 != actualArm9CRC32) {
+ System.out.println(actualArm9CRC32);
+ return false;
+ }
+
+ for (int overlayNumber : romEntry.overlayExpectedCRC32s.keySet()) {
+ long expectedCRC32 = romEntry.overlayExpectedCRC32s.get(overlayNumber);
+ long actualCRC32 = actualOverlayCRC32s.get(overlayNumber);
+ if (expectedCRC32 != actualCRC32) {
+ return false;
+ }
+ }
+
+ for (String fileKey : romEntry.files.keySet()) {
+ long expectedCRC32 = romEntry.files.get(fileKey).expectedCRC32;
+ long actualCRC32 = actualFileCRC32s.get(fileKey);
+ if (expectedCRC32 != actualCRC32) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public BufferedImage getMascotImage() {
+ try {
+ Pokemon pk = randomPokemon();
+ NARCArchive pokespritesNARC = this.readNARC(romEntry.getFile("PokemonGraphics"));
+ int spriteIndex = pk.number * 6 + 2 + random.nextInt(2);
+ int palIndex = pk.number * 6 + 4;
+ if (random.nextInt(10) == 0) {
+ // shiny
+ palIndex++;
+ }
+
+ // read sprite
+ byte[] rawSprite = pokespritesNARC.files.get(spriteIndex);
+ if (rawSprite.length == 0) {
+ // Must use other gender form
+ rawSprite = pokespritesNARC.files.get(spriteIndex ^ 1);
+ }
+ int[] spriteData = new int[3200];
+ for (int i = 0; i < 3200; i++) {
+ spriteData[i] = readWord(rawSprite, i * 2 + 48);
+ }
+
+ // Decrypt sprite (why does EVERYTHING use the RNG formula geez)
+ if (romEntry.romType != Gen4Constants.Type_DP) {
+ int key = spriteData[0];
+ for (int i = 0; i < 3200; i++) {
+ spriteData[i] ^= (key & 0xFFFF);
+ key = key * 0x41C64E6D + 0x6073;
+ }
+ } else {
+ // D/P sprites are encrypted *backwards*. Wut.
+ int key = spriteData[3199];
+ for (int i = 3199; i >= 0; i--) {
+ spriteData[i] ^= (key & 0xFFFF);
+ key = key * 0x41C64E6D + 0x6073;
+ }
+ }
+
+ byte[] rawPalette = pokespritesNARC.files.get(palIndex);
+
+ int[] palette = new int[16];
+ for (int i = 1; i < 16; i++) {
+ palette[i] = GFXFunctions.conv16BitColorToARGB(readWord(rawPalette, 40 + i * 2));
+ }
+
+ // Deliberately chop off the right half of the image while still
+ // correctly indexing the array.
+ BufferedImage bim = new BufferedImage(80, 80, BufferedImage.TYPE_INT_ARGB);
+ for (int y = 0; y < 80; y++) {
+ for (int x = 0; x < 80; x++) {
+ int value = ((spriteData[y * 40 + x / 4]) >> (x % 4) * 4) & 0x0F;
+ bim.setRGB(x, y, palette[value]);
+ }
+ }
+ return bim;
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public List<Integer> getAllConsumableHeldItems() {
+ return Gen4Constants.consumableHeldItems;
+ }
+
+ @Override
+ public List<Integer> getAllHeldItems() {
+ return Gen4Constants.allHeldItems;
+ }
+
+ @Override
+ public List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, int[] pokeMoves) {
+ List<Integer> items = new ArrayList<>();
+ items.addAll(Gen4Constants.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(Gen4Constants.generalPurposeItems);
+ }
+ for (int moveIdx : pokeMoves) {
+ Move move = moves.get(moveIdx);
+ if (move == null) {
+ continue;
+ }
+ if (move.category == MoveCategory.PHYSICAL) {
+ items.add(Items.liechiBerry);
+ if (!consumableOnly) {
+ items.addAll(Gen4Constants.typeBoostingItems.get(move.type));
+ items.add(Items.choiceBand);
+ items.add(Items.muscleBand);
+ }
+ }
+ if (move.category == MoveCategory.SPECIAL) {
+ items.add(Items.petayaBerry);
+ if (!consumableOnly) {
+ items.addAll(Gen4Constants.typeBoostingItems.get(move.type));
+ items.add(Items.wiseGlasses);
+ items.add(Items.choiceSpecs);
+ }
+ }
+ if (!consumableOnly && Gen4Constants.moveBoostingItems.containsKey(moveIdx)) {
+ items.addAll(Gen4Constants.moveBoostingItems.get(moveIdx));
+ }
+ }
+ Map<Type, Effectiveness> byType = Effectiveness.against(tp.pokemon.primaryType, tp.pokemon.secondaryType, 4, effectivenessUpdated);
+ for(Map.Entry<Type, Effectiveness> entry : byType.entrySet()) {
+ Integer berry = Gen4Constants.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));
+ }
+
+ if (!consumableOnly) {
+ if (Gen4Constants.abilityBoostingItems.containsKey(ability)) {
+ items.addAll(Gen4Constants.abilityBoostingItems.get(ability));
+ }
+ if (tp.pokemon.primaryType == Type.POISON || tp.pokemon.secondaryType == Type.POISON) {
+ items.add(Items.blackSludge);
+ }
+ List<Integer> speciesItems = Gen4Constants.speciesBoostingItems.get(tp.pokemon.number);
+ if (speciesItems != null) {
+ for (int i = 0; i < frequencyBoostCount; i++) {
+ items.addAll(speciesItems);
+ }
+ }
+ }
+ return items;
+ }
+}
diff --git a/src/com/pkrandom/romhandlers/Gen5RomHandler.java b/src/com/pkrandom/romhandlers/Gen5RomHandler.java new file mode 100755 index 0000000..02010e7 --- /dev/null +++ b/src/com/pkrandom/romhandlers/Gen5RomHandler.java @@ -0,0 +1,4343 @@ +package com.pkrandom.romhandlers;
+
+/*----------------------------------------------------------------------------*/
+/*-- Gen5RomHandler.java - randomizer handler for B/W/B2/W2. --*/
+/*-- --*/
+/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/
+/*-- Pokemon and any associated names and the like are --*/
+/*-- trademark and (C) Nintendo 1996-2020. --*/
+/*-- --*/
+/*-- The custom code written here is licensed under the terms of the GPL: --*/
+/*-- --*/
+/*-- This program is free software: you can redistribute it and/or modify --*/
+/*-- it under the terms of the GNU General Public License as published by --*/
+/*-- the Free Software Foundation, either version 3 of the License, or --*/
+/*-- (at your option) any later version. --*/
+/*-- --*/
+/*-- This program is distributed in the hope that it will be useful, --*/
+/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/
+/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/
+/*-- GNU General Public License for more details. --*/
+/*-- --*/
+/*-- You should have received a copy of the GNU General Public License --*/
+/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/
+/*----------------------------------------------------------------------------*/
+
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import com.pkrandom.*;
+import com.pkrandom.constants.*;
+import com.pkrandom.exceptions.RandomizationException;
+import com.pkrandom.pokemon.*;
+import pptxt.PPTxtHandler;
+
+import com.pkrandom.exceptions.RandomizerIOException;
+import com.pkrandom.newnds.NARCArchive;
+import compressors.DSDecmp;
+
+public class Gen5RomHandler extends AbstractDSRomHandler {
+
+ public static class Factory extends RomHandler.Factory {
+
+ @Override
+ public Gen5RomHandler create(Random random, PrintStream logStream) {
+ return new Gen5RomHandler(random, logStream);
+ }
+
+ public boolean isLoadable(String filename) {
+ return detectNDSRomInner(getROMCodeFromFile(filename), getVersionFromFile(filename));
+ }
+ }
+
+ public Gen5RomHandler(Random random) {
+ super(random, null);
+ }
+
+ public Gen5RomHandler(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 expectedCRC32;
+ }
+
+ private static class RomEntry {
+ private String name;
+ private String romCode;
+ private byte version;
+ private int romType;
+ private long arm9ExpectedCRC32;
+ private boolean staticPokemonSupport = false, copyStaticPokemon = false, copyRoamingPokemon = false,
+ copyTradeScripts = false, isBlack = false;
+ private Map<String, String> strings = new HashMap<>();
+ private Map<String, Integer> numbers = new HashMap<>();
+ private Map<String, String> tweakFiles = new HashMap<>();
+ private Map<String, int[]> arrayEntries = new HashMap<>();
+ private Map<String, OffsetWithinEntry[]> offsetArrayEntries = new HashMap<>();
+ private Map<String, RomFileEntry> files = new HashMap<>();
+ private Map<Integer, Long> overlayExpectedCRC32s = new HashMap<>();
+ private List<StaticPokemon> staticPokemon = new ArrayList<>();
+ private List<StaticPokemon> staticPokemonFakeBall = new ArrayList<>();
+ private List<RoamingPokemon> roamingPokemon = new ArrayList<>();
+ private List<TradeScript> tradeScripts = new ArrayList<>();
+
+
+ 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<RomEntry> roms;
+
+ static {
+ loadROMInfo();
+ }
+
+ private static void loadROMInfo() {
+ roms = new ArrayList<>();
+ RomEntry current = null;
+ try {
+ Scanner sc = new Scanner(FileFunctions.openConfig("gen5_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("Version")) {
+ current.version = Byte.parseByte(r[1]);
+ } else if (r[0].equals("Type")) {
+ if (r[1].equalsIgnoreCase("BW2")) {
+ current.romType = Gen5Constants.Type_BW2;
+ } else {
+ current.romType = Gen5Constants.Type_BW;
+ }
+ } else if (r[0].equals("CopyFrom")) {
+ for (RomEntry otherEntry : roms) {
+ if (r[1].equalsIgnoreCase(otherEntry.romCode)) {
+ // copy from here
+ 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);
+ if (current.copyStaticPokemon) {
+ current.staticPokemon.addAll(otherEntry.staticPokemon);
+ current.staticPokemonFakeBall.addAll(otherEntry.staticPokemonFakeBall);
+ current.staticPokemonSupport = true;
+ } else {
+ current.staticPokemonSupport = false;
+ }
+ if (current.copyTradeScripts) {
+ current.tradeScripts.addAll(otherEntry.tradeScripts);
+ }
+ if (current.copyRoamingPokemon) {
+ current.roamingPokemon.addAll(otherEntry.roamingPokemon);
+ }
+ }
+ }
+ } 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(",");
+ RomFileEntry entry = new RomFileEntry();
+ entry.path = values[0].trim();
+ entry.expectedCRC32 = parseRILong("0x" + values[1].trim());
+ current.files.put(key, entry);
+ } else if (r[0].equals("Arm9CRC32")) {
+ current.arm9ExpectedCRC32 = parseRILong("0x" + r[1]);
+ } else if (r[0].startsWith("OverlayCRC32<")) {
+ String keyString = r[0].split("<")[1].split(">")[0];
+ int key = parseRIInt(keyString);
+ long value = parseRILong("0x" + r[1]);
+ current.overlayExpectedCRC32s.put(key, value);
+ } else if (r[0].equals("StaticPokemon{}")) {
+ current.staticPokemon.add(parseStaticPokemon(r[1]));
+ } else if (r[0].equals("StaticPokemonFakeBall{}")) {
+ current.staticPokemonFakeBall.add(parseStaticPokemon(r[1]));
+ } else if (r[0].equals("RoamingPokemon{}")) {
+ current.roamingPokemon.add(parseRoamingPokemon(r[1]));
+ } else if (r[0].equals("TradeScript[]")) {
+ String[] offsets = r[1].substring(1, r[1].length() - 1).split(",");
+ int[] reqOffs = new int[offsets.length];
+ int[] givOffs = new int[offsets.length];
+ int file = 0;
+ int c = 0;
+ for (String off : offsets) {
+ String[] parts = off.split(":");
+ file = parseRIInt(parts[0]);
+ reqOffs[c] = parseRIInt(parts[1]);
+ givOffs[c++] = parseRIInt(parts[2]);
+ }
+ TradeScript ts = new TradeScript();
+ ts.fileNum = file;
+ ts.requestedOffsets = reqOffs;
+ ts.givenOffsets = givOffs;
+ current.tradeScripts.add(ts);
+ } else if (r[0].equals("StaticPokemonSupport")) {
+ int spsupport = parseRIInt(r[1]);
+ current.staticPokemonSupport = (spsupport > 0);
+ } else if (r[0].equals("CopyStaticPokemon")) {
+ int csp = parseRIInt(r[1]);
+ current.copyStaticPokemon = (csp > 0);
+ } else if (r[0].equals("CopyRoamingPokemon")) {
+ int crp = parseRIInt(r[1]);
+ current.copyRoamingPokemon = (crp > 0);
+ } else if (r[0].equals("CopyTradeScripts")) {
+ int cts = parseRIInt(r[1]);
+ current.copyTradeScripts = (cts > 0);
+ } else if (r[0].startsWith("StarterOffsets")) {
+ String[] offsets = r[1].substring(1, r[1].length() - 1).split(",");
+ OffsetWithinEntry[] offs = new OffsetWithinEntry[offsets.length];
+ int c = 0;
+ for (String off : offsets) {
+ String[] parts = off.split(":");
+ OffsetWithinEntry owe = new OffsetWithinEntry();
+ owe.entry = parseRIInt(parts[0]);
+ owe.offset = parseRIInt(parts[1]);
+ offs[c++] = owe;
+ }
+ current.offsetArrayEntries.put(r[0], offs);
+ } else if (r[0].endsWith("Tweak")) {
+ current.tweakFiles.put(r[0], r[1]);
+ } else if (r[0].equals("IsBlack")) {
+ int isBlack = parseRIInt(r[1]);
+ current.isBlack = (isBlack > 0);
+ } 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")
+ || r[0].endsWith("Size") || r[0].endsWith("Index")) {
+ 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;
+ }
+ }
+
+ private static StaticPokemon parseStaticPokemon(String staticPokemonString) {
+ StaticPokemon sp = new StaticPokemon();
+ String pattern = "[A-z]+=\\[([0-9]+:0x[0-9a-fA-F]+,?\\s?)+]";
+ Pattern r = Pattern.compile(pattern);
+ Matcher m = r.matcher(staticPokemonString);
+ while (m.find()) {
+ String[] segments = m.group().split("=");
+ String[] offsets = segments[1].substring(1, segments[1].length() - 1).split(",");
+ FileEntry[] entries = new FileEntry[offsets.length];
+ for (int i = 0; i < entries.length; i++) {
+ String[] parts = offsets[i].split(":");
+ entries[i] = new FileEntry(parseRIInt(parts[0]), parseRIInt(parts[1]));
+ }
+ switch (segments[0]) {
+ case "Species":
+ sp.speciesEntries = entries;
+ break;
+ case "Level":
+ sp.levelEntries = entries;
+ break;
+ case "Forme":
+ sp.formeEntries = entries;
+ break;
+ }
+ }
+ return sp;
+ }
+
+ private static RoamingPokemon parseRoamingPokemon(String roamingPokemonString) {
+ RoamingPokemon rp = new RoamingPokemon();
+ String pattern = "[A-z]+=\\[(0x[0-9a-fA-F]+,?\\s?)+]|[A-z]+=\\[([0-9]+:0x[0-9a-fA-F]+,?\\s?)+]";
+ Pattern r = Pattern.compile(pattern);
+ Matcher m = r.matcher(roamingPokemonString);
+ while (m.find()) {
+ String[] segments = m.group().split("=");
+ String[] offsets = segments[1].substring(1, segments[1].length() - 1).split(",");
+ switch (segments[0]) {
+ case "Species":
+ int[] speciesOverlayOffsets = new int[offsets.length];
+ for (int i = 0; i < speciesOverlayOffsets.length; i++) {
+ speciesOverlayOffsets[i] = parseRIInt(offsets[i]);
+ }
+ rp.speciesOverlayOffsets = speciesOverlayOffsets;
+ break;
+ case "Level":
+ int[] levelOverlayOffsets = new int[offsets.length];
+ for (int i = 0; i < levelOverlayOffsets.length; i++) {
+ levelOverlayOffsets[i] = parseRIInt(offsets[i]);
+ }
+ rp.levelOverlayOffsets = levelOverlayOffsets;
+ break;
+ case "Script":
+ FileEntry[] entries = new FileEntry[offsets.length];
+ for (int i = 0; i < entries.length; i++) {
+ String[] parts = offsets[i].split(":");
+ entries[i] = new FileEntry(parseRIInt(parts[0]), parseRIInt(parts[1]));
+ }
+ rp.speciesScriptOffsets = entries;
+ break;
+ }
+ }
+ return rp;
+ }
+
+ // This ROM
+ private Pokemon[] pokes;
+ private Map<Integer,FormeInfo> formeMappings = new TreeMap<>();
+ private List<Pokemon> pokemonList;
+ private List<Pokemon> pokemonListInclFormes;
+ private Move[] moves;
+ private RomEntry romEntry;
+ private byte[] arm9;
+ private List<String> abilityNames;
+ private List<String> itemNames;
+ private List<String> shopNames;
+ private boolean loadedWildMapNames;
+ private Map<Integer, String> wildMapNames;
+ private ItemList allowedItems, nonBadItems;
+ private List<Integer> regularShopItems;
+ private List<Integer> opShopItems;
+ private int hiddenHollowCount = 0;
+ private boolean hiddenHollowCounted = false;
+ private List<Integer> originalDoubleTrainers = new ArrayList<>();
+ private boolean effectivenessUpdated;
+ private int pickupItemsTableOffset;
+ private long actualArm9CRC32;
+ private Map<Integer, Long> actualOverlayCRC32s;
+ private Map<String, Long> actualFileCRC32s;
+
+ private NARCArchive pokeNarc, moveNarc, stringsNarc, storyTextNarc, scriptNarc, shopNarc;
+
+ @Override
+ protected boolean detectNDSRom(String ndsCode, byte version) {
+ return detectNDSRomInner(ndsCode, version);
+ }
+
+ private static boolean detectNDSRomInner(String ndsCode, byte version) {
+ return entryFor(ndsCode, version) != null;
+ }
+
+ private static RomEntry entryFor(String ndsCode, byte version) {
+ if (ndsCode == null) {
+ return null;
+ }
+
+ for (RomEntry re : roms) {
+ if (ndsCode.equals(re.romCode) && re.version == version) {
+ return re;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void loadedROM(String romCode, byte version) {
+ this.romEntry = entryFor(romCode, version);
+ try {
+ arm9 = readARM9();
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ try {
+ stringsNarc = readNARC(romEntry.getFile("TextStrings"));
+ storyTextNarc = readNARC(romEntry.getFile("TextStory"));
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ try {
+ scriptNarc = readNARC(romEntry.getFile("Scripts"));
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ try {
+ shopNarc = readNARC(romEntry.getFile("ShopItems"));
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+ loadPokemonStats();
+ pokemonListInclFormes = Arrays.asList(pokes);
+ pokemonList = Arrays.asList(Arrays.copyOfRange(pokes,0,Gen5Constants.pokemonCount + 1));
+ loadMoves();
+
+ abilityNames = getStrings(false, romEntry.getInt("AbilityNamesTextOffset"));
+ itemNames = getStrings(false, romEntry.getInt("ItemNamesTextOffset"));
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ shopNames = Gen5Constants.bw1ShopNames;
+ }
+ else if (romEntry.romType == Gen5Constants.Type_BW2) {
+ shopNames = Gen5Constants.bw2ShopNames;
+ }
+
+ loadedWildMapNames = false;
+
+ allowedItems = Gen5Constants.allowedItems.copy();
+ nonBadItems = Gen5Constants.getNonBadItems(romEntry.romType).copy();
+ regularShopItems = Gen5Constants.regularShopItems;
+ opShopItems = Gen5Constants.opShopItems;
+
+ try {
+ computeCRC32sForRom();
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ // If there are tweaks for expanding the ARM9, do it here to keep it simple.
+ boolean shouldExtendARM9 = romEntry.tweakFiles.containsKey("ShedinjaEvolutionTweak") || romEntry.tweakFiles.containsKey("NewIndexToMusicTweak");
+ if (shouldExtendARM9) {
+ int extendBy = romEntry.getInt("Arm9ExtensionSize");
+ arm9 = extendARM9(arm9, extendBy, romEntry.getString("TCMCopyingPrefix"), Gen5Constants.arm9Offset);
+ }
+ }
+
+ private void loadPokemonStats() {
+ try {
+ pokeNarc = this.readNARC(romEntry.getFile("PokemonStats"));
+ String[] pokeNames = readPokemonNames();
+ int formeCount = Gen5Constants.getFormeCount(romEntry.romType);
+ pokes = new Pokemon[Gen5Constants.pokemonCount + formeCount + 1];
+ for (int i = 1; i <= Gen5Constants.pokemonCount; i++) {
+ pokes[i] = new Pokemon();
+ pokes[i].number = i;
+ loadBasicPokeStats(pokes[i], pokeNarc.files.get(i), formeMappings);
+ // Name?
+ pokes[i].name = pokeNames[i];
+ }
+
+ int i = Gen5Constants.pokemonCount + 1;
+ for (int k: formeMappings.keySet()) {
+ pokes[i] = new Pokemon();
+ pokes[i].number = i;
+ loadBasicPokeStats(pokes[i], pokeNarc.files.get(k), 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].formeSpriteIndex = fi.formeSpriteOffset + Gen5Constants.pokemonCount + Gen5Constants.getNonPokemonBattleSpriteCount(romEntry.romType);
+ pokes[i].formeSuffix = Gen5Constants.getFormeSuffix(k,romEntry.romType);
+ i = i + 1;
+ }
+ populateEvolutions();
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ private void loadMoves() {
+ try {
+ moveNarc = this.readNARC(romEntry.getFile("MoveData"));
+ moves = new Move[Gen5Constants.moveCount + 1];
+ List<String> moveNames = getStrings(false, romEntry.getInt("MoveNamesTextOffset"));
+ for (int i = 1; i <= Gen5Constants.moveCount; i++) {
+ byte[] moveData = moveNarc.files.get(i);
+ moves[i] = new Move();
+ moves[i].name = moveNames.get(i);
+ moves[i].number = i;
+ moves[i].internalId = i;
+ moves[i].effectIndex = readWord(moveData, 16);
+ moves[i].hitratio = (moveData[4] & 0xFF);
+ moves[i].power = moveData[3] & 0xFF;
+ moves[i].pp = moveData[5] & 0xFF;
+ moves[i].type = Gen5Constants.typeTable[moveData[0] & 0xFF];
+ moves[i].flinchPercentChance = moveData[15] & 0xFF;
+ moves[i].target = moveData[20] & 0xFF;
+ moves[i].category = Gen5Constants.moveCategoryIndices[moveData[2] & 0xFF];
+ moves[i].priority = moveData[6];
+
+ int critStages = moveData[14] & 0xFF;
+ if (critStages == 6) {
+ moves[i].criticalChance = CriticalChance.GUARANTEED;
+ } else if (critStages > 0) {
+ moves[i].criticalChance = CriticalChance.INCREASED;
+ }
+
+ int internalStatusType = readWord(moveData, 8);
+ int flags = FileFunctions.readFullInt(moveData, 32);
+ moves[i].makesContact = (flags & 0x001) != 0;
+ moves[i].isChargeMove = (flags & 0x002) != 0;
+ moves[i].isRechargeMove = (flags & 0x004) != 0;
+ moves[i].isPunchMove = (flags & 0x080) != 0;
+ moves[i].isSoundMove = (flags & 0x100) != 0;
+ moves[i].isTrapMove = (moves[i].effectIndex == Gen5Constants.trappingEffect || internalStatusType == 8);
+ int qualities = moveData[1];
+ int recoilOrAbsorbPercent = moveData[18];
+ if (qualities == Gen5Constants.damageAbsorbQuality) {
+ moves[i].absorbPercent = recoilOrAbsorbPercent;
+ } else {
+ moves[i].recoilPercent = -recoilOrAbsorbPercent;
+ }
+
+ 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
+ }
+
+ switch (qualities) {
+ case Gen5Constants.noDamageStatChangeQuality:
+ case Gen5Constants.noDamageStatusAndStatChangeQuality:
+ // All Allies or Self
+ if (moves[i].target == 6 || moves[i].target == 7) {
+ moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER;
+ } else {
+ moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET;
+ }
+ break;
+ case Gen5Constants.damageTargetDebuffQuality:
+ moves[i].statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET;
+ break;
+ case Gen5Constants.damageUserBuffQuality:
+ moves[i].statChangeMoveType = StatChangeMoveType.DAMAGE_USER;
+ break;
+ default:
+ moves[i].statChangeMoveType = StatChangeMoveType.NONE_OR_UNKNOWN;
+ break;
+ }
+
+ for (int statChange = 0; statChange < 3; statChange++) {
+ moves[i].statChanges[statChange].type = StatChangeType.values()[moveData[21 + statChange]];
+ moves[i].statChanges[statChange].stages = moveData[24 + statChange];
+ moves[i].statChanges[statChange].percentChance = moveData[27 + statChange];
+ }
+
+ // Exclude status types that aren't in the StatusType enum.
+ if (internalStatusType < 7) {
+ moves[i].statusType = StatusType.values()[internalStatusType];
+ if (moves[i].statusType == StatusType.POISON && (i == Moves.toxic || i == Moves.poisonFang)) {
+ moves[i].statusType = StatusType.TOXIC_POISON;
+ }
+ moves[i].statusPercentChance = moveData[10] & 0xFF;
+ if (moves[i].number == Moves.chatter) {
+ moves[i].statusPercentChance = 1.0;
+ }
+ switch (qualities) {
+ case Gen5Constants.noDamageStatusQuality:
+ case Gen5Constants.noDamageStatusAndStatChangeQuality:
+ moves[i].statusMoveType = StatusMoveType.NO_DAMAGE;
+ break;
+ case Gen5Constants.damageStatusQuality:
+ moves[i].statusMoveType = StatusMoveType.DAMAGE;
+ break;
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ private void loadBasicPokeStats(Pokemon pkmn, byte[] stats, Map<Integer,FormeInfo> altFormes) {
+ pkmn.hp = stats[Gen5Constants.bsHPOffset] & 0xFF;
+ pkmn.attack = stats[Gen5Constants.bsAttackOffset] & 0xFF;
+ pkmn.defense = stats[Gen5Constants.bsDefenseOffset] & 0xFF;
+ pkmn.speed = stats[Gen5Constants.bsSpeedOffset] & 0xFF;
+ pkmn.spatk = stats[Gen5Constants.bsSpAtkOffset] & 0xFF;
+ pkmn.spdef = stats[Gen5Constants.bsSpDefOffset] & 0xFF;
+ // Type
+ pkmn.primaryType = Gen5Constants.typeTable[stats[Gen5Constants.bsPrimaryTypeOffset] & 0xFF];
+ pkmn.secondaryType = Gen5Constants.typeTable[stats[Gen5Constants.bsSecondaryTypeOffset] & 0xFF];
+ // Only one type?
+ if (pkmn.secondaryType == pkmn.primaryType) {
+ pkmn.secondaryType = null;
+ }
+ pkmn.catchRate = stats[Gen5Constants.bsCatchRateOffset] & 0xFF;
+ pkmn.growthCurve = ExpCurve.fromByte(stats[Gen5Constants.bsGrowthCurveOffset]);
+
+ pkmn.ability1 = stats[Gen5Constants.bsAbility1Offset] & 0xFF;
+ pkmn.ability2 = stats[Gen5Constants.bsAbility2Offset] & 0xFF;
+ pkmn.ability3 = stats[Gen5Constants.bsAbility3Offset] & 0xFF;
+
+ // Held Items?
+ int item1 = readWord(stats, Gen5Constants.bsCommonHeldItemOffset);
+ int item2 = readWord(stats, Gen5Constants.bsRareHeldItemOffset);
+
+ if (item1 == item2) {
+ // guaranteed
+ pkmn.guaranteedHeldItem = item1;
+ pkmn.commonHeldItem = 0;
+ pkmn.rareHeldItem = 0;
+ pkmn.darkGrassHeldItem = 0;
+ } else {
+ pkmn.guaranteedHeldItem = 0;
+ pkmn.commonHeldItem = item1;
+ pkmn.rareHeldItem = item2;
+ pkmn.darkGrassHeldItem = readWord(stats, Gen5Constants.bsDarkGrassHeldItemOffset);
+ }
+
+ int formeCount = stats[Gen5Constants.bsFormeCountOffset] & 0xFF;
+ if (formeCount > 1) {
+ int firstFormeOffset = readWord(stats, Gen5Constants.bsFormeOffset);
+ if (firstFormeOffset != 0) {
+ for (int i = 1; i < formeCount; i++) {
+ altFormes.put(firstFormeOffset + i - 1,new FormeInfo(pkmn.number,i,readWord(stats,Gen5Constants.bsFormeSpriteOffset))); // Assumes that formes are in memory in the same order as their numbers
+ if (pkmn.number == Species.keldeo) {
+ pkmn.cosmeticForms = formeCount;
+ }
+ }
+ } else {
+ if (pkmn.number != Species.cherrim && pkmn.number != Species.arceus && pkmn.number != Species.deerling && pkmn.number != Species.sawsbuck && pkmn.number < Species.genesect) {
+ // Reason for exclusions:
+ // Cherrim/Arceus/Genesect: to avoid confusion
+ // Deerling/Sawsbuck: handled automatically in gen 5
+ pkmn.cosmeticForms = formeCount;
+ }
+ if (pkmn.number == Species.Gen5Formes.keldeoCosmetic1) {
+ pkmn.actuallyCosmetic = true;
+ }
+ }
+ }
+ }
+
+ private String[] readPokemonNames() {
+ String[] pokeNames = new String[Gen5Constants.pokemonCount + 1];
+ List<String> nameList = getStrings(false, romEntry.getInt("PokemonNamesTextOffset"));
+ for (int i = 1; i <= Gen5Constants.pokemonCount; i++) {
+ pokeNames[i] = nameList.get(i);
+ }
+ return pokeNames;
+ }
+
+ @Override
+ protected void savingROM() {
+ savePokemonStats();
+ saveMoves();
+ try {
+ writeARM9(arm9);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ try {
+ writeNARC(romEntry.getFile("TextStrings"), stringsNarc);
+ writeNARC(romEntry.getFile("TextStory"), storyTextNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ try {
+ writeNARC(romEntry.getFile("Scripts"), scriptNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void saveMoves() {
+ for (int i = 1; i <= Gen5Constants.moveCount; i++) {
+ byte[] data = moveNarc.files.get(i);
+ data[2] = Gen5Constants.moveCategoryToByte(moves[i].category);
+ data[3] = (byte) moves[i].power;
+ data[0] = Gen5Constants.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 {
+ this.writeNARC(romEntry.getFile("MoveData"), moveNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ private void savePokemonStats() {
+ List<String> nameList = getStrings(false, romEntry.getInt("PokemonNamesTextOffset"));
+
+ int formeCount = Gen5Constants.getFormeCount(romEntry.romType);
+ int formeOffset = Gen5Constants.getFormeOffset(romEntry.romType);
+ for (int i = 1; i <= Gen5Constants.pokemonCount + formeCount; i++) {
+ if (i > Gen5Constants.pokemonCount) {
+ saveBasicPokeStats(pokes[i], pokeNarc.files.get(i + formeOffset));
+ continue;
+ }
+ saveBasicPokeStats(pokes[i], pokeNarc.files.get(i));
+ nameList.set(i, pokes[i].name);
+ }
+
+ setStrings(false, romEntry.getInt("PokemonNamesTextOffset"), nameList);
+
+ try {
+ this.writeNARC(romEntry.getFile("PokemonStats"), pokeNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ writeEvolutions();
+ }
+
+ private void saveBasicPokeStats(Pokemon pkmn, byte[] stats) {
+ stats[Gen5Constants.bsHPOffset] = (byte) pkmn.hp;
+ stats[Gen5Constants.bsAttackOffset] = (byte) pkmn.attack;
+ stats[Gen5Constants.bsDefenseOffset] = (byte) pkmn.defense;
+ stats[Gen5Constants.bsSpeedOffset] = (byte) pkmn.speed;
+ stats[Gen5Constants.bsSpAtkOffset] = (byte) pkmn.spatk;
+ stats[Gen5Constants.bsSpDefOffset] = (byte) pkmn.spdef;
+ stats[Gen5Constants.bsPrimaryTypeOffset] = Gen5Constants.typeToByte(pkmn.primaryType);
+ if (pkmn.secondaryType == null) {
+ stats[Gen5Constants.bsSecondaryTypeOffset] = stats[Gen5Constants.bsPrimaryTypeOffset];
+ } else {
+ stats[Gen5Constants.bsSecondaryTypeOffset] = Gen5Constants.typeToByte(pkmn.secondaryType);
+ }
+ stats[Gen5Constants.bsCatchRateOffset] = (byte) pkmn.catchRate;
+ stats[Gen5Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte();
+
+ stats[Gen5Constants.bsAbility1Offset] = (byte) pkmn.ability1;
+ stats[Gen5Constants.bsAbility2Offset] = (byte) pkmn.ability2;
+ stats[Gen5Constants.bsAbility3Offset] = (byte) pkmn.ability3;
+
+ // Held items
+ if (pkmn.guaranteedHeldItem > 0) {
+ writeWord(stats, Gen5Constants.bsCommonHeldItemOffset, pkmn.guaranteedHeldItem);
+ writeWord(stats, Gen5Constants.bsRareHeldItemOffset, pkmn.guaranteedHeldItem);
+ writeWord(stats, Gen5Constants.bsDarkGrassHeldItemOffset, 0);
+ } else {
+ writeWord(stats, Gen5Constants.bsCommonHeldItemOffset, pkmn.commonHeldItem);
+ writeWord(stats, Gen5Constants.bsRareHeldItemOffset, pkmn.rareHeldItem);
+ writeWord(stats, Gen5Constants.bsDarkGrassHeldItemOffset, pkmn.darkGrassHeldItem);
+ }
+ }
+
+ @Override
+ public List<Pokemon> getPokemon() {
+ return pokemonList;
+ }
+
+ @Override
+ public List<Pokemon> getPokemonInclFormes() {
+ return pokemonListInclFormes;
+ }
+
+ @Override
+ public List<Pokemon> getAltFormes() {
+ int formeCount = Gen5Constants.getFormeCount(romEntry.romType);
+ return pokemonListInclFormes.subList(Gen5Constants.pokemonCount + 1, Gen5Constants.pokemonCount + formeCount + 1);
+ }
+
+ @Override
+ public List<MegaEvolution> getMegaEvolutions() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public Pokemon getAltFormeOfPokemon(Pokemon pk, int forme) {
+ int pokeNum = Gen5Constants.getAbsolutePokeNumByBaseForme(pk.number,forme);
+ return pokeNum != 0 ? pokes[pokeNum] : pk;
+ }
+
+ @Override
+ public List<Pokemon> getIrregularFormes() {
+ return Gen5Constants.getIrregularFormes(romEntry.romType).stream().map(i -> pokes[i]).collect(Collectors.toList());
+ }
+
+ @Override
+ public boolean hasFunctionalFormes() {
+ return true;
+ }
+
+ @Override
+ public List<Pokemon> getStarters() {
+ NARCArchive scriptNARC = scriptNarc;
+ List<Pokemon> starters = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ OffsetWithinEntry[] thisStarter = romEntry.offsetArrayEntries.get("StarterOffsets" + (i + 1));
+ starters.add(pokes[readWord(scriptNARC.files.get(thisStarter[0].entry), thisStarter[0].offset)]);
+ }
+ return starters;
+ }
+
+ @Override
+ public boolean setStarters(List<Pokemon> newStarters) {
+ if (newStarters.size() != 3) {
+ return false;
+ }
+
+ // Fix up starter offsets
+ try {
+ NARCArchive scriptNARC = scriptNarc;
+ for (int i = 0; i < 3; i++) {
+ int starter = newStarters.get(i).number;
+ OffsetWithinEntry[] thisStarter = romEntry.offsetArrayEntries.get("StarterOffsets" + (i + 1));
+ for (OffsetWithinEntry entry : thisStarter) {
+ writeWord(scriptNARC.files.get(entry.entry), entry.offset, starter);
+ }
+ }
+ // GIVE ME BACK MY PURRLOIN
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ byte[] newScript = Gen5Constants.bw2NewStarterScript;
+ byte[] oldFile = scriptNARC.files.get(romEntry.getInt("PokedexGivenFileOffset"));
+ byte[] newFile = new byte[oldFile.length + newScript.length];
+ int offset = find(oldFile, Gen5Constants.bw2StarterScriptMagic);
+ if (offset > 0) {
+ System.arraycopy(oldFile, 0, newFile, 0, oldFile.length);
+ System.arraycopy(newScript, 0, newFile, oldFile.length, newScript.length);
+ if (romEntry.romCode.charAt(3) == 'J') {
+ newFile[oldFile.length + 0x6] -= 4;
+ }
+ newFile[offset++] = 0x1E;
+ newFile[offset++] = 0x0;
+ writeRelativePointer(newFile, offset, oldFile.length);
+ scriptNARC.files.set(romEntry.getInt("PokedexGivenFileOffset"), newFile);
+ }
+ } else {
+ byte[] newScript = Gen5Constants.bw1NewStarterScript;
+
+ byte[] oldFile = scriptNARC.files.get(romEntry.getInt("PokedexGivenFileOffset"));
+ byte[] newFile = new byte[oldFile.length + newScript.length];
+ int offset = find(oldFile, Gen5Constants.bw1StarterScriptMagic);
+ if (offset > 0) {
+ System.arraycopy(oldFile, 0, newFile, 0, oldFile.length);
+ System.arraycopy(newScript, 0, newFile, oldFile.length, newScript.length);
+ if (romEntry.romCode.charAt(3) == 'J') {
+ newFile[oldFile.length + 0x4] -= 4;
+ newFile[oldFile.length + 0x8] -= 4;
+ }
+ newFile[offset++] = 0x04;
+ newFile[offset++] = 0x0;
+ writeRelativePointer(newFile, offset, oldFile.length);
+ scriptNARC.files.set(romEntry.getInt("PokedexGivenFileOffset"), newFile);
+ }
+ }
+
+ // Starter sprites
+ NARCArchive starterNARC = this.readNARC(romEntry.getFile("StarterGraphics"));
+ NARCArchive pokespritesNARC = this.readNARC(romEntry.getFile("PokemonGraphics"));
+ replaceStarterFiles(starterNARC, pokespritesNARC, 0, newStarters.get(0).number);
+ replaceStarterFiles(starterNARC, pokespritesNARC, 1, newStarters.get(1).number);
+ replaceStarterFiles(starterNARC, pokespritesNARC, 2, newStarters.get(2).number);
+ writeNARC(romEntry.getFile("StarterGraphics"), starterNARC);
+
+ // Starter cries
+ byte[] starterCryOverlay = this.readOverlay(romEntry.getInt("StarterCryOvlNumber"));
+ String starterCryTablePrefix = romEntry.getString("StarterCryTablePrefix");
+ int offset = find(starterCryOverlay, starterCryTablePrefix);
+ if (offset > 0) {
+ offset += starterCryTablePrefix.length() / 2; // because it was a prefix
+ for (Pokemon newStarter : newStarters) {
+ writeWord(starterCryOverlay, offset, newStarter.number);
+ offset += 2;
+ }
+ this.writeOverlay(romEntry.getInt("StarterCryOvlNumber"), starterCryOverlay);
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ // Fix text depending on version
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ List<String> yourHouseStrings = getStrings(true, romEntry.getInt("StarterLocationTextOffset"));
+ for (int i = 0; i < 3; i++) {
+ yourHouseStrings.set(Gen5Constants.bw1StarterTextOffset - i,
+ "\\xF000\\xBD02\\x0000The " + newStarters.get(i).primaryType.camelCase()
+ + "-type Pok\\x00E9mon\\xFFFE\\xF000\\xBD02\\x0000" + newStarters.get(i).name);
+ }
+ // Update what the friends say
+ yourHouseStrings
+ .set(Gen5Constants.bw1CherenText1Offset,
+ "Cheren: Hey, how come you get to pick\\xFFFEout my Pok\\x00E9mon?"
+ + "\\xF000\\xBE01\\x0000\\xFFFEOh, never mind. I wanted this one\\xFFFEfrom the start, anyway."
+ + "\\xF000\\xBE01\\x0000");
+ yourHouseStrings.set(Gen5Constants.bw1CherenText2Offset,
+ "It's decided. You'll be my opponent...\\xFFFEin our first Pok\\x00E9mon battle!"
+ + "\\xF000\\xBE01\\x0000\\xFFFELet's see what you can do, \\xFFFEmy Pok\\x00E9mon!"
+ + "\\xF000\\xBE01\\x0000");
+
+ // rewrite
+ setStrings(true, romEntry.getInt("StarterLocationTextOffset"), yourHouseStrings);
+ } else {
+ List<String> starterTownStrings = getStrings(true, romEntry.getInt("StarterLocationTextOffset"));
+ for (int i = 0; i < 3; i++) {
+ starterTownStrings.set(Gen5Constants.bw2StarterTextOffset - i, "\\xF000\\xBD02\\x0000The "
+ + newStarters.get(i).primaryType.camelCase()
+ + "-type Pok\\x00E9mon\\xFFFE\\xF000\\xBD02\\x0000" + newStarters.get(i).name);
+ }
+ // Update what the rival says
+ starterTownStrings.set(Gen5Constants.bw2RivalTextOffset,
+ "\\xF000\\x0100\\x0001\\x0001: Let's see how good\\xFFFEa Trainer you are!"
+ + "\\xF000\\xBE01\\x0000\\xFFFEI'll use my Pok\\x00E9mon"
+ + "\\xFFFEthat I raised from an Egg!\\xF000\\xBE01\\x0000");
+
+ // rewrite
+ setStrings(true, romEntry.getInt("StarterLocationTextOffset"), starterTownStrings);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean hasStarterAltFormes() {
+ return false;
+ }
+
+ @Override
+ public int starterCount() {
+ return 3;
+ }
+
+ @Override
+ public Map<Integer, StatChange> getUpdatedPokemonStats(int generation) {
+ return GlobalConstants.getStatChanges(generation);
+ }
+
+ @Override
+ public boolean supportsStarterHeldItems() {
+ return false;
+ }
+
+ @Override
+ public List<Integer> getStarterHeldItems() {
+ // do nothing
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setStarterHeldItems(List<Integer> items) {
+ // do nothing
+ }
+
+ private void replaceStarterFiles(NARCArchive starterNARC, NARCArchive pokespritesNARC, int starterIndex,
+ int pokeNumber) {
+ starterNARC.files.set(starterIndex * 2, pokespritesNARC.files.get(pokeNumber * 20 + 18));
+ // Get the picture...
+ byte[] compressedPic = pokespritesNARC.files.get(pokeNumber * 20);
+ // Decompress it with JavaDSDecmp
+ byte[] uncompressedPic = DSDecmp.Decompress(compressedPic);
+ starterNARC.files.set(12 + starterIndex, uncompressedPic);
+ }
+
+ @Override
+ public List<Move> getMoves() {
+ return Arrays.asList(moves);
+ }
+
+ @Override
+ public List<EncounterSet> getEncounters(boolean useTimeOfDay) {
+ if (!loadedWildMapNames) {
+ loadWildMapNames();
+ }
+ try {
+ NARCArchive encounterNARC = readNARC(romEntry.getFile("WildPokemon"));
+ List<EncounterSet> encounters = new ArrayList<>();
+ int idx = -1;
+ for (byte[] entry : encounterNARC.files) {
+ idx++;
+ if (entry.length > Gen5Constants.perSeasonEncounterDataLength && useTimeOfDay) {
+ for (int i = 0; i < 4; i++) {
+ processEncounterEntry(encounters, entry, i * Gen5Constants.perSeasonEncounterDataLength, idx);
+ }
+ } else {
+ processEncounterEntry(encounters, entry, 0, idx);
+ }
+ }
+ return encounters;
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void processEncounterEntry(List<EncounterSet> encounters, byte[] entry, int startOffset, int idx) {
+
+ if (!wildMapNames.containsKey(idx)) {
+ wildMapNames.put(idx, "? Unknown ?");
+ }
+ String mapName = wildMapNames.get(idx);
+
+ int[] amounts = Gen5Constants.encountersOfEachType;
+
+ int offset = 8;
+ for (int i = 0; i < 7; i++) {
+ int rate = entry[startOffset + i] & 0xFF;
+ if (rate != 0) {
+ List<Encounter> encs = readEncounters(entry, startOffset + offset, amounts[i]);
+ EncounterSet area = new EncounterSet();
+ area.rate = rate;
+ area.encounters = encs;
+ area.offset = idx;
+ area.displayName = mapName + " " + Gen5Constants.encounterTypeNames[i];
+ encounters.add(area);
+ }
+ offset += amounts[i] * 4;
+ }
+
+ }
+
+ private List<Encounter> readEncounters(byte[] data, int offset, int number) {
+ List<Encounter> encs = new ArrayList<>();
+ for (int i = 0; i < number; i++) {
+ Encounter enc1 = new Encounter();
+ int species = readWord(data, offset + i * 4) & 0x7FF;
+ int forme = readWord(data, offset + i * 4) >> 11;
+ Pokemon baseForme = pokes[species];
+ if (forme <= baseForme.cosmeticForms || forme == 30 || forme == 31) {
+ enc1.pokemon = pokes[species];
+ } else {
+ int speciesWithForme = Gen5Constants.getAbsolutePokeNumByBaseForme(species,forme);
+ if (speciesWithForme == 0) {
+ enc1.pokemon = pokes[species]; // Failsafe
+ } else {
+ enc1.pokemon = pokes[speciesWithForme];
+ }
+ }
+ enc1.formeNumber = forme;
+ enc1.level = data[offset + 2 + i * 4] & 0xFF;
+ enc1.maxLevel = data[offset + 3 + i * 4] & 0xFF;
+ encs.add(enc1);
+ }
+ return encs;
+ }
+
+ @Override
+ public void setEncounters(boolean useTimeOfDay, List<EncounterSet> encountersList) {
+ try {
+ NARCArchive encounterNARC = readNARC(romEntry.getFile("WildPokemon"));
+ Iterator<EncounterSet> encounters = encountersList.iterator();
+ for (byte[] entry : encounterNARC.files) {
+ writeEncounterEntry(encounters, entry, 0);
+ if (entry.length > 232) {
+ if (useTimeOfDay) {
+ for (int i = 1; i < 4; i++) {
+ writeEncounterEntry(encounters, entry, i * 232);
+ }
+ } else {
+ // copy for other 3 seasons
+ System.arraycopy(entry, 0, entry, 232, 232);
+ System.arraycopy(entry, 0, entry, 464, 232);
+ System.arraycopy(entry, 0, entry, 696, 232);
+ }
+ }
+ }
+
+ // Save
+ writeNARC(romEntry.getFile("WildPokemon"), encounterNARC);
+
+ this.updatePokedexAreaData(encounterNARC);
+
+ // Habitat List
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ // disabled: habitat list changes cause a crash if too many
+ // entries for now.
+
+ // NARCArchive habitatNARC = readNARC(romEntry.getFile("HabitatList"));
+ // for (int i = 0; i < habitatNARC.files.size(); i++) {
+ // byte[] oldEntry = habitatNARC.files.get(i);
+ // int[] encounterFiles = habitatListEntries[i];
+ // Map<Pokemon, byte[]> pokemonHere = new TreeMap<Pokemon,
+ // byte[]>();
+ // for (int encFile : encounterFiles) {
+ // byte[] encEntry = encounterNARC.files.get(encFile);
+ // if (encEntry.length > 232) {
+ // for (int s = 0; s < 4; s++) {
+ // addHabitats(encEntry, s * 232, pokemonHere, s);
+ // }
+ // } else {
+ // for (int s = 0; s < 4; s++) {
+ // addHabitats(encEntry, 0, pokemonHere, s);
+ // }
+ // }
+ // }
+ // // Make the new file
+ // byte[] habitatEntry = new byte[10 + pokemonHere.size() * 28];
+ // System.arraycopy(oldEntry, 0, habitatEntry, 0, 10);
+ // habitatEntry[8] = (byte) pokemonHere.size();
+ // // 28-byte entries for each pokemon
+ // int num = -1;
+ // for (Pokemon pkmn : pokemonHere.keySet()) {
+ // num++;
+ // writeWord(habitatEntry, 10 + num * 28, pkmn.number);
+ // byte[] slots = pokemonHere.get(pkmn);
+ // System.arraycopy(slots, 0, habitatEntry, 12 + num * 28,
+ // 12);
+ // }
+ // // Save
+ // habitatNARC.files.set(i, habitatEntry);
+ // }
+ // // Save habitat
+ // this.writeNARC(romEntry.getFile("HabitatList"),
+ // habitatNARC);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ private void updatePokedexAreaData(NARCArchive encounterNARC) throws IOException {
+ NARCArchive areaNARC = this.readNARC(romEntry.getFile("PokedexAreaData"));
+ int areaDataEntryLength = Gen5Constants.getAreaDataEntryLength(romEntry.romType);
+ int encounterAreaCount = Gen5Constants.getEncounterAreaCount(romEntry.romType);
+ List<byte[]> newFiles = new ArrayList<>();
+ for (int i = 0; i < Gen5Constants.pokemonCount; i++) {
+ byte[] nf = new byte[areaDataEntryLength];
+ newFiles.add(nf);
+ }
+ // Get data now
+ for (int i = 0; i < encounterNARC.files.size(); i++) {
+ byte[] encEntry = encounterNARC.files.get(i);
+ if (encEntry.length > Gen5Constants.perSeasonEncounterDataLength) {
+ for (int season = 0; season < 4; season++) {
+ updateAreaDataFromEncounterEntry(encEntry, season * Gen5Constants.perSeasonEncounterDataLength, newFiles, season, i);
+ }
+ } else {
+ for (int season = 0; season < 4; season++) {
+ updateAreaDataFromEncounterEntry(encEntry, 0, newFiles, season, i);
+ }
+ }
+ }
+ // Now update unobtainables, check for seasonal-dependent entries, & save
+ for (int i = 0; i < Gen5Constants.pokemonCount; i++) {
+ byte[] file = newFiles.get(i);
+ for (int season = 0; season < 4; season++) {
+ boolean unobtainable = true;
+ for (int enc = 0; enc < encounterAreaCount; enc++) {
+ if (file[season * (encounterAreaCount + 1) + enc + 2] != 0) {
+ unobtainable = false;
+ break;
+ }
+ }
+ if (unobtainable) {
+ file[season * (encounterAreaCount + 1) + 1] = 1;
+ }
+ }
+ boolean seasonalDependent = false;
+ for (int enc = 0; enc < encounterAreaCount; enc++) {
+ byte springEnc = file[enc + 2];
+ byte summerEnc = file[(encounterAreaCount + 1) + enc + 2];
+ byte autumnEnc = file[2 * (encounterAreaCount + 1) + enc + 2];
+ byte winterEnc = file[3 * (encounterAreaCount + 1) + enc + 2];
+ boolean allSeasonsAreTheSame = springEnc == summerEnc && springEnc == autumnEnc && springEnc == winterEnc;
+ if (!allSeasonsAreTheSame) {
+ seasonalDependent = true;
+ break;
+ }
+ }
+ if (!seasonalDependent) {
+ file[0] = 1;
+ }
+ areaNARC.files.set(i, file);
+ }
+ // Save
+ this.writeNARC(romEntry.getFile("PokedexAreaData"), areaNARC);
+ }
+
+ private void updateAreaDataFromEncounterEntry(byte[] entry, int startOffset, List<byte[]> areaData, int season, int fileNumber) {
+ int[] amounts = Gen5Constants.encountersOfEachType;
+ int encounterAreaCount = Gen5Constants.getEncounterAreaCount(romEntry.romType);
+ int[] wildFileToAreaMap = Gen5Constants.getWildFileToAreaMap(romEntry.romType);
+
+ int offset = 8;
+ for (int i = 0; i < 7; i++) {
+ int rate = entry[startOffset + i] & 0xFF;
+ if (rate != 0) {
+ for (int e = 0; e < amounts[i]; e++) {
+ Pokemon pkmn = pokes[((entry[startOffset + offset + e * 4] & 0xFF) + ((entry[startOffset + offset
+ + 1 + e * 4] & 0x03) << 8))];
+ byte[] pokeFile = areaData.get(pkmn.getBaseNumber() - 1);
+ int areaIndex = wildFileToAreaMap[fileNumber];
+ // Route 4?
+ if (romEntry.romType == Gen5Constants.Type_BW2 && areaIndex == Gen5Constants.bw2Route4AreaIndex) {
+ if ((fileNumber == Gen5Constants.b2Route4EncounterFile && romEntry.romCode.charAt(2) == 'D')
+ || (fileNumber == Gen5Constants.w2Route4EncounterFile && romEntry.romCode.charAt(2) == 'E')) {
+ areaIndex = -1; // wrong version
+ }
+ }
+ // Victory Road?
+ if (romEntry.romType == Gen5Constants.Type_BW2 && areaIndex == Gen5Constants.bw2VictoryRoadAreaIndex) {
+ if (romEntry.romCode.charAt(2) == 'D') {
+ // White 2
+ if (fileNumber == Gen5Constants.b2VRExclusiveRoom1
+ || fileNumber == Gen5Constants.b2VRExclusiveRoom2) {
+ areaIndex = -1; // wrong version
+ }
+ } else {
+ // Black 2
+ if (fileNumber == Gen5Constants.w2VRExclusiveRoom1
+ || fileNumber == Gen5Constants.w2VRExclusiveRoom2) {
+ areaIndex = -1; // wrong version
+ }
+ }
+ }
+ // Reversal Mountain?
+ if (romEntry.romType == Gen5Constants.Type_BW2 && areaIndex == Gen5Constants.bw2ReversalMountainAreaIndex) {
+ if (romEntry.romCode.charAt(2) == 'D') {
+ // White 2
+ if (fileNumber >= Gen5Constants.b2ReversalMountainStart
+ && fileNumber <= Gen5Constants.b2ReversalMountainEnd) {
+ areaIndex = -1; // wrong version
+ }
+ } else {
+ // Black 2
+ if (fileNumber >= Gen5Constants.w2ReversalMountainStart
+ && fileNumber <= Gen5Constants.w2ReversalMountainEnd) {
+ areaIndex = -1; // wrong version
+ }
+ }
+ }
+ // Skip stuff that isn't on the map or is wrong version
+ if (areaIndex != -1) {
+ pokeFile[season * (encounterAreaCount + 1) + 2 + areaIndex] |= (1 << i);
+ }
+ }
+ }
+ offset += amounts[i] * 4;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private void addHabitats(byte[] entry, int startOffset, Map<Pokemon, byte[]> pokemonHere, int season) {
+ int[] amounts = Gen5Constants.encountersOfEachType;
+ int[] type = Gen5Constants.habitatClassificationOfEachType;
+
+ int offset = 8;
+ for (int i = 0; i < 7; i++) {
+ int rate = entry[startOffset + i] & 0xFF;
+ if (rate != 0) {
+ for (int e = 0; e < amounts[i]; e++) {
+ Pokemon pkmn = pokes[((entry[startOffset + offset + e * 4] & 0xFF) + ((entry[startOffset + offset
+ + 1 + e * 4] & 0x03) << 8))];
+ if (pokemonHere.containsKey(pkmn)) {
+ pokemonHere.get(pkmn)[type[i] + season * 3] = 1;
+ } else {
+ byte[] locs = new byte[12];
+ locs[type[i] + season * 3] = 1;
+ pokemonHere.put(pkmn, locs);
+ }
+ }
+ }
+ offset += amounts[i] * 4;
+ }
+ }
+
+ private void writeEncounterEntry(Iterator<EncounterSet> encounters, byte[] entry, int startOffset) {
+ int[] amounts = Gen5Constants.encountersOfEachType;
+
+ int offset = 8;
+ for (int i = 0; i < 7; i++) {
+ int rate = entry[startOffset + i] & 0xFF;
+ if (rate != 0) {
+ EncounterSet area = encounters.next();
+ for (int j = 0; j < amounts[i]; j++) {
+ Encounter enc = area.encounters.get(j);
+ int speciesAndFormeData = (enc.formeNumber << 11) + enc.pokemon.getBaseNumber();
+ writeWord(entry, startOffset + offset + j * 4, speciesAndFormeData);
+ entry[startOffset + offset + j * 4 + 2] = (byte) enc.level;
+ entry[startOffset + offset + j * 4 + 3] = (byte) enc.maxLevel;
+ }
+ }
+ offset += amounts[i] * 4;
+ }
+ }
+
+ private void loadWildMapNames() {
+ try {
+ wildMapNames = new HashMap<>();
+ byte[] mapHeaderData = this.readNARC(romEntry.getFile("MapTableFile")).files.get(0);
+ int numMapHeaders = mapHeaderData.length / 48;
+ List<String> allMapNames = getStrings(false, romEntry.getInt("MapNamesTextOffset"));
+ for (int map = 0; map < numMapHeaders; map++) {
+ int baseOffset = map * 48;
+ int mapNameIndex = mapHeaderData[baseOffset + 26] & 0xFF;
+ String mapName = allMapNames.get(mapNameIndex);
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ int wildSet = mapHeaderData[baseOffset + 20] & 0xFF;
+ if (wildSet != 255) {
+ wildMapNames.put(wildSet, mapName);
+ }
+ } else {
+ int wildSet = readWord(mapHeaderData, baseOffset + 20);
+ if (wildSet != 65535) {
+ wildMapNames.put(wildSet, mapName);
+ }
+ }
+ }
+ loadedWildMapNames = true;
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ @Override
+ public List<Trainer> getTrainers() {
+ List<Trainer> allTrainers = new ArrayList<>();
+ try {
+ NARCArchive trainers = this.readNARC(romEntry.getFile("TrainerData"));
+ NARCArchive trpokes = this.readNARC(romEntry.getFile("TrainerPokemon"));
+ int trainernum = trainers.files.size();
+ List<String> tclasses = this.getTrainerClassNames();
+ List<String> tnames = this.getTrainerNames();
+ for (int i = 1; i < trainernum; i++) {
+ // Trainer entries are 20 bytes
+ // Team flags; 1 byte; 0x01 = custom moves, 0x02 = held item
+ // Class; 1 byte
+ // Battle Mode; 1 byte; 0=single, 1=double, 2=triple, 3=rotation
+ // Number of pokemon in team; 1 byte
+ // Items; 2 bytes each, 4 item slots
+ // AI Flags; 2 byte
+ // 2 bytes not used
+ // Healer; 1 byte; 0x01 means they will heal player's pokes after defeat.
+ // 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 (e.g. berries)
+ byte[] trainer = trainers.files.get(i);
+ byte[] trpoke = trpokes.files.get(i);
+ Trainer tr = new Trainer();
+ tr.poketype = trainer[0] & 0xFF;
+ tr.index = i;
+ tr.trainerclass = trainer[1] & 0xFF;
+ int numPokes = trainer[3] & 0xFF;
+ int pokeOffs = 0;
+ tr.fullDisplayName = tclasses.get(tr.trainerclass) + " " + tnames.get(i - 1);
+ if (trainer[2] == 1) {
+ originalDoubleTrainers.add(i);
+ }
+ for (int poke = 0; poke < numPokes; poke++) {
+ // Structure is
+ // IV SB LV LV SP SP FRM FRM
+ // (HI HI)
+ // (M1 M1 M2 M2 M3 M3 M4 M4)
+ // where SB = 0 0 Ab Ab 0 0 Fm Ml
+ // IV is a "difficulty" level between 0 and 255 to represent 0 to 31 IVs.
+ // These IVs affect all attributes. For the vanilla games, the
+ // vast majority of trainers have 0 IVs; Elite Four members will
+ // have 30 IVs.
+ // 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 difficulty = trpoke[pokeOffs] & 0xFF;
+ 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.IVs = (difficulty) * 31 / 255;
+ int abilityAndFlag = trpoke[pokeOffs + 1];
+ tpk.abilitySlot = (abilityAndFlag >>> 4) & 0xF;
+ tpk.forcedGenderFlag = (abilityAndFlag & 0xF);
+ tpk.forme = formnum;
+ tpk.formeSuffix = Gen5Constants.getFormeSuffixByBaseForme(species,formnum);
+ pokeOffs += 8;
+ if (tr.pokemonHaveItems()) {
+ tpk.heldItem = readWord(trpoke, pokeOffs);
+ pokeOffs += 2;
+ }
+ if (tr.pokemonHaveCustomMoves()) {
+ for (int move = 0; move < 4; move++) {
+ tpk.moves[move] = readWord(trpoke, pokeOffs + (move*2));
+ }
+ pokeOffs += 8;
+ }
+ tr.pokemon.add(tpk);
+ }
+ allTrainers.add(tr);
+ }
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ Gen5Constants.tagTrainersBW(allTrainers);
+ Gen5Constants.setMultiBattleStatusBW(allTrainers);
+ } else {
+ if (!romEntry.getFile("DriftveilPokemon").isEmpty()) {
+ NARCArchive driftveil = this.readNARC(romEntry.getFile("DriftveilPokemon"));
+ int currentFile = 1;
+ for (int trno = 0; trno < 17; trno++) {
+ Trainer tr = new Trainer();
+ tr.poketype = 3; // have held items and custom moves
+ int nameAndClassIndex = Gen5Constants.bw2DriftveilTrainerOffsets.get(trno);
+ tr.fullDisplayName = tclasses.get(Gen5Constants.normalTrainerClassLength + nameAndClassIndex) + " " + tnames.get(Gen5Constants.normalTrainerNameLength + nameAndClassIndex);
+ tr.requiresUniqueHeldItems = true;
+ int pokemonNum = 6;
+ if (trno < 2) {
+ pokemonNum = 3;
+ }
+ for (int poke = 0; poke < pokemonNum; poke++) {
+ byte[] pkmndata = driftveil.files.get(currentFile);
+ int species = readWord(pkmndata, 0);
+ TrainerPokemon tpk = new TrainerPokemon();
+ tpk.level = 25;
+ tpk.pokemon = pokes[species];
+ tpk.IVs = 31;
+ tpk.heldItem = readWord(pkmndata, 12);
+ for (int move = 0; move < 4; move++) {
+ tpk.moves[move] = readWord(pkmndata, 2 + (move*2));
+ }
+ tr.pokemon.add(tpk);
+ currentFile++;
+ }
+ allTrainers.add(tr);
+ }
+ }
+ boolean isBlack2 = romEntry.romCode.startsWith("IRE");
+ Gen5Constants.tagTrainersBW2(allTrainers);
+ Gen5Constants.setMultiBattleStatusBW2(allTrainers, isBlack2);
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ return allTrainers;
+ }
+
+ @Override
+ public List<Integer> getMainPlaythroughTrainers() {
+ if (romEntry.romType == Gen5Constants.Type_BW) { // BW1
+ return Gen5Constants.bw1MainPlaythroughTrainers;
+ }
+ else if (romEntry.romType == Gen5Constants.Type_BW2) { // BW2
+ return Gen5Constants.bw2MainPlaythroughTrainers;
+ }
+ else {
+ return Gen5Constants.emptyPlaythroughTrainers;
+ }
+ }
+
+ @Override
+ public List<Integer> getEliteFourTrainers(boolean isChallengeMode) {
+ if (isChallengeMode) {
+ return Arrays.stream(romEntry.arrayEntries.get("ChallengeModeEliteFourIndices")).boxed().collect(Collectors.toList());
+ } else {
+ return Arrays.stream(romEntry.arrayEntries.get("EliteFourIndices")).boxed().collect(Collectors.toList());
+ }
+ }
+
+
+ @Override
+ public List<Integer> getEvolutionItems() {
+ return Gen5Constants.evolutionItems;
+ }
+
+ @Override
+ public void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode) {
+ Iterator<Trainer> allTrainers = trainerData.iterator();
+ try {
+ NARCArchive trainers = this.readNARC(romEntry.getFile("TrainerData"));
+ NARCArchive trpokes = new NARCArchive();
+ // Get current movesets in case we need to reset them for certain
+ // trainer mons.
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+ // empty entry
+ trpokes.files.add(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 });
+ int trainernum = trainers.files.size();
+ for (int i = 1; i < trainernum; i++) {
+ byte[] trainer = trainers.files.get(i);
+ Trainer tr = allTrainers.next();
+ // preserve original poketype for held item & moves
+ trainer[0] = (byte) tr.poketype;
+ int numPokes = tr.pokemon.size();
+ trainer[3] = (byte) numPokes;
+
+ if (doubleBattleMode) {
+ if (!tr.skipImportant()) {
+ if (trainer[2] == 0) {
+ trainer[2] = 1;
+ trainer[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<TrainerPokemon> tpokes = tr.pokemon.iterator();
+ for (int poke = 0; poke < numPokes; poke++) {
+ TrainerPokemon tp = tpokes.next();
+ // Add 1 to offset integer division truncation
+ int difficulty = Math.min(255, 1 + (tp.IVs * 255) / 31);
+ byte abilityAndFlag = (byte)((tp.abilitySlot << 4) | tp.forcedGenderFlag);
+ writeWord(trpoke, pokeOffs, difficulty | abilityAndFlag << 8);
+ writeWord(trpoke, pokeOffs + 2, tp.level);
+ writeWord(trpoke, pokeOffs + 4, tp.pokemon.number);
+ writeWord(trpoke, pokeOffs + 6, tp.forme);
+ // no form info, so no byte 6/7
+ pokeOffs += 8;
+ if (tr.pokemonHaveItems()) {
+ writeWord(trpoke, pokeOffs, tp.heldItem);
+ pokeOffs += 2;
+ }
+ if (tr.pokemonHaveCustomMoves()) {
+ if (tp.resetMoves) {
+ int[] pokeMoves = RomFunctions.getMovesAtLevel(getAltFormeOfPokemon(tp.pokemon, tp.forme).number, movesets, tp.level);
+ for (int m = 0; m < 4; m++) {
+ writeWord(trpoke, pokeOffs + m * 2, pokeMoves[m]);
+ }
+ } else {
+ writeWord(trpoke, pokeOffs, tp.moves[0]);
+ writeWord(trpoke, pokeOffs + 2, tp.moves[1]);
+ writeWord(trpoke, pokeOffs + 4, tp.moves[2]);
+ writeWord(trpoke, pokeOffs + 6, tp.moves[3]);
+ }
+ pokeOffs += 8;
+ }
+ }
+ trpokes.files.add(trpoke);
+ }
+ this.writeNARC(romEntry.getFile("TrainerData"), trainers);
+ this.writeNARC(romEntry.getFile("TrainerPokemon"), trpokes);
+
+ if (doubleBattleMode) {
+
+ NARCArchive trainerTextBoxes = readNARC(romEntry.getFile("TrainerTextBoxes"));
+ byte[] data = trainerTextBoxes.files.get(0);
+ for (int i = 0; i < data.length; i += 4) {
+ int trainerIndex = readWord(data, i);
+ if (originalDoubleTrainers.contains(trainerIndex)) {
+ int textBoxIndex = readWord(data, i+2);
+ if (textBoxIndex == 3) {
+ writeWord(data, i+2, 0);
+ } else if (textBoxIndex == 5) {
+ writeWord(data, i+2, 2);
+ } else if (textBoxIndex == 6) {
+ writeWord(data, i+2, 0x18);
+ }
+ }
+ }
+
+ trainerTextBoxes.files.set(0, data);
+ writeNARC(romEntry.getFile("TrainerTextBoxes"), trainerTextBoxes);
+
+
+ try {
+ byte[] fieldOverlay = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ String trainerOverworldTextBoxPrefix = romEntry.getString("TrainerOverworldTextBoxPrefix");
+ int offset = find(fieldOverlay, trainerOverworldTextBoxPrefix);
+ if (offset > 0) {
+ offset += trainerOverworldTextBoxPrefix.length() / 2; // because it was a prefix
+ // Overwrite text box values for trainer 1 in a doubles pair to use the same as a single trainer
+ fieldOverlay[offset-2] = 0;
+ fieldOverlay[offset] = 2;
+ fieldOverlay[offset+2] = 0x18;
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ String doubleBattleLimitPrefix = romEntry.getString("DoubleBattleLimitPrefix");
+ offset = find(fieldOverlay, doubleBattleLimitPrefix);
+ if (offset > 0) {
+ offset += trainerOverworldTextBoxPrefix.length() / 2; // because it was a prefix
+ // No limit for doubles trainers, i.e. they will spot you even if you have a single Pokemon
+ writeWord(fieldOverlay, offset, 0x46C0); // nop
+ writeWord(fieldOverlay, offset+2, 0x46C0); // nop
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ String doubleBattleGetPointerPrefix = romEntry.getString("DoubleBattleGetPointerPrefix");
+ int beqToSingleTrainer = romEntry.getInt("BeqToSingleTrainerNumber");
+ offset = find(fieldOverlay, doubleBattleGetPointerPrefix);
+ if (offset > 0) {
+ offset += trainerOverworldTextBoxPrefix.length() / 2; // because it was a prefix
+ // Move some instructions up
+ writeWord(fieldOverlay, offset + 0x10, readWord(fieldOverlay, offset + 0xE));
+ writeWord(fieldOverlay, offset + 0xE, readWord(fieldOverlay, offset + 0xC));
+ writeWord(fieldOverlay, offset + 0xC, readWord(fieldOverlay, offset + 0xA));
+ // Add a beq and cmp to go to the "single trainer" case if a certain pointer is 0
+ writeWord(fieldOverlay, offset + 0xA, beqToSingleTrainer);
+ writeWord(fieldOverlay, offset + 8, 0x2800);
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ writeOverlay(romEntry.getInt("FieldOvlNumber"), fieldOverlay);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ String textBoxChoicePrefix = romEntry.getString("TextBoxChoicePrefix");
+ int offset = find(arm9,textBoxChoicePrefix);
+
+ if (offset > 0) {
+ // Change a branch destination in order to only check the relevant trainer instead of checking
+ // every trainer in the game (will result in incorrect text boxes when being spotted by doubles
+ // pairs, but this is better than the game freezing for half a second and getting a blank text box)
+ offset += textBoxChoicePrefix.length() / 2;
+ arm9[offset-4] = 2;
+ } else {
+ throw new RandomizationException("Double Battle Mode not supported for this game");
+ }
+
+ }
+
+ // Deal with PWT
+ if (romEntry.romType == Gen5Constants.Type_BW2 && !romEntry.getFile("DriftveilPokemon").isEmpty()) {
+ NARCArchive driftveil = this.readNARC(romEntry.getFile("DriftveilPokemon"));
+ int currentFile = 1;
+ for (int trno = 0; trno < 17; trno++) {
+ Trainer tr = allTrainers.next();
+ Iterator<TrainerPokemon> tpks = tr.pokemon.iterator();
+ int pokemonNum = 6;
+ if (trno < 2) {
+ pokemonNum = 3;
+ }
+ for (int poke = 0; poke < pokemonNum; poke++) {
+ byte[] pkmndata = driftveil.files.get(currentFile);
+ TrainerPokemon tp = tpks.next();
+ // pokemon and held item
+ writeWord(pkmndata, 0, tp.pokemon.number);
+ writeWord(pkmndata, 12, tp.heldItem);
+ // handle moves
+ if (tp.resetMoves) {
+ int[] pokeMoves = RomFunctions.getMovesAtLevel(tp.pokemon.number, movesets, tp.level);
+ for (int m = 0; m < 4; m++) {
+ writeWord(pkmndata, 2 + m * 2, pokeMoves[m]);
+ }
+ } else {
+ writeWord(pkmndata, 2, tp.moves[0]);
+ writeWord(pkmndata, 4, tp.moves[1]);
+ writeWord(pkmndata, 6, tp.moves[2]);
+ writeWord(pkmndata, 8, tp.moves[3]);
+ }
+ currentFile++;
+ }
+ }
+ this.writeNARC(romEntry.getFile("DriftveilPokemon"), driftveil);
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ }
+
+ @Override
+ public Map<Integer, List<MoveLearnt>> getMovesLearnt() {
+ Map<Integer, List<MoveLearnt>> movesets = new TreeMap<>();
+ try {
+ NARCArchive movesLearnt = this.readNARC(romEntry.getFile("PokemonMovesets"));
+ int formeCount = Gen5Constants.getFormeCount(romEntry.romType);
+ int formeOffset = Gen5Constants.getFormeOffset(romEntry.romType);
+ for (int i = 1; i <= Gen5Constants.pokemonCount + formeCount; i++) {
+ Pokemon pkmn = pokes[i];
+ byte[] movedata;
+ if (i > Gen5Constants.pokemonCount) {
+ movedata = movesLearnt.files.get(i + formeOffset);
+ } else {
+ movedata = movesLearnt.files.get(i);
+ }
+ int moveDataLoc = 0;
+ List<MoveLearnt> 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<Integer, List<MoveLearnt>> movesets) {
+ try {
+ NARCArchive movesLearnt = readNARC(romEntry.getFile("PokemonMovesets"));
+ int formeCount = Gen5Constants.getFormeCount(romEntry.romType);
+ int formeOffset = Gen5Constants.getFormeOffset(romEntry.romType);
+ for (int i = 1; i <= Gen5Constants.pokemonCount + formeCount; i++) {
+ Pokemon pkmn = pokes[i];
+ List<MoveLearnt> 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);
+ }
+ }
+ // Save
+ this.writeNARC(romEntry.getFile("PokemonMovesets"), movesLearnt);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ }
+
+ @Override
+ public Map<Integer, List<Integer>> getEggMoves() {
+ Map<Integer, List<Integer>> eggMoves = new TreeMap<>();
+ try {
+ NARCArchive eggMovesNarc = this.readNARC(romEntry.getFile("EggMoves"));
+ for (int i = 1; i <= Gen5Constants.pokemonCount; i++) {
+ Pokemon pkmn = pokes[i];
+ byte[] movedata = eggMovesNarc.files.get(i);
+ int numberOfEggMoves = readWord(movedata, 0);
+ List<Integer> moves = new ArrayList<>();
+ for (int j = 0; j < numberOfEggMoves; j++) {
+ int move = readWord(movedata, 2 + (j * 2));
+ moves.add(move);
+ }
+ eggMoves.put(pkmn.number, moves);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return eggMoves;
+ }
+
+ @Override
+ public void setEggMoves(Map<Integer, List<Integer>> eggMoves) {
+ try {
+ NARCArchive eggMovesNarc = this.readNARC(romEntry.getFile("EggMoves"));
+ for (int i = 1; i <= Gen5Constants.pokemonCount; i++) {
+ Pokemon pkmn = pokes[i];
+ byte[] movedata = eggMovesNarc.files.get(i);
+ List<Integer> moves = eggMoves.get(pkmn.number);
+ for (int j = 0; j < moves.size(); j++) {
+ writeWord(movedata, 2 + (j * 2), moves.get(j));
+ }
+ }
+ // Save
+ this.writeNARC(romEntry.getFile("EggMoves"), eggMovesNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private static class FileEntry {
+ private int file;
+ private int offset;
+
+ public FileEntry(int file, int offset) {
+ this.file = file;
+ this.offset = offset;
+ }
+ }
+
+ private static class StaticPokemon {
+ private FileEntry[] speciesEntries;
+ private FileEntry[] formeEntries;
+ private FileEntry[] levelEntries;
+
+ public StaticPokemon() {
+ this.speciesEntries = new FileEntry[0];
+ this.formeEntries = new FileEntry[0];
+ this.levelEntries = new FileEntry[0];
+ }
+
+ public Pokemon getPokemon(Gen5RomHandler parent, NARCArchive scriptNARC) {
+ return parent.pokes[parent.readWord(scriptNARC.files.get(speciesEntries[0].file), speciesEntries[0].offset)];
+ }
+
+ public void setPokemon(Gen5RomHandler parent, NARCArchive scriptNARC, Pokemon pkmn) {
+ int value = pkmn.number;
+ for (int i = 0; i < speciesEntries.length; i++) {
+ byte[] file = scriptNARC.files.get(speciesEntries[i].file);
+ parent.writeWord(file, speciesEntries[i].offset, value);
+ }
+ }
+
+ public int getForme(NARCArchive scriptNARC) {
+ if (formeEntries.length == 0) {
+ return 0;
+ }
+ byte[] file = scriptNARC.files.get(formeEntries[0].file);
+ return file[formeEntries[0].offset];
+ }
+
+ public void setForme(NARCArchive scriptNARC, int forme) {
+ for (int i = 0; i < formeEntries.length; i++) {
+ byte[] file = scriptNARC.files.get(formeEntries[i].file);
+ file[formeEntries[i].offset] = (byte) forme;
+ }
+ }
+
+ public int getLevelCount() {
+ return levelEntries.length;
+ }
+
+ public int getLevel(NARCArchive scriptOrMapNARC, int i) {
+ if (levelEntries.length <= i) {
+ return 1;
+ }
+ byte[] file = scriptOrMapNARC.files.get(levelEntries[i].file);
+ return file[levelEntries[i].offset];
+ }
+
+ public void setLevel(NARCArchive scriptOrMapNARC, int level, int i) {
+ if (levelEntries.length > i) { // Might not have a level entry e.g., it's an egg
+ byte[] file = scriptOrMapNARC.files.get(levelEntries[i].file);
+ file[levelEntries[i].offset] = (byte) level;
+ }
+ }
+ }
+
+ private static class RoamingPokemon {
+ private int[] speciesOverlayOffsets;
+ private int[] levelOverlayOffsets;
+ private FileEntry[] speciesScriptOffsets;
+
+ public RoamingPokemon() {
+ this.speciesOverlayOffsets = new int[0];
+ this.levelOverlayOffsets = new int[0];
+ this.speciesScriptOffsets = new FileEntry[0];
+ }
+
+ public Pokemon getPokemon(Gen5RomHandler parent) throws IOException {
+ byte[] overlay = parent.readOverlay(parent.romEntry.getInt("RoamerOvlNumber"));
+ int species = parent.readWord(overlay, speciesOverlayOffsets[0]);
+ return parent.pokes[species];
+ }
+
+ public void setPokemon(Gen5RomHandler parent, NARCArchive scriptNARC, Pokemon pkmn) throws IOException {
+ int value = pkmn.number;
+ byte[] overlay = parent.readOverlay(parent.romEntry.getInt("RoamerOvlNumber"));
+ for (int speciesOverlayOffset : speciesOverlayOffsets) {
+ parent.writeWord(overlay, speciesOverlayOffset, value);
+ }
+ parent.writeOverlay(parent.romEntry.getInt("RoamerOvlNumber"), overlay);
+ for (FileEntry speciesScriptOffset : speciesScriptOffsets) {
+ byte[] file = scriptNARC.files.get(speciesScriptOffset.file);
+ parent.writeWord(file, speciesScriptOffset.offset, value);
+ }
+ }
+
+ public int getLevel(Gen5RomHandler parent) throws IOException {
+ if (levelOverlayOffsets.length == 0) {
+ return 1;
+ }
+ byte[] overlay = parent.readOverlay(parent.romEntry.getInt("RoamerOvlNumber"));
+ return overlay[levelOverlayOffsets[0]];
+ }
+
+ public void setLevel(Gen5RomHandler parent, int level) throws IOException {
+ byte[] overlay = parent.readOverlay(parent.romEntry.getInt("RoamerOvlNumber"));
+ for (int levelOverlayOffset : levelOverlayOffsets) {
+ overlay[levelOverlayOffset] = (byte) level;
+ }
+ parent.writeOverlay(parent.romEntry.getInt("RoamerOvlNumber"), overlay);
+ }
+ }
+
+ private static class TradeScript {
+ private int fileNum;
+ private int[] requestedOffsets;
+ private int[] givenOffsets;
+
+ public void setPokemon(Gen5RomHandler parent, NARCArchive scriptNARC, Pokemon requested, Pokemon given) {
+ int req = requested.number;
+ int giv = given.number;
+ for (int i = 0; i < requestedOffsets.length; i++) {
+ byte[] file = scriptNARC.files.get(fileNum);
+ parent.writeWord(file, requestedOffsets[i], req);
+ parent.writeWord(file, givenOffsets[i], giv);
+ }
+ }
+ }
+
+ @Override
+ public boolean canChangeStaticPokemon() {
+ return romEntry.staticPokemonSupport;
+ }
+
+ @Override
+ public boolean hasStaticAltFormes() {
+ return false;
+ }
+
+ @Override
+ public boolean hasMainGameLegendaries() {
+ return true;
+ }
+
+ @Override
+ public List<Integer> getMainGameLegendaries() {
+ return Arrays.stream(romEntry.arrayEntries.get("MainGameLegendaries")).boxed().collect(Collectors.toList());
+ }
+
+ @Override
+ public List<Integer> getSpecialMusicStatics() {
+ return Arrays.stream(romEntry.arrayEntries.get("SpecialMusicStatics")).boxed().collect(Collectors.toList());
+ }
+
+ @Override
+ public void applyCorrectStaticMusic(Map<Integer, Integer> specialMusicStaticChanges) {
+
+ try {
+ byte[] fieldOverlay = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ genericIPSPatch(fieldOverlay, "NewIndexToMusicOvlTweak");
+ writeOverlay(romEntry.getInt("FieldOvlNumber"), fieldOverlay);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ // Relies on arm9 already being extended, which it *should* have been in loadedROM
+ genericIPSPatch(arm9, "NewIndexToMusicTweak");
+
+ String newIndexToMusicPrefix = romEntry.getString("NewIndexToMusicPrefix");
+ int newIndexToMusicPoolOffset = find(arm9, newIndexToMusicPrefix);
+ newIndexToMusicPoolOffset += newIndexToMusicPrefix.length() / 2;
+
+ List<Integer> replaced = new ArrayList<>();
+ int iMax = -1;
+
+ switch(romEntry.romType) {
+ case Gen5Constants.Type_BW:
+ for (int oldStatic: specialMusicStaticChanges.keySet()) {
+ int i = newIndexToMusicPoolOffset;
+ int index = readWord(arm9, i);
+ while (index != oldStatic || replaced.contains(i)) {
+ i += 4;
+ index = readWord(arm9, i);
+ }
+ writeWord(arm9, i, specialMusicStaticChanges.get(oldStatic));
+ replaced.add(i);
+ if (i > iMax) iMax = i;
+ }
+ break;
+ case Gen5Constants.Type_BW2:
+ for (int oldStatic: specialMusicStaticChanges.keySet()) {
+ int i = newIndexToMusicPoolOffset;
+ int index = readWord(arm9, i);
+ while (index != oldStatic || replaced.contains(i)) {
+ i += 4;
+ index = readWord(arm9, i);
+ }
+ // Special Kyurem-B/W handling
+ if (index > Gen5Constants.pokemonCount) {
+ writeWord(arm9, i - 0xFE, 0);
+ writeWord(arm9, i - 0xFC, 0);
+ writeWord(arm9, i - 0xFA, 0);
+ writeWord(arm9, i - 0xF8, 0x4290);
+ }
+ writeWord(arm9, i, specialMusicStaticChanges.get(oldStatic));
+ replaced.add(i);
+ if (i > iMax) iMax = i;
+ }
+ break;
+ }
+
+ List<Integer> specialMusicStatics = getSpecialMusicStatics();
+
+ for (int i = newIndexToMusicPoolOffset; i <= iMax; i+= 4) {
+ if (!replaced.contains(i)) {
+ int pkID = readWord(arm9, i);
+
+ // If a Pokemon is a "special music static" but the music hasn't been replaced, leave as is
+ // Otherwise zero it out, because the original static encounter doesn't exist
+ if (!specialMusicStatics.contains(pkID)) {
+ writeWord(arm9, i, 0);
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public boolean hasStaticMusicFix() {
+ return romEntry.tweakFiles.get("NewIndexToMusicTweak") != null;
+ }
+
+ @Override
+ public List<TotemPokemon> getTotemPokemon() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setTotemPokemon(List<TotemPokemon> totemPokemon) {
+
+ }
+
+ @Override
+ public List<StaticEncounter> getStaticPokemon() {
+ List<StaticEncounter> sp = new ArrayList<>();
+ if (!romEntry.staticPokemonSupport) {
+ return sp;
+ }
+ int[] staticEggOffsets = new int[0];
+ if (romEntry.arrayEntries.containsKey("StaticEggPokemonOffsets")) {
+ staticEggOffsets = romEntry.arrayEntries.get("StaticEggPokemonOffsets");
+ }
+
+ // Regular static encounters
+ NARCArchive scriptNARC = scriptNarc;
+ for (int i = 0; i < romEntry.staticPokemon.size(); i++) {
+ int currentOffset = i;
+ StaticPokemon statP = romEntry.staticPokemon.get(i);
+ StaticEncounter se = new StaticEncounter();
+ Pokemon newPK = statP.getPokemon(this, scriptNARC);
+ newPK = getAltFormeOfPokemon(newPK, statP.getForme(scriptNARC));
+ se.pkmn = newPK;
+ se.level = statP.getLevel(scriptNARC, 0);
+ se.isEgg = Arrays.stream(staticEggOffsets).anyMatch(x-> x == currentOffset);
+ for (int levelEntry = 1; levelEntry < statP.getLevelCount(); levelEntry++) {
+ StaticEncounter linkedStatic = new StaticEncounter();
+ linkedStatic.pkmn = newPK;
+ linkedStatic.level = statP.getLevel(scriptNARC, levelEntry);
+ se.linkedEncounters.add(linkedStatic);
+ }
+ sp.add(se);
+ }
+
+ // Foongus/Amoongus fake ball encounters
+ try {
+ NARCArchive mapNARC = readNARC(romEntry.getFile("MapFiles"));
+ for (int i = 0; i < romEntry.staticPokemonFakeBall.size(); i++) {
+ StaticPokemon statP = romEntry.staticPokemonFakeBall.get(i);
+ StaticEncounter se = new StaticEncounter();
+ Pokemon newPK = statP.getPokemon(this, scriptNARC);
+ se.pkmn = newPK;
+ se.level = statP.getLevel(mapNARC, 0);
+ for (int levelEntry = 1; levelEntry < statP.getLevelCount(); levelEntry++) {
+ StaticEncounter linkedStatic = new StaticEncounter();
+ linkedStatic.pkmn = newPK;
+ linkedStatic.level = statP.getLevel(mapNARC, levelEntry);
+ se.linkedEncounters.add(linkedStatic);
+ }
+ sp.add(se);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ // BW2 hidden grotto encounters
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ List<Pokemon> allowedHiddenHollowPokemon = new ArrayList<>();
+ allowedHiddenHollowPokemon.addAll(Arrays.asList(Arrays.copyOfRange(pokes,1,494)));
+ allowedHiddenHollowPokemon.addAll(
+ Gen5Constants.bw2HiddenHollowUnovaPokemon.stream().map(i -> pokes[i]).collect(Collectors.toList()));
+
+ try {
+ NARCArchive hhNARC = this.readNARC(romEntry.getFile("HiddenHollows"));
+ for (byte[] hhEntry : hhNARC.files) {
+ for (int version = 0; version < 2; version++) {
+ if (version != romEntry.getInt("HiddenHollowIndex")) continue;
+ for (int raritySlot = 0; raritySlot < 3; raritySlot++) {
+ List<StaticEncounter> encountersInGroup = new ArrayList<>();
+ for (int group = 0; group < 4; group++) {
+ StaticEncounter se = new StaticEncounter();
+ Pokemon newPK = pokes[readWord(hhEntry, version * 78 + raritySlot * 26 + group * 2)];
+ newPK = getAltFormeOfPokemon(newPK, hhEntry[version * 78 + raritySlot * 26 + 20 + group]);
+ se.pkmn = newPK;
+ se.level = hhEntry[version * 78 + raritySlot * 26 + 12 + group];
+ se.maxLevel = hhEntry[version * 78 + raritySlot * 26 + 8 + group];
+ se.isEgg = false;
+ se.restrictedPool = true;
+ se.restrictedList = allowedHiddenHollowPokemon;
+ boolean originalEncounter = true;
+ for (StaticEncounter encounterInGroup: encountersInGroup) {
+ if (encounterInGroup.pkmn.equals(se.pkmn)) {
+ encounterInGroup.linkedEncounters.add(se);
+ originalEncounter = false;
+ break;
+ }
+ }
+ if (originalEncounter) {
+ encountersInGroup.add(se);
+ sp.add(se);
+ if (!hiddenHollowCounted) {
+ hiddenHollowCount++;
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+ hiddenHollowCounted = true;
+
+ // Roaming encounters
+ if (romEntry.roamingPokemon.size() > 0) {
+ try {
+ int firstSpeciesOffset = romEntry.roamingPokemon.get(0).speciesOverlayOffsets[0];
+ byte[] overlay = readOverlay(romEntry.getInt("RoamerOvlNumber"));
+ if (readWord(overlay, firstSpeciesOffset) > pokes.length) {
+ // In the original code, this is "mov r0, #0x2", which read as a word is
+ // 0x2002, much larger than the number of species in the game.
+ applyBlackWhiteRoamerPatch();
+ }
+ for (int i = 0; i < romEntry.roamingPokemon.size(); i++) {
+ RoamingPokemon roamer = romEntry.roamingPokemon.get(i);
+ StaticEncounter se = new StaticEncounter();
+ se.pkmn = roamer.getPokemon(this);
+ se.level = roamer.getLevel(this);
+ sp.add(se);
+ }
+ } catch (Exception e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ return sp;
+ }
+
+ @Override
+ public boolean setStaticPokemon(List<StaticEncounter> staticPokemon) {
+ if (!romEntry.staticPokemonSupport) {
+ return false;
+ }
+ if (staticPokemon.size() != (romEntry.staticPokemon.size() + romEntry.staticPokemonFakeBall.size() + hiddenHollowCount + romEntry.roamingPokemon.size())) {
+ return false;
+ }
+ Iterator<StaticEncounter> statics = staticPokemon.iterator();
+
+ // Regular static encounters
+ NARCArchive scriptNARC = scriptNarc;
+ for (StaticPokemon statP : romEntry.staticPokemon) {
+ StaticEncounter se = statics.next();
+ statP.setPokemon(this, scriptNARC, se.pkmn);
+ statP.setForme(scriptNARC, se.pkmn.formeNumber);
+ statP.setLevel(scriptNARC, se.level, 0);
+ for (int i = 0; i < se.linkedEncounters.size(); i++) {
+ StaticEncounter linkedStatic = se.linkedEncounters.get(i);
+ statP.setLevel(scriptNARC, linkedStatic.level, i + 1);
+ }
+ }
+
+ // Foongus/Amoongus fake ball encounters
+ try {
+ NARCArchive mapNARC = readNARC(romEntry.getFile("MapFiles"));
+ for (StaticPokemon statP : romEntry.staticPokemonFakeBall) {
+ StaticEncounter se = statics.next();
+ statP.setPokemon(this, scriptNARC, se.pkmn);
+ statP.setLevel(mapNARC, se.level, 0);
+ for (int i = 0; i < se.linkedEncounters.size(); i++) {
+ StaticEncounter linkedStatic = se.linkedEncounters.get(i);
+ statP.setLevel(mapNARC, linkedStatic.level, i + 1);
+ }
+ }
+ this.writeNARC(romEntry.getFile("MapFiles"), mapNARC);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ // BW2 hidden grotto encounters
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ try {
+ NARCArchive hhNARC = this.readNARC(romEntry.getFile("HiddenHollows"));
+ for (byte[] hhEntry : hhNARC.files) {
+ for (int version = 0; version < 2; version++) {
+ if (version != romEntry.getInt("HiddenHollowIndex")) continue;
+ for (int raritySlot = 0; raritySlot < 3; raritySlot++) {
+ for (int group = 0; group < 4; group++) {
+ StaticEncounter se = statics.next();
+ writeWord(hhEntry, version * 78 + raritySlot * 26 + group * 2, se.pkmn.number);
+ int genderRatio = this.random.nextInt(101);
+ hhEntry[version * 78 + raritySlot * 26 + 16 + group] = (byte) genderRatio;
+ hhEntry[version * 78 + raritySlot * 26 + 20 + group] = (byte) se.forme; // forme
+ hhEntry[version * 78 + raritySlot * 26 + 12 + group] = (byte) se.level;
+ hhEntry[version * 78 + raritySlot * 26 + 8 + group] = (byte) se.maxLevel;
+ for (int i = 0; i < se.linkedEncounters.size(); i++) {
+ StaticEncounter linkedStatic = se.linkedEncounters.get(i);
+ group++;
+ writeWord(hhEntry, version * 78 + raritySlot * 26 + group * 2, linkedStatic.pkmn.number);
+ hhEntry[version * 78 + raritySlot * 26 + 16 + group] = (byte) genderRatio;
+ hhEntry[version * 78 + raritySlot * 26 + 20 + group] = (byte) linkedStatic.forme; // forme
+ hhEntry[version * 78 + raritySlot * 26 + 12 + group] = (byte) linkedStatic.level;
+ hhEntry[version * 78 + raritySlot * 26 + 8 + group] = (byte) linkedStatic.maxLevel;
+ }
+ }
+ }
+ }
+ }
+ this.writeNARC(romEntry.getFile("HiddenHollows"), hhNARC);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ // Roaming encounters
+ try {
+ for (int i = 0; i < romEntry.roamingPokemon.size(); i++) {
+ RoamingPokemon roamer = romEntry.roamingPokemon.get(i);
+ StaticEncounter roamerEncounter = statics.next();
+ roamer.setPokemon(this, scriptNarc, roamerEncounter.pkmn);
+ roamer.setLevel(this, roamerEncounter.level);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+
+ // In Black/White, the game has multiple hardcoded checks for Reshiram/Zekrom's species
+ // ID in order to properly move it out of a box and into the first slot of the player's
+ // party. We need to replace these checks with the species ID of whatever occupies
+ // Reshiram/Zekrom's static encounter for the game to still function properly.
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ int boxLegendaryIndex = romEntry.getInt("BoxLegendaryOffset");
+ try {
+ int boxLegendarySpecies = staticPokemon.get(boxLegendaryIndex).pkmn.number;
+ fixBoxLegendaryBW1(boxLegendarySpecies);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ return true;
+ }
+
+ private void fixBoxLegendaryBW1(int boxLegendarySpecies) throws IOException {
+ byte[] boxLegendaryOverlay = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ if (romEntry.isBlack) {
+ // In Black, Reshiram's species ID is always retrieved via a pc-relative
+ // load to some constant. All we need to is replace these constants with
+ // the new species ID.
+ int firstConstantOffset = find(boxLegendaryOverlay, Gen5Constants.blackBoxLegendaryCheckPrefix1);
+ if (firstConstantOffset > 0) {
+ firstConstantOffset += Gen5Constants.blackBoxLegendaryCheckPrefix1.length() / 2; // because it was a prefix
+ FileFunctions.writeFullInt(boxLegendaryOverlay, firstConstantOffset, boxLegendarySpecies);
+ }
+ int secondConstantOffset = find(boxLegendaryOverlay, Gen5Constants.blackBoxLegendaryCheckPrefix2);
+ if (secondConstantOffset > 0) {
+ secondConstantOffset += Gen5Constants.blackBoxLegendaryCheckPrefix2.length() / 2; // because it was a prefix
+ FileFunctions.writeFullInt(boxLegendaryOverlay, secondConstantOffset, boxLegendarySpecies);
+ }
+ } else {
+ // In White, Zekrom's species ID is always loaded by loading 161 into a register
+ // and then shifting left by 2. Thus, we need to be more clever with how we
+ // modify code in order to set up some pc-relative loads.
+ int firstFunctionOffset = find(boxLegendaryOverlay, Gen5Constants.whiteBoxLegendaryCheckPrefix1);
+ if (firstFunctionOffset > 0) {
+ firstFunctionOffset += Gen5Constants.whiteBoxLegendaryCheckPrefix1.length() / 2; // because it was a prefix
+
+ // First, nop the instruction that loads a pointer to the string
+ // "scrcmd_pokemon_fld.c" into a register; this has seemingly no
+ // effect on the game and was probably used strictly for debugging.
+ boxLegendaryOverlay[firstFunctionOffset + 66] = 0x00;
+ boxLegendaryOverlay[firstFunctionOffset + 67] = 0x00;
+
+ // In the space that used to hold the address of the "scrcmd_pokemon_fld.c"
+ // string, we're going to instead store the species ID of the box legendary
+ // so that we can do a pc-relative load to it.
+ FileFunctions.writeFullInt(boxLegendaryOverlay, firstFunctionOffset + 320, boxLegendarySpecies);
+
+ // Zekrom's species ID is originally loaded by doing a mov into r1 and then a shift
+ // on that same register four instructions later. This nops out the first instruction
+ // and replaces the left shift with a pc-relative load to the constant we stored above.
+ boxLegendaryOverlay[firstFunctionOffset + 18] = 0x00;
+ boxLegendaryOverlay[firstFunctionOffset + 19] = 0x00;
+ boxLegendaryOverlay[firstFunctionOffset + 26] = 0x49;
+ boxLegendaryOverlay[firstFunctionOffset + 27] = 0x49;
+ }
+
+ int secondFunctionOffset = find(boxLegendaryOverlay, Gen5Constants.whiteBoxLegendaryCheckPrefix2);
+ if (secondFunctionOffset > 0) {
+ secondFunctionOffset += Gen5Constants.whiteBoxLegendaryCheckPrefix2.length() / 2; // because it was a prefix
+
+ // A completely unrelated function below this one decides to pc-relative load 0x00000000 into r4
+ // instead of just doing a mov. We can replace it with a simple "mov r4, #0x0", but we have to be
+ // careful about where we put it. The original code calls a function, performs an "add r6, r0, #0x0",
+ // then does the load into r4. This means that whether or not the Z bit is set depends on the result
+ // of the function call. If we naively replace the load with our mov, we'll be forcibly setting the Z
+ // bit to 1, which will cause the subsequent beq to potentially take us to the wrong place. To get
+ // around this, we reorder the code so the "mov r4, #0x0" occurs *before* the "add r6, r0, #0x0".
+ boxLegendaryOverlay[secondFunctionOffset + 502] = 0x00;
+ boxLegendaryOverlay[secondFunctionOffset + 503] = 0x24;
+ boxLegendaryOverlay[secondFunctionOffset + 504] = 0x06;
+ boxLegendaryOverlay[secondFunctionOffset + 505] = 0x1C;
+
+ // Now replace the 0x00000000 constant with the species ID
+ FileFunctions.writeFullInt(boxLegendaryOverlay, secondFunctionOffset + 556, boxLegendarySpecies);
+
+ // Lastly, replace the mov and lsl that originally puts Zekrom's species ID into r1
+ // with a pc-relative of the above constant and a nop.
+ boxLegendaryOverlay[secondFunctionOffset + 78] = 0x77;
+ boxLegendaryOverlay[secondFunctionOffset + 79] = 0x49;
+ boxLegendaryOverlay[secondFunctionOffset + 80] = 0x00;
+ boxLegendaryOverlay[secondFunctionOffset + 81] = 0x00;
+ }
+ }
+ writeOverlay(romEntry.getInt("FieldOvlNumber"), boxLegendaryOverlay);
+ }
+
+ private void applyBlackWhiteRoamerPatch() throws IOException {
+ int offset = romEntry.getInt("GetRoamerFlagOffsetStartOffset");
+ byte[] overlay = readOverlay(romEntry.getInt("RoamerOvlNumber"));
+
+ // This function returns 0 for Thundurus, 1 for Tornadus, and 2 for any other species.
+ // In testing, this 2 case is never used, so we can use the space for it to pc-relative
+ // load Thundurus's ID. The original code compares to Tornadus and Thundurus then does
+ // "bne #0xA" to the default case. Change it to "bne #0x4", which will just make this
+ // case immediately return.
+ overlay[offset + 10] = 0x00;
+
+ // Now in the space that used to do "mov r0, #0x2" and return, write Thundurus's ID
+ FileFunctions.writeFullInt(overlay, offset + 20, Species.thundurus);
+
+ // Lastly, instead of computing Thundurus's ID as TornadusID + 1, pc-relative load it
+ // from what we wrote earlier.
+ overlay[offset + 6] = 0x03;
+ overlay[offset + 7] = 0x49;
+ writeOverlay(romEntry.getInt("RoamerOvlNumber"), overlay);
+ }
+
+ @Override
+ public int miscTweaksAvailable() {
+ int available = 0;
+ if (romEntry.tweakFiles.get("FastestTextTweak") != null) {
+ available |= MiscTweak.FASTEST_TEXT.getValue();
+ }
+ available |= MiscTweak.BAN_LUCKY_EGG.getValue();
+ available |= MiscTweak.NO_FREE_LUCKY_EGG.getValue();
+ available |= MiscTweak.BAN_BIG_MANIAC_ITEMS.getValue();
+ available |= MiscTweak.UPDATE_TYPE_EFFECTIVENESS.getValue();
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ available |= MiscTweak.BALANCE_STATIC_LEVELS.getValue();
+ }
+ if (romEntry.tweakFiles.get("NationalDexAtStartTweak") != null) {
+ available |= MiscTweak.NATIONAL_DEX_AT_START.getValue();
+ }
+ available |= MiscTweak.RUN_WITHOUT_RUNNING_SHOES.getValue();
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ available |= MiscTweak.FORCE_CHALLENGE_MODE.getValue();
+ }
+ available |= MiscTweak.DISABLE_LOW_HP_MUSIC.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.NO_FREE_LUCKY_EGG) {
+ removeFreeLuckyEgg();
+ } else if (tweak == MiscTweak.BAN_BIG_MANIAC_ITEMS) {
+ // BalmMushroom, Big Nugget, Pearl String, Comet Shard
+ allowedItems.banRange(Items.balmMushroom, 4);
+ nonBadItems.banRange(Items.balmMushroom, 4);
+
+ // Relics
+ allowedItems.banRange(Items.relicVase, 4);
+ nonBadItems.banRange(Items.relicVase, 4);
+
+ // Rare berries
+ allowedItems.banRange(Items.lansatBerry, 7);
+ nonBadItems.banRange(Items.lansatBerry, 7);
+ } else if (tweak == MiscTweak.BALANCE_STATIC_LEVELS) {
+ byte[] fossilFile = scriptNarc.files.get(Gen5Constants.fossilPokemonFile);
+ writeWord(fossilFile,Gen5Constants.fossilPokemonLevelOffset,20);
+ } else if (tweak == MiscTweak.NATIONAL_DEX_AT_START) {
+ patchForNationalDex();
+ } else if (tweak == MiscTweak.RUN_WITHOUT_RUNNING_SHOES) {
+ applyRunWithoutRunningShoesPatch();
+ } else if (tweak == MiscTweak.UPDATE_TYPE_EFFECTIVENESS) {
+ updateTypeEffectiveness();
+ } else if (tweak == MiscTweak.FORCE_CHALLENGE_MODE) {
+ forceChallengeMode();
+ } else if (tweak == MiscTweak.DISABLE_LOW_HP_MUSIC) {
+ disableLowHpMusic();
+ }
+ }
+
+ @Override
+ public boolean isEffectivenessUpdated() {
+ return effectivenessUpdated;
+ }
+
+ // Removes the free lucky egg you receive from Professor Juniper and replaces it with a gooey mulch.
+ private void removeFreeLuckyEgg() {
+ int scriptFileGifts = romEntry.getInt("LuckyEggScriptOffset");
+ int setVarGift = Gen5Constants.hiddenItemSetVarCommand;
+ int mulchIndex = this.random.nextInt(4);
+
+ byte[] itemScripts = scriptNarc.files.get(scriptFileGifts);
+ int offset = 0;
+ int lookingForEggs = romEntry.romType == Gen5Constants.Type_BW ? 1 : 2;
+ while (lookingForEggs > 0) {
+ int part1 = readWord(itemScripts, offset);
+ if (part1 == Gen5Constants.scriptListTerminator) {
+ // done
+ break;
+ }
+ int offsetInFile = readRelativePointer(itemScripts, offset);
+ offset += 4;
+ if (offsetInFile > itemScripts.length) {
+ break;
+ }
+ while (true) {
+ offsetInFile++;
+ // Gift items are not necessarily word aligned, so need to read one byte at a time
+ int b = readByte(itemScripts, offsetInFile);
+ if (b == setVarGift) {
+ int command = readWord(itemScripts, offsetInFile);
+ int variable = readWord(itemScripts,offsetInFile + 2);
+ int item = readWord(itemScripts, offsetInFile + 4);
+ if (command == setVarGift && variable == Gen5Constants.hiddenItemVarSet && item == Items.luckyEgg) {
+
+ writeWord(itemScripts, offsetInFile + 4, Gen5Constants.mulchIndices[mulchIndex]);
+ lookingForEggs--;
+ }
+ }
+ if (b == 0x2E) { // Beginning of a new block in the file
+ break;
+ }
+ }
+ }
+ }
+
+ private void applyFastestText() {
+ genericIPSPatch(arm9, "FastestTextTweak");
+ }
+
+ private void patchForNationalDex() {
+ byte[] pokedexScript = scriptNarc.files.get(romEntry.getInt("NationalDexScriptOffset"));
+
+ // Our patcher breaks if the output file is larger than the input file. In our case, we want
+ // to expand the script by four bytes to add an instruction to enable the national dex. Thus,
+ // the IPS patch was created with us adding four 0x00 bytes to the end of the script in mind.
+ byte[] expandedPokedexScript = new byte[pokedexScript.length + 4];
+ System.arraycopy(pokedexScript, 0, expandedPokedexScript, 0, pokedexScript.length);
+ genericIPSPatch(expandedPokedexScript, "NationalDexAtStartTweak");
+ scriptNarc.files.set(romEntry.getInt("NationalDexScriptOffset"), expandedPokedexScript);
+ }
+
+ private void applyRunWithoutRunningShoesPatch() {
+ try {
+ // In the overlay that handles field movement, there's a very simple function
+ // that checks if the player has the Running Shoes by checking if flag 2403 is
+ // set on the save file. If it isn't, the code branches to a separate code path
+ // where the function returns 0. The below code simply nops this branch so that
+ // this function always returns 1, regardless of the status of flag 2403.
+ byte[] fieldOverlay = readOverlay(romEntry.getInt("FieldOvlNumber"));
+ String prefix = Gen5Constants.runningShoesPrefix;
+ int offset = find(fieldOverlay, prefix);
+ if (offset != 0) {
+ writeWord(fieldOverlay, offset, 0);
+ writeOverlay(romEntry.getInt("FieldOvlNumber"), fieldOverlay);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void updateTypeEffectiveness() {
+ try {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("BattleOvlNumber"));
+ int typeEffectivenessTableOffset = find(battleOverlay, Gen5Constants.typeEffectivenessTableLocator);
+ if (typeEffectivenessTableOffset > 0) {
+ Effectiveness[][] typeEffectivenessTable = readTypeEffectivenessTable(battleOverlay, typeEffectivenessTableOffset);
+ log("--Updating Type Effectiveness--");
+ int steel = Gen5Constants.typeToByte(Type.STEEL);
+ int dark = Gen5Constants.typeToByte(Type.DARK);
+ int ghost = Gen5Constants.typeToByte(Type.GHOST);
+ typeEffectivenessTable[ghost][steel] = Effectiveness.NEUTRAL;
+ log("Replaced: Ghost not very effective vs Steel => Ghost neutral vs Steel");
+ typeEffectivenessTable[dark][steel] = Effectiveness.NEUTRAL;
+ log("Replaced: Dark not very effective vs Steel => Dark neutral vs Steel");
+ logBlankLine();
+ writeTypeEffectivenessTable(typeEffectivenessTable, battleOverlay, typeEffectivenessTableOffset);
+ writeOverlay(romEntry.getInt("BattleOvlNumber"), battleOverlay);
+ effectivenessUpdated = true;
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private Effectiveness[][] readTypeEffectivenessTable(byte[] battleOverlay, int typeEffectivenessTableOffset) {
+ Effectiveness[][] effectivenessTable = new Effectiveness[Type.DARK.ordinal() + 1][Type.DARK.ordinal() + 1];
+ for (int attacker = Type.NORMAL.ordinal(); attacker <= Type.DARK.ordinal(); attacker++) {
+ for (int defender = Type.NORMAL.ordinal(); defender <= Type.DARK.ordinal(); defender++) {
+ int offset = typeEffectivenessTableOffset + (attacker * (Type.DARK.ordinal() + 1)) + defender;
+ int effectivenessInternal = battleOverlay[offset];
+ Effectiveness effectiveness = null;
+ switch (effectivenessInternal) {
+ case 8:
+ effectiveness = Effectiveness.DOUBLE;
+ break;
+ case 4:
+ effectiveness = Effectiveness.NEUTRAL;
+ break;
+ case 2:
+ effectiveness = Effectiveness.HALF;
+ break;
+ case 0:
+ effectiveness = Effectiveness.ZERO;
+ break;
+ }
+ effectivenessTable[attacker][defender] = effectiveness;
+ }
+ }
+ return effectivenessTable;
+ }
+
+ private void writeTypeEffectivenessTable(Effectiveness[][] typeEffectivenessTable, byte[] battleOverlay,
+ int typeEffectivenessTableOffset) {
+ for (int attacker = Type.NORMAL.ordinal(); attacker <= Type.DARK.ordinal(); attacker++) {
+ for (int defender = Type.NORMAL.ordinal(); defender <= Type.DARK.ordinal(); defender++) {
+ Effectiveness effectiveness = typeEffectivenessTable[attacker][defender];
+ int offset = typeEffectivenessTableOffset + (attacker * (Type.DARK.ordinal() + 1)) + defender;
+ byte effectivenessInternal = 0;
+ switch (effectiveness) {
+ case DOUBLE:
+ effectivenessInternal = 8;
+ break;
+ case NEUTRAL:
+ effectivenessInternal = 4;
+ break;
+ case HALF:
+ effectivenessInternal = 2;
+ break;
+ case ZERO:
+ effectivenessInternal = 0;
+ break;
+ }
+ battleOverlay[offset] = effectivenessInternal;
+ }
+ }
+ }
+
+ private void forceChallengeMode() {
+ int offset = find(arm9, Gen5Constants.forceChallengeModeLocator);
+ if (offset > 0) {
+ // offset is now pointing at the start of sub_2010528, which is the function that
+ // determines which difficulty the player currently has enabled. It returns 0 for
+ // Easy Mode, 1 for Normal Mode, and 2 for Challenge Mode. Since we're just trying
+ // to force Challenge Mode, all we need to do is:
+ // mov r0, #0x2
+ // bx lr
+ arm9[offset] = 0x02;
+ arm9[offset + 1] = 0x20;
+ arm9[offset + 2] = 0x70;
+ arm9[offset + 3] = 0x47;
+ }
+ }
+
+ private void disableLowHpMusic() {
+ try {
+ byte[] lowHealthMusicOverlay = readOverlay(romEntry.getInt("LowHealthMusicOvlNumber"));
+ int offset = find(lowHealthMusicOverlay, Gen5Constants.lowHealthMusicLocator);
+ if (offset > 0) {
+ // The game calls a function that returns 2 if the Pokemon has low HP. The ASM looks like this:
+ // bl funcThatReturns2IfThePokemonHasLowHp
+ // cmp r0, #0x2
+ // bne pokemonDoesNotHaveLowHp
+ // mov r7, #0x1
+ // The offset variable is currently pointing at the bne instruction. If we change that bne to an unconditional
+ // branch, the game will never think the player's Pokemon has low HP (for the purposes of changing the music).
+ lowHealthMusicOverlay[offset + 1] = (byte)0xE0;
+ writeOverlay(romEntry.getInt("LowHealthMusicOvlNumber"), lowHealthMusicOverlay);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public void enableGuaranteedPokemonCatching() {
+ try {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("BattleOvlNumber"));
+ int offset = find(battleOverlay, Gen5Constants.perfectOddsBranchLocator);
+ if (offset > 0) {
+ // The game checks to see if your odds are greater then or equal to 255 using the following
+ // code. Note that they compare to 0xFF000 instead of 0xFF; it looks like all catching code
+ // probabilities are shifted like this?
+ // mov r0, #0xFF
+ // lsl r0, r0, #0xC
+ // cmp r7, r0
+ // blt oddsLessThanOrEqualTo254
+ // The below code just nops the branch out so it always acts like our odds are 255, and
+ // Pokemon are automatically caught no matter what.
+ battleOverlay[offset] = 0x00;
+ battleOverlay[offset + 1] = 0x00;
+ writeOverlay(romEntry.getInt("BattleOvlNumber"), battleOverlay);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private boolean genericIPSPatch(byte[] data, String ctName) {
+ String patchName = romEntry.tweakFiles.get(ctName);
+ if (patchName == null) {
+ return false;
+ }
+
+ try {
+ FileFunctions.applyPatch(data, patchName);
+ return true;
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public List<Integer> getTMMoves() {
+ String tmDataPrefix = Gen5Constants.tmDataPrefix;
+ int offset = find(arm9, tmDataPrefix);
+ if (offset > 0) {
+ offset += Gen5Constants.tmDataPrefix.length() / 2; // because it was
+ // a prefix
+ List<Integer> tms = new ArrayList<>();
+ for (int i = 0; i < Gen5Constants.tmBlockOneCount; i++) {
+ tms.add(readWord(arm9, offset + i * 2));
+ }
+ // Skip past first 92 TMs and 6 HMs
+ offset += (Gen5Constants.tmBlockOneCount + Gen5Constants.hmCount) * 2;
+ for (int i = 0; i < (Gen5Constants.tmCount - Gen5Constants.tmBlockOneCount); i++) {
+ tms.add(readWord(arm9, offset + i * 2));
+ }
+ return tms;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public List<Integer> getHMMoves() {
+ String tmDataPrefix = Gen5Constants.tmDataPrefix;
+ int offset = find(arm9, tmDataPrefix);
+ if (offset > 0) {
+ offset += Gen5Constants.tmDataPrefix.length() / 2; // because it was
+ // a prefix
+ offset += Gen5Constants.tmBlockOneCount * 2; // TM data
+ List<Integer> hms = new ArrayList<>();
+ for (int i = 0; i < Gen5Constants.hmCount; i++) {
+ hms.add(readWord(arm9, offset + i * 2));
+ }
+ return hms;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void setTMMoves(List<Integer> moveIndexes) {
+ String tmDataPrefix = Gen5Constants.tmDataPrefix;
+ int offset = find(arm9, tmDataPrefix);
+ if (offset > 0) {
+ offset += Gen5Constants.tmDataPrefix.length() / 2; // because it was
+ // a prefix
+ for (int i = 0; i < Gen5Constants.tmBlockOneCount; i++) {
+ writeWord(arm9, offset + i * 2, moveIndexes.get(i));
+ }
+ // Skip past those 92 TMs and 6 HMs
+ offset += (Gen5Constants.tmBlockOneCount + Gen5Constants.hmCount) * 2;
+ for (int i = 0; i < (Gen5Constants.tmCount - Gen5Constants.tmBlockOneCount); i++) {
+ writeWord(arm9, offset + i * 2, moveIndexes.get(i + Gen5Constants.tmBlockOneCount));
+ }
+
+ // Update TM item descriptions
+ List<String> itemDescriptions = getStrings(false, romEntry.getInt("ItemDescriptionsTextOffset"));
+ List<String> moveDescriptions = getStrings(false, romEntry.getInt("MoveDescriptionsTextOffset"));
+ // TM01 is item 328 and so on
+ for (int i = 0; i < Gen5Constants.tmBlockOneCount; i++) {
+ itemDescriptions.set(i + Gen5Constants.tmBlockOneOffset, moveDescriptions.get(moveIndexes.get(i)));
+ }
+ // TM93-95 are 618-620
+ for (int i = 0; i < (Gen5Constants.tmCount - Gen5Constants.tmBlockOneCount); i++) {
+ itemDescriptions.set(i + Gen5Constants.tmBlockTwoOffset,
+ moveDescriptions.get(moveIndexes.get(i + Gen5Constants.tmBlockOneCount)));
+ }
+ // Save the new item descriptions
+ setStrings(false, romEntry.getInt("ItemDescriptionsTextOffset"), itemDescriptions);
+ // Palettes
+ String baseOfPalettes;
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ baseOfPalettes = Gen5Constants.bw1ItemPalettesPrefix;
+ } else {
+ baseOfPalettes = Gen5Constants.bw2ItemPalettesPrefix;
+ }
+ int offsPals = find(arm9, baseOfPalettes);
+ if (offsPals > 0) {
+ // Write pals
+ for (int i = 0; i < Gen5Constants.tmBlockOneCount; i++) {
+ int itmNum = Gen5Constants.tmBlockOneOffset + i;
+ Move m = this.moves[moveIndexes.get(i)];
+ int pal = this.typeTMPaletteNumber(m.type);
+ writeWord(arm9, offsPals + itmNum * 4 + 2, pal);
+ }
+ for (int i = 0; i < (Gen5Constants.tmCount - Gen5Constants.tmBlockOneCount); i++) {
+ int itmNum = Gen5Constants.tmBlockTwoOffset + i;
+ Move m = this.moves[moveIndexes.get(i + Gen5Constants.tmBlockOneCount)];
+ int pal = this.typeTMPaletteNumber(m.type);
+ writeWord(arm9, offsPals + itmNum * 4 + 2, pal);
+ }
+ }
+ }
+ }
+
+ private static RomFunctions.StringSizeDeterminer ssd = encodedText -> {
+ int offs = 0;
+ int len = encodedText.length();
+ while (encodedText.indexOf("\\x", offs) != -1) {
+ len -= 5;
+ offs = encodedText.indexOf("\\x", offs) + 1;
+ }
+ return len;
+ };
+
+ @Override
+ public int getTMCount() {
+ return Gen5Constants.tmCount;
+ }
+
+ @Override
+ public int getHMCount() {
+ return Gen5Constants.hmCount;
+ }
+
+ @Override
+ public Map<Pokemon, boolean[]> getTMHMCompatibility() {
+ Map<Pokemon, boolean[]> compat = new TreeMap<>();
+ int formeCount = Gen5Constants.getFormeCount(romEntry.romType);
+ int formeOffset = Gen5Constants.getFormeOffset(romEntry.romType);
+ for (int i = 1; i <= Gen5Constants.pokemonCount + formeCount; i++) {
+ byte[] data;
+ if (i > Gen5Constants.pokemonCount) {
+ data = pokeNarc.files.get(i + formeOffset);
+ } else {
+ data = pokeNarc.files.get(i);
+ }
+ Pokemon pkmn = pokes[i];
+ boolean[] flags = new boolean[Gen5Constants.tmCount + Gen5Constants.hmCount + 1];
+ for (int j = 0; j < 13; j++) {
+ readByteIntoFlags(data, flags, j * 8 + 1, Gen5Constants.bsTMHMCompatOffset + j);
+ }
+ compat.put(pkmn, flags);
+ }
+ return compat;
+ }
+
+ @Override
+ public void setTMHMCompatibility(Map<Pokemon, boolean[]> compatData) {
+ int formeOffset = Gen5Constants.getFormeOffset(romEntry.romType);
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ int number = pkmn.number;
+ if (number > Gen5Constants.pokemonCount) {
+ number += formeOffset;
+ }
+ byte[] data = pokeNarc.files.get(number);
+ for (int j = 0; j < 13; j++) {
+ data[Gen5Constants.bsTMHMCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasMoveTutors() {
+ return romEntry.romType == Gen5Constants.Type_BW2;
+ }
+
+ @Override
+ public List<Integer> getMoveTutorMoves() {
+ if (!hasMoveTutors()) {
+ return new ArrayList<>();
+ }
+ int baseOffset = romEntry.getInt("MoveTutorDataOffset");
+ int amount = Gen5Constants.bw2MoveTutorCount;
+ int bytesPer = Gen5Constants.bw2MoveTutorBytesPerEntry;
+ List<Integer> mtMoves = new ArrayList<>();
+ try {
+ byte[] mtFile = readOverlay(romEntry.getInt("MoveTutorOvlNumber"));
+ for (int i = 0; i < amount; i++) {
+ mtMoves.add(readWord(mtFile, baseOffset + i * bytesPer));
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return mtMoves;
+ }
+
+ @Override
+ public void setMoveTutorMoves(List<Integer> moves) {
+ if (!hasMoveTutors()) {
+ return;
+ }
+ int baseOffset = romEntry.getInt("MoveTutorDataOffset");
+ int amount = Gen5Constants.bw2MoveTutorCount;
+ int bytesPer = Gen5Constants.bw2MoveTutorBytesPerEntry;
+ if (moves.size() != amount) {
+ return;
+ }
+ try {
+ byte[] mtFile = readOverlay(romEntry.getInt("MoveTutorOvlNumber"));
+ for (int i = 0; i < amount; i++) {
+ writeWord(mtFile, baseOffset + i * bytesPer, moves.get(i));
+ }
+ writeOverlay(romEntry.getInt("MoveTutorOvlNumber"), mtFile);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public Map<Pokemon, boolean[]> getMoveTutorCompatibility() {
+ if (!hasMoveTutors()) {
+ return new TreeMap<>();
+ }
+ Map<Pokemon, boolean[]> compat = new TreeMap<>();
+ int[] countsPersonalOrder = new int[] { 15, 17, 13, 15 };
+ int[] countsMoveOrder = new int[] { 13, 15, 15, 17 };
+ int[] personalToMoveOrder = new int[] { 1, 3, 0, 2 };
+ int formeCount = Gen5Constants.getFormeCount(romEntry.romType);
+ int formeOffset = Gen5Constants.getFormeOffset(romEntry.romType);
+ for (int i = 1; i <= Gen5Constants.pokemonCount + formeCount; i++) {
+ byte[] data;
+ if (i > Gen5Constants.pokemonCount) {
+ data = pokeNarc.files.get(i + formeOffset);
+ } else {
+ data = pokeNarc.files.get(i);
+ }
+ Pokemon pkmn = pokes[i];
+ boolean[] flags = new boolean[Gen5Constants.bw2MoveTutorCount + 1];
+ for (int mt = 0; mt < 4; mt++) {
+ boolean[] mtflags = new boolean[countsPersonalOrder[mt] + 1];
+ for (int j = 0; j < 4; j++) {
+ readByteIntoFlags(data, mtflags, j * 8 + 1, Gen5Constants.bsMTCompatOffset + mt * 4 + j);
+ }
+ int offsetOfThisData = 0;
+ for (int cmoIndex = 0; cmoIndex < personalToMoveOrder[mt]; cmoIndex++) {
+ offsetOfThisData += countsMoveOrder[cmoIndex];
+ }
+ System.arraycopy(mtflags, 1, flags, offsetOfThisData + 1, countsPersonalOrder[mt]);
+ }
+ compat.put(pkmn, flags);
+ }
+ return compat;
+ }
+
+ @Override
+ public void setMoveTutorCompatibility(Map<Pokemon, boolean[]> compatData) {
+ if (!hasMoveTutors()) {
+ return;
+ }
+ int formeOffset = Gen5Constants.getFormeOffset(romEntry.romType);
+ // BW2 move tutor flags aren't using the same order as the move tutor
+ // move data.
+ // We unscramble them from move data order to personal.narc flag order.
+ int[] countsPersonalOrder = new int[] { 15, 17, 13, 15 };
+ int[] countsMoveOrder = new int[] { 13, 15, 15, 17 };
+ int[] personalToMoveOrder = new int[] { 1, 3, 0, 2 };
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compatData.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ int number = pkmn.number;
+ if (number > Gen5Constants.pokemonCount) {
+ number += formeOffset;
+ }
+ byte[] data = pokeNarc.files.get(number);
+ for (int mt = 0; mt < 4; mt++) {
+ int offsetOfThisData = 0;
+ for (int cmoIndex = 0; cmoIndex < personalToMoveOrder[mt]; cmoIndex++) {
+ offsetOfThisData += countsMoveOrder[cmoIndex];
+ }
+ boolean[] mtflags = new boolean[countsPersonalOrder[mt] + 1];
+ System.arraycopy(flags, offsetOfThisData + 1, mtflags, 1, countsPersonalOrder[mt]);
+ for (int j = 0; j < 4; j++) {
+ data[Gen5Constants.bsMTCompatOffset + mt * 4 + j] = getByteFromFlags(mtflags, j * 8 + 1);
+ }
+ }
+ }
+ }
+
+ 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<Integer> 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);
+ }
+ }
+
+ private List<String> getStrings(boolean isStoryText, int index) {
+ NARCArchive baseNARC = isStoryText ? storyTextNarc : stringsNarc;
+ byte[] rawFile = baseNARC.files.get(index);
+ return new ArrayList<>(PPTxtHandler.readTexts(rawFile));
+ }
+
+ private void setStrings(boolean isStoryText, int index, List<String> strings) {
+ NARCArchive baseNARC = isStoryText ? storyTextNarc : stringsNarc;
+ byte[] oldRawFile = baseNARC.files.get(index);
+ byte[] newRawFile = PPTxtHandler.saveEntry(oldRawFile, strings);
+ baseNARC.files.set(index, newRawFile);
+ }
+
+ @Override
+ public String getROMName() {
+ return "Pokemon " + romEntry.name;
+ }
+
+ @Override
+ public String getROMCode() {
+ return romEntry.romCode;
+ }
+
+ @Override
+ public String getSupportLevel() {
+ return romEntry.staticPokemonSupport ? "Complete" : "No Static Pokemon";
+ }
+
+ @Override
+ public boolean hasTimeBasedEncounters() {
+ return true; // All BW/BW2 do [seasons]
+ }
+
+ @Override
+ public boolean hasWildAltFormes() {
+ return true;
+ }
+
+ private void populateEvolutions() {
+ for (Pokemon pkmn : pokes) {
+ if (pkmn != null) {
+ pkmn.evolutionsFrom.clear();
+ pkmn.evolutionsTo.clear();
+ }
+ }
+
+ // Read NARC
+ try {
+ NARCArchive evoNARC = readNARC(romEntry.getFile("PokemonEvolutions"));
+ for (int i = 1; i <= Gen5Constants.pokemonCount; i++) {
+ Pokemon pk = pokes[i];
+ byte[] evoEntry = evoNARC.files.get(i);
+ for (int evo = 0; evo < 7; evo++) {
+ int method = readWord(evoEntry, evo * 6);
+ int species = readWord(evoEntry, evo * 6 + 4);
+ if (method >= 1 && method <= Gen5Constants.evolutionMethodCount && species >= 1) {
+ EvolutionType et = EvolutionType.fromIndex(5, 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);
+ pokes[species].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 writeEvolutions() {
+ try {
+ NARCArchive evoNARC = readNARC(romEntry.getFile("PokemonEvolutions"));
+ for (int i = 1; i <= Gen5Constants.pokemonCount; i++) {
+ byte[] evoEntry = evoNARC.files.get(i);
+ Pokemon pk = pokes[i];
+ if (pk.number == Species.nincada && romEntry.tweakFiles.containsKey("ShedinjaEvolutionTweak")) {
+ writeShedinjaEvolution();
+ }
+ int evosWritten = 0;
+ for (Evolution evo : pk.evolutionsFrom) {
+ writeWord(evoEntry, evosWritten * 6, evo.type.toIndex(5));
+ writeWord(evoEntry, evosWritten * 6 + 2, evo.extraInfo);
+ writeWord(evoEntry, evosWritten * 6 + 4, evo.to.number);
+ evosWritten++;
+ if (evosWritten == 7) {
+ break;
+ }
+ }
+ while (evosWritten < 7) {
+ writeWord(evoEntry, evosWritten * 6, 0);
+ writeWord(evoEntry, evosWritten * 6 + 2, 0);
+ writeWord(evoEntry, evosWritten * 6 + 4, 0);
+ evosWritten++;
+ }
+ }
+ writeNARC(romEntry.getFile("PokemonEvolutions"), evoNARC);
+ } 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 extraEvolution = nincada.evolutionsFrom.get(1).to;
+
+ // Update the evolution overlay to point towards our custom code in the expanded arm9.
+ byte[] evolutionOverlay = readOverlay(romEntry.getInt("EvolutionOvlNumber"));
+ genericIPSPatch(evolutionOverlay, "ShedinjaEvolutionOvlTweak");
+ writeOverlay(romEntry.getInt("EvolutionOvlNumber"), evolutionOverlay);
+
+ // Relies on arm9 already being extended, which it *should* have been in loadedROM
+ genericIPSPatch(arm9, "ShedinjaEvolutionTweak");
+
+ // After applying the tweak, Shedinja's ID is simply pc-relative loaded, so just
+ // update the constant
+ int offset = romEntry.getInt("ShedinjaSpeciesOffset");
+ if (offset > 0) {
+ FileFunctions.writeFullInt(arm9, offset, extraEvolution.number);
+ }
+ }
+
+ @Override
+ public void removeImpossibleEvolutions(Settings settings) {
+ boolean changeMoveEvos = !(settings.getMovesetsMod() == Settings.MovesetsMod.UNCHANGED);
+
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+ Set<Evolution> 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());
+ }
+ }
+
+ 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);
+
+ // Reduce the amount of happiness required to evolve.
+ int offset = find(arm9, Gen5Constants.friendshipValueForEvoLocator);
+ if (offset > 0) {
+ // Amount of required happiness for HAPPINESS evolutions.
+ if (arm9[offset] == (byte)220) {
+ arm9[offset] = (byte)160;
+ }
+ // Amount of required happiness for HAPPINESS_DAY evolutions.
+ if (arm9[offset + 20] == (byte)220) {
+ arm9[offset + 20] = (byte)160;
+ }
+ // Amount of required happiness for HAPPINESS_NIGHT evolutions.
+ if (arm9[offset + 38] == (byte)220) {
+ arm9[offset + 38] = (byte)160;
+ }
+ }
+
+ 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<Evolution> 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);
+ }
+ }
+ }
+ 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<String> getTrainerNames() {
+ List<String> tnames = getStrings(false, romEntry.getInt("TrainerNamesTextOffset"));
+ tnames.remove(0); // blank one
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ List<String> pwtNames = getStrings(false, romEntry.getInt("PWTTrainerNamesTextOffset"));
+ tnames.addAll(pwtNames);
+ }
+ // Tack the mugshot names on the end
+ List<String> mnames = getStrings(false, romEntry.getInt("TrainerMugshotsTextOffset"));
+ for (String mname : mnames) {
+ if (!mname.isEmpty() && (mname.charAt(0) >= 'A' && mname.charAt(0) <= 'Z')) {
+ tnames.add(mname);
+ }
+ }
+ return tnames;
+ }
+
+ @Override
+ public int maxTrainerNameLength() {
+ return 10;// based off the english ROMs
+ }
+
+ @Override
+ public void setTrainerNames(List<String> trainerNames) {
+ List<String> tnames = getStrings(false, romEntry.getInt("TrainerNamesTextOffset"));
+ // Grab the mugshot names off the back of the list of trainer names
+ // we got back
+ List<String> mnames = getStrings(false, romEntry.getInt("TrainerMugshotsTextOffset"));
+ int trNamesSize = trainerNames.size();
+ for (int i = mnames.size() - 1; i >= 0; i--) {
+ String origMName = mnames.get(i);
+ if (!origMName.isEmpty() && (origMName.charAt(0) >= 'A' && origMName.charAt(0) <= 'Z')) {
+ // Grab replacement
+ String replacement = trainerNames.remove(--trNamesSize);
+ mnames.set(i, replacement);
+ }
+ }
+ // Save back mugshot names
+ setStrings(false, romEntry.getInt("TrainerMugshotsTextOffset"), mnames);
+
+ // Now save the rest of trainer names
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ List<String> pwtNames = getStrings(false, romEntry.getInt("PWTTrainerNamesTextOffset"));
+ List<String> newTNames = new ArrayList<>();
+ List<String> newPWTNames = new ArrayList<>();
+ newTNames.add(0, tnames.get(0)); // the 0-entry, preserve it
+ for (int i = 1; i < tnames.size() + pwtNames.size(); i++) {
+ if (i < tnames.size()) {
+ newTNames.add(trainerNames.get(i - 1));
+ } else {
+ newPWTNames.add(trainerNames.get(i - 1));
+ }
+ }
+ setStrings(false, romEntry.getInt("TrainerNamesTextOffset"), newTNames);
+ setStrings(false, romEntry.getInt("PWTTrainerNamesTextOffset"), newPWTNames);
+ } else {
+ List<String> newTNames = new ArrayList<>(trainerNames);
+ newTNames.add(0, tnames.get(0)); // the 0-entry, preserve it
+ setStrings(false, romEntry.getInt("TrainerNamesTextOffset"), newTNames);
+ }
+ }
+
+ @Override
+ public TrainerNameMode trainerNameMode() {
+ return TrainerNameMode.MAX_LENGTH;
+ }
+
+ @Override
+ public List<Integer> getTCNameLengthsByTrainer() {
+ // not needed
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<String> getTrainerClassNames() {
+ List<String> classNames = getStrings(false, romEntry.getInt("TrainerClassesTextOffset"));
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ classNames.addAll(getStrings(false, romEntry.getInt("PWTTrainerClassesTextOffset")));
+ }
+ return classNames;
+ }
+
+ @Override
+ public void setTrainerClassNames(List<String> trainerClassNames) {
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ List<String> newTClasses = new ArrayList<>();
+ List<String> newPWTClasses = new ArrayList<>();
+ List<String> classNames = getStrings(false, romEntry.getInt("TrainerClassesTextOffset"));
+ List<String> pwtClassNames = getStrings(false, romEntry.getInt("PWTTrainerClassesTextOffset"));
+ for (int i = 0; i < classNames.size() + pwtClassNames.size(); i++) {
+ if (i < classNames.size()) {
+ newTClasses.add(trainerClassNames.get(i));
+ } else {
+ newPWTClasses.add(trainerClassNames.get(i));
+ }
+ }
+ setStrings(false, romEntry.getInt("TrainerClassesTextOffset"), newTClasses);
+ setStrings(false, romEntry.getInt("PWTTrainerClassesTextOffset"), newPWTClasses);
+ } else {
+ setStrings(false, romEntry.getInt("TrainerClassesTextOffset"), trainerClassNames);
+ }
+ }
+
+ @Override
+ public int maxTrainerClassNameLength() {
+ return 12;// based off the english ROMs
+ }
+
+ @Override
+ public boolean fixedTrainerClassNamesLength() {
+ return false;
+ }
+
+ @Override
+ public List<Integer> getDoublesTrainerClasses() {
+ int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses");
+ List<Integer> doubles = new ArrayList<>();
+ for (int tClass : doublesClasses) {
+ doubles.add(tClass);
+ }
+ return doubles;
+ }
+
+ @Override
+ public String getDefaultExtension() {
+ return "nds";
+ }
+
+ @Override
+ public int abilitiesPerPokemon() {
+ return 3;
+ }
+
+ @Override
+ public int highestAbilityIndex() {
+ return Gen5Constants.highestAbilityIndex;
+ }
+
+ @Override
+ public int internalStringLength(String string) {
+ return ssd.lengthFor(string);
+ }
+
+ @Override
+ public void randomizeIntroPokemon() {
+ try {
+ int introPokemon = randomPokemon().number;
+ byte[] introGraphicOverlay = readOverlay(romEntry.getInt("IntroGraphicOvlNumber"));
+ int offset = find(introGraphicOverlay, Gen5Constants.introGraphicPrefix);
+ if (offset > 0) {
+ offset += Gen5Constants.introGraphicPrefix.length() / 2; // because it was a prefix
+ // offset is now pointing at the species constant that gets pc-relative
+ // loaded to determine what sprite to load.
+ writeWord(introGraphicOverlay, offset, introPokemon);
+ writeOverlay(romEntry.getInt("IntroGraphicOvlNumber"), introGraphicOverlay);
+ }
+
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ byte[] introCryOverlay = readOverlay(romEntry.getInt("IntroCryOvlNumber"));
+ offset = find(introCryOverlay, Gen5Constants.bw1IntroCryPrefix);
+ if (offset > 0) {
+ offset += Gen5Constants.bw1IntroCryPrefix.length() / 2; // because it was a prefix
+ // The function starting from the offset looks like this:
+ // mov r0, #0x8f
+ // str r1, [sp, #local_94]
+ // lsl r0, r0, #0x2
+ // mov r2, #0x40
+ // mov r3, #0x0
+ // bl PlayCry
+ // [rest of the function...]
+ // pop { r3, r4, r5, r6, r7, pc }
+ // C0 46 (these are useless padding bytes)
+ // To make this more extensible, we want to pc-relative load a species ID into r0 instead.
+ // Start by moving everything below the left shift up by 2 bytes. We won't need the left
+ // shift later, and it will give us 4 bytes after the pop to use for the ID.
+ for (int i = offset + 6; i < offset + 40; i++) {
+ introCryOverlay[i - 2] = introCryOverlay[i];
+ }
+
+ // The call to PlayCry needs to be adjusted as well, since it got moved.
+ introCryOverlay[offset + 10]++;
+
+ // Now write the species ID in the 4 bytes of space now available at the bottom,
+ // and then write a pc-relative load to this species ID at the offset.
+ FileFunctions.writeFullInt(introCryOverlay, offset + 38, introPokemon);
+ introCryOverlay[offset] = 0x9;
+ introCryOverlay[offset + 1] = 0x48;
+ writeOverlay(romEntry.getInt("IntroCryOvlNumber"), introCryOverlay);
+ }
+ } else {
+ byte[] introCryOverlay = readOverlay(romEntry.getInt("IntroCryOvlNumber"));
+ offset = find(introCryOverlay, Gen5Constants.bw2IntroCryLocator);
+ if (offset > 0) {
+ // offset is now pointing at the species constant that gets pc-relative
+ // loaded to determine what cry to play.
+ writeWord(introCryOverlay, offset, introPokemon);
+ writeOverlay(romEntry.getInt("IntroCryOvlNumber"), introCryOverlay);
+ }
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public ItemList getAllowedItems() {
+ return allowedItems;
+ }
+
+ @Override
+ public ItemList getNonBadItems() {
+ return nonBadItems;
+ }
+
+ @Override
+ public List<Integer> getUniqueNoSellItems() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getRegularShopItems() {
+ return regularShopItems;
+ }
+
+ @Override
+ public List<Integer> getOPShopItems() {
+ return opShopItems;
+ }
+
+
+ @Override
+ public String[] getItemNames() {
+ return itemNames.toArray(new String[0]);
+ }
+
+ @Override
+ public String abilityName(int number) {
+ return abilityNames.get(number);
+ }
+
+ @Override
+ public Map<Integer, List<Integer>> getAbilityVariations() {
+ return Gen5Constants.abilityVariations;
+ }
+
+ @Override
+ public List<Integer> getUselessAbilities() {
+ return new ArrayList<>(Gen5Constants.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;
+ }
+
+ // In Gen 5, alt formes for Trainer Pokemon use the base forme's ability
+ Pokemon pkmn = tp.pokemon;
+ while (pkmn.baseForme != null) {
+ pkmn = pkmn.baseForme;
+ }
+
+ List<Integer> abilityList = Arrays.asList(pkmn.ability1, pkmn.ability2, pkmn.ability3);
+ return abilityList.get(tp.abilitySlot - 1);
+ }
+
+ @Override
+ public boolean hasMegaEvolutions() {
+ return false;
+ }
+
+ private List<Integer> getFieldItems() {
+ List<Integer> fieldItems = new ArrayList<>();
+ // normal items
+ int scriptFileNormal = romEntry.getInt("ItemBallsScriptOffset");
+ int scriptFileHidden = romEntry.getInt("HiddenItemsScriptOffset");
+ int[] skipTable = romEntry.arrayEntries.get("ItemBallsSkip");
+ int[] skipTableH = romEntry.arrayEntries.get("HiddenItemsSkip");
+ int setVarNormal = Gen5Constants.normalItemSetVarCommand;
+ int setVarHidden = Gen5Constants.hiddenItemSetVarCommand;
+
+ byte[] itemScripts = scriptNarc.files.get(scriptFileNormal);
+ int offset = 0;
+ int skipTableOffset = 0;
+ while (true) {
+ int part1 = readWord(itemScripts, offset);
+ if (part1 == Gen5Constants.scriptListTerminator) {
+ // done
+ break;
+ }
+ int offsetInFile = readRelativePointer(itemScripts, offset);
+ offset += 4;
+ if (offsetInFile > itemScripts.length) {
+ break;
+ }
+ if (skipTableOffset < skipTable.length && (skipTable[skipTableOffset] == (offset / 4) - 1)) {
+ skipTableOffset++;
+ continue;
+ }
+ int command = readWord(itemScripts, offsetInFile + 2);
+ int variable = readWord(itemScripts, offsetInFile + 4);
+ if (command == setVarNormal && variable == Gen5Constants.normalItemVarSet) {
+ int item = readWord(itemScripts, offsetInFile + 6);
+ fieldItems.add(item);
+ }
+
+ }
+
+ // hidden items
+ byte[] hitemScripts = scriptNarc.files.get(scriptFileHidden);
+ offset = 0;
+ skipTableOffset = 0;
+ while (true) {
+ int part1 = readWord(hitemScripts, offset);
+ if (part1 == Gen5Constants.scriptListTerminator) {
+ // done
+ break;
+ }
+ int offsetInFile = readRelativePointer(hitemScripts, offset);
+ if (offsetInFile > hitemScripts.length) {
+ break;
+ }
+ offset += 4;
+ if (skipTableOffset < skipTable.length && (skipTableH[skipTableOffset] == (offset / 4) - 1)) {
+ skipTableOffset++;
+ continue;
+ }
+ int command = readWord(hitemScripts, offsetInFile + 2);
+ int variable = readWord(hitemScripts, offsetInFile + 4);
+ if (command == setVarHidden && variable == Gen5Constants.hiddenItemVarSet) {
+ int item = readWord(hitemScripts, offsetInFile + 6);
+ fieldItems.add(item);
+ }
+
+ }
+
+ return fieldItems;
+ }
+
+ private void setFieldItems(List<Integer> fieldItems) {
+ Iterator<Integer> iterItems = fieldItems.iterator();
+
+ // normal items
+ int scriptFileNormal = romEntry.getInt("ItemBallsScriptOffset");
+ int scriptFileHidden = romEntry.getInt("HiddenItemsScriptOffset");
+ int[] skipTable = romEntry.arrayEntries.get("ItemBallsSkip");
+ int[] skipTableH = romEntry.arrayEntries.get("HiddenItemsSkip");
+ int setVarNormal = Gen5Constants.normalItemSetVarCommand;
+ int setVarHidden = Gen5Constants.hiddenItemSetVarCommand;
+
+ byte[] itemScripts = scriptNarc.files.get(scriptFileNormal);
+ int offset = 0;
+ int skipTableOffset = 0;
+ while (true) {
+ int part1 = readWord(itemScripts, offset);
+ if (part1 == Gen5Constants.scriptListTerminator) {
+ // done
+ break;
+ }
+ int offsetInFile = readRelativePointer(itemScripts, offset);
+ offset += 4;
+ if (offsetInFile > itemScripts.length) {
+ break;
+ }
+ if (skipTableOffset < skipTable.length && (skipTable[skipTableOffset] == (offset / 4) - 1)) {
+ skipTableOffset++;
+ continue;
+ }
+ int command = readWord(itemScripts, offsetInFile + 2);
+ int variable = readWord(itemScripts, offsetInFile + 4);
+ if (command == setVarNormal && variable == Gen5Constants.normalItemVarSet) {
+ int item = iterItems.next();
+ writeWord(itemScripts, offsetInFile + 6, item);
+ }
+
+ }
+
+ // hidden items
+ byte[] hitemScripts = scriptNarc.files.get(scriptFileHidden);
+ offset = 0;
+ skipTableOffset = 0;
+ while (true) {
+ int part1 = readWord(hitemScripts, offset);
+ if (part1 == Gen5Constants.scriptListTerminator) {
+ // done
+ break;
+ }
+ int offsetInFile = readRelativePointer(hitemScripts, offset);
+ offset += 4;
+ if (offsetInFile > hitemScripts.length) {
+ break;
+ }
+ if (skipTableOffset < skipTable.length && (skipTableH[skipTableOffset] == (offset / 4) - 1)) {
+ skipTableOffset++;
+ continue;
+ }
+ int command = readWord(hitemScripts, offsetInFile + 2);
+ int variable = readWord(hitemScripts, offsetInFile + 4);
+ if (command == setVarHidden && variable == Gen5Constants.hiddenItemVarSet) {
+ int item = iterItems.next();
+ writeWord(hitemScripts, offsetInFile + 6, item);
+ }
+
+ }
+ }
+
+ private int tmFromIndex(int index) {
+ if (index >= Gen5Constants.tmBlockOneOffset
+ && index < Gen5Constants.tmBlockOneOffset + Gen5Constants.tmBlockOneCount) {
+ return index - (Gen5Constants.tmBlockOneOffset - 1);
+ } else {
+ return (index + Gen5Constants.tmBlockOneCount) - (Gen5Constants.tmBlockTwoOffset - 1);
+ }
+ }
+
+ private int indexFromTM(int tm) {
+ if (tm >= 1 && tm <= Gen5Constants.tmBlockOneCount) {
+ return tm + (Gen5Constants.tmBlockOneOffset - 1);
+ } else {
+ return tm + (Gen5Constants.tmBlockTwoOffset - 1 - Gen5Constants.tmBlockOneCount);
+ }
+ }
+
+ @Override
+ public List<Integer> getCurrentFieldTMs() {
+ List<Integer> fieldItems = this.getFieldItems();
+ List<Integer> fieldTMs = new ArrayList<>();
+
+ for (int item : fieldItems) {
+ if (Gen5Constants.allowedItems.isTM(item)) {
+ fieldTMs.add(tmFromIndex(item));
+ }
+ }
+
+ return fieldTMs;
+ }
+
+ @Override
+ public void setFieldTMs(List<Integer> fieldTMs) {
+ List<Integer> fieldItems = this.getFieldItems();
+ int fiLength = fieldItems.size();
+ Iterator<Integer> iterTMs = fieldTMs.iterator();
+
+ for (int i = 0; i < fiLength; i++) {
+ int oldItem = fieldItems.get(i);
+ if (Gen5Constants.allowedItems.isTM(oldItem)) {
+ int newItem = indexFromTM(iterTMs.next());
+ fieldItems.set(i, newItem);
+ }
+ }
+
+ this.setFieldItems(fieldItems);
+ }
+
+ @Override
+ public List<Integer> getRegularFieldItems() {
+ List<Integer> fieldItems = this.getFieldItems();
+ List<Integer> fieldRegItems = new ArrayList<>();
+
+ for (int item : fieldItems) {
+ if (Gen5Constants.allowedItems.isAllowed(item) && !(Gen5Constants.allowedItems.isTM(item))) {
+ fieldRegItems.add(item);
+ }
+ }
+
+ return fieldRegItems;
+ }
+
+ @Override
+ public void setRegularFieldItems(List<Integer> items) {
+ List<Integer> fieldItems = this.getFieldItems();
+ int fiLength = fieldItems.size();
+ Iterator<Integer> iterNewItems = items.iterator();
+
+ for (int i = 0; i < fiLength; i++) {
+ int oldItem = fieldItems.get(i);
+ if (!(Gen5Constants.allowedItems.isTM(oldItem)) && Gen5Constants.allowedItems.isAllowed(oldItem)) {
+ int newItem = iterNewItems.next();
+ fieldItems.set(i, newItem);
+ }
+ }
+
+ this.setFieldItems(fieldItems);
+ }
+
+ @Override
+ public List<Integer> getRequiredFieldTMs() {
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ return Gen5Constants.bw1RequiredFieldTMs;
+ } else {
+ return Gen5Constants.bw2RequiredFieldTMs;
+ }
+ }
+
+ @Override
+ public List<IngameTrade> getIngameTrades() {
+ List<IngameTrade> trades = new ArrayList<>();
+ try {
+ NARCArchive tradeNARC = this.readNARC(romEntry.getFile("InGameTrades"));
+ List<String> tradeStrings = getStrings(false, romEntry.getInt("IngameTradesTextOffset"));
+ int[] unused = romEntry.arrayEntries.get("TradesUnused");
+ int unusedOffset = 0;
+ int tableSize = tradeNARC.files.size();
+
+ for (int entry = 0; entry < tableSize; entry++) {
+ if (unusedOffset < unused.length && unused[unusedOffset] == entry) {
+ unusedOffset++;
+ continue;
+ }
+ IngameTrade trade = new IngameTrade();
+ byte[] tfile = tradeNARC.files.get(entry);
+ trade.nickname = tradeStrings.get(entry * 2);
+ trade.givenPokemon = pokes[readLong(tfile, 4)];
+ trade.ivs = new int[6];
+ for (int iv = 0; iv < 6; iv++) {
+ trade.ivs[iv] = readLong(tfile, 0x10 + iv * 4);
+ }
+ trade.otId = readWord(tfile, 0x34);
+ trade.item = readLong(tfile, 0x4C);
+ trade.otName = tradeStrings.get(entry * 2 + 1);
+ trade.requestedPokemon = pokes[readLong(tfile, 0x5C)];
+ trades.add(trade);
+ }
+ } catch (Exception ex) {
+ throw new RandomizerIOException(ex);
+ }
+
+ return trades;
+
+ }
+
+ @Override
+ public void setIngameTrades(List<IngameTrade> trades) {
+ // info
+ int tradeOffset = 0;
+ List<IngameTrade> oldTrades = this.getIngameTrades();
+ try {
+ NARCArchive tradeNARC = this.readNARC(romEntry.getFile("InGameTrades"));
+ List<String> tradeStrings = getStrings(false, romEntry.getInt("IngameTradesTextOffset"));
+ int tradeCount = tradeNARC.files.size();
+ int[] unused = romEntry.arrayEntries.get("TradesUnused");
+ int unusedOffset = 0;
+ for (int i = 0; i < tradeCount; i++) {
+ if (unusedOffset < unused.length && unused[unusedOffset] == i) {
+ unusedOffset++;
+ continue;
+ }
+ byte[] tfile = tradeNARC.files.get(i);
+ IngameTrade trade = trades.get(tradeOffset++);
+ tradeStrings.set(i * 2, trade.nickname);
+ tradeStrings.set(i * 2 + 1, trade.otName);
+ writeLong(tfile, 4, trade.givenPokemon.number);
+ writeLong(tfile, 8, 0); // disable forme
+ for (int iv = 0; iv < 6; iv++) {
+ writeLong(tfile, 0x10 + iv * 4, trade.ivs[iv]);
+ }
+ writeLong(tfile, 0x2C, 0xFF); // random nature
+ writeWord(tfile, 0x34, trade.otId);
+ writeLong(tfile, 0x4C, trade.item);
+ writeLong(tfile, 0x5C, trade.requestedPokemon.number);
+ if (romEntry.tradeScripts.size() > 0) {
+ romEntry.tradeScripts.get(i - unusedOffset).setPokemon(this,scriptNarc,trade.requestedPokemon,trade.givenPokemon);
+ }
+ }
+ this.writeNARC(romEntry.getFile("InGameTrades"), tradeNARC);
+ this.setStrings(false, romEntry.getInt("IngameTradesTextOffset"), tradeStrings);
+ // update what the people say when they talk to you
+ unusedOffset = 0;
+ if (romEntry.arrayEntries.containsKey("IngameTradePersonTextOffsets")) {
+ int[] textOffsets = romEntry.arrayEntries.get("IngameTradePersonTextOffsets");
+ for (int tr = 0; tr < textOffsets.length; tr++) {
+ if (unusedOffset < unused.length && unused[unusedOffset] == tr+24) {
+ unusedOffset++;
+ continue;
+ }
+ if (textOffsets[tr] > 0) {
+ if (tr+24 >= oldTrades.size() || tr+24 >= trades.size()) {
+ break;
+ }
+ IngameTrade oldTrade = oldTrades.get(tr+24);
+ IngameTrade newTrade = trades.get(tr+24);
+ Map<String, String> replacements = new TreeMap<>();
+ replacements.put(oldTrade.givenPokemon.name, newTrade.givenPokemon.name);
+ if (oldTrade.requestedPokemon != newTrade.requestedPokemon) {
+ replacements.put(oldTrade.requestedPokemon.name, newTrade.requestedPokemon.name);
+ }
+ replaceAllStringsInEntry(textOffsets[tr], replacements);
+ }
+ }
+ }
+ } catch (IOException ex) {
+ throw new RandomizerIOException(ex);
+ }
+ }
+
+ private void replaceAllStringsInEntry(int entry, Map<String, String> replacements) {
+ List<String> thisTradeStrings = this.getStrings(true, entry);
+ int ttsCount = thisTradeStrings.size();
+ for (int strNum = 0; strNum < ttsCount; strNum++) {
+ String newString = thisTradeStrings.get(strNum);
+ for (String old: replacements.keySet()) {
+ newString = newString.replaceAll(old,replacements.get(old));
+ }
+ thisTradeStrings.set(strNum, newString);
+ }
+ this.setStrings(true, entry, thisTradeStrings);
+ }
+
+ @Override
+ public boolean hasDVs() {
+ return false;
+ }
+
+ @Override
+ public int generationOfPokemon() {
+ return 5;
+ }
+
+ @Override
+ public void removeEvosForPokemonPool() {
+ // slightly more complicated than gen2/3
+ // we have to update a "baby table" too
+ List<Pokemon> pokemonIncluded = this.mainPokemonList;
+ Set<Evolution> keepEvos = new HashSet<>();
+ for (Pokemon pk : pokes) {
+ if (pk != null) {
+ keepEvos.clear();
+ for (Evolution evol : pk.evolutionsFrom) {
+ if (pokemonIncluded.contains(evol.from) && pokemonIncluded.contains(evol.to)) {
+ keepEvos.add(evol);
+ } else {
+ evol.to.evolutionsTo.remove(evol);
+ }
+ }
+ pk.evolutionsFrom.retainAll(keepEvos);
+ }
+ }
+
+ try {
+ NARCArchive babyNARC = readNARC(romEntry.getFile("BabyPokemon"));
+ // baby pokemon
+ for (int i = 1; i <= Gen5Constants.pokemonCount; 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(babyNARC.files.get(i), 0, baby.number);
+ }
+ // finish up
+ writeNARC(romEntry.getFile("BabyPokemon"), babyNARC);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public boolean supportsFourStartingMoves() {
+ return true;
+ }
+
+ @Override
+ public List<Integer> getFieldMoves() {
+ // cut, fly, surf, strength, flash, dig, teleport, waterfall,
+ // sweet scent, dive
+ return Gen5Constants.fieldMoves;
+ }
+
+ @Override
+ public List<Integer> getEarlyRequiredHMMoves() {
+ // BW1: cut
+ // BW2: none
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ return Gen5Constants.bw2EarlyRequiredHMMoves;
+ } else {
+ return Gen5Constants.bw1EarlyRequiredHMMoves;
+ }
+ }
+
+ @Override
+ public Map<Integer, Shop> getShopItems() {
+ int[] tmShops = romEntry.arrayEntries.get("TMShops");
+ int[] regularShops = romEntry.arrayEntries.get("RegularShops");
+ int[] shopItemOffsets = romEntry.arrayEntries.get("ShopItemOffsets");
+ int[] shopItemSizes = romEntry.arrayEntries.get("ShopItemSizes");
+ int shopCount = romEntry.getInt("ShopCount");
+ List<Integer> shopItems = new ArrayList<>();
+ Map<Integer, Shop> shopItemsMap = new TreeMap<>();
+
+ try {
+ byte[] shopItemOverlay = readOverlay(romEntry.getInt("ShopItemOvlNumber"));
+ IntStream.range(0, shopCount).forEachOrdered(i -> {
+ boolean badShop = false;
+ for (int tmShop : tmShops) {
+ if (i == tmShop) {
+ badShop = true;
+ break;
+ }
+ }
+ for (int regularShop : regularShops) {
+ if (badShop) break;
+ if (i == regularShop) {
+ badShop = true;
+ break;
+ }
+ }
+ if (!badShop) {
+ List<Integer> items = new ArrayList<>();
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ for (int j = 0; j < shopItemSizes[i]; j++) {
+ items.add(readWord(shopItemOverlay, shopItemOffsets[i] + j * 2));
+ }
+ } else if (romEntry.romType == Gen5Constants.Type_BW2) {
+ byte[] shop = shopNarc.files.get(i);
+ for (int j = 0; j < shop.length; j += 2) {
+ items.add(readWord(shop, j));
+ }
+ }
+ Shop shop = new Shop();
+ shop.items = items;
+ shop.name = shopNames.get(i);
+ shop.isMainGame = Gen5Constants.getMainGameShops(romEntry.romType).contains(i);
+ shopItemsMap.put(i, shop);
+ }
+ });
+ return shopItemsMap;
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public void setShopItems(Map<Integer, Shop> shopItems) {
+ int[] shopItemOffsets = romEntry.arrayEntries.get("ShopItemOffsets");
+ int[] shopItemSizes = romEntry.arrayEntries.get("ShopItemSizes");
+ int[] tmShops = romEntry.arrayEntries.get("TMShops");
+ int[] regularShops = romEntry.arrayEntries.get("RegularShops");
+ int shopCount = romEntry.getInt("ShopCount");
+
+ try {
+ byte[] shopItemOverlay = readOverlay(romEntry.getInt("ShopItemOvlNumber"));
+ IntStream.range(0, shopCount).forEachOrdered(i -> {
+ boolean badShop = false;
+ for (int tmShop : tmShops) {
+ if (badShop) break;
+ if (i == tmShop) badShop = true;
+ }
+ for (int regularShop : regularShops) {
+ if (badShop) break;
+ if (i == regularShop) badShop = true;
+ }
+ if (!badShop) {
+ List<Integer> shopContents = shopItems.get(i).items;
+ Iterator<Integer> iterItems = shopContents.iterator();
+ if (romEntry.romType == Gen5Constants.Type_BW) {
+ for (int j = 0; j < shopItemSizes[i]; j++) {
+ Integer item = iterItems.next();
+ writeWord(shopItemOverlay, shopItemOffsets[i] + j * 2, item);
+ }
+ } else if (romEntry.romType == Gen5Constants.Type_BW2) {
+ byte[] shop = shopNarc.files.get(i);
+ for (int j = 0; j < shop.length; j += 2) {
+ Integer item = iterItems.next();
+ writeWord(shop, j, item);
+ }
+ }
+ }
+ });
+ if (romEntry.romType == Gen5Constants.Type_BW2) {
+ writeNARC(romEntry.getFile("ShopItems"), shopNarc);
+ } else {
+ writeOverlay(romEntry.getInt("ShopItemOvlNumber"), shopItemOverlay);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public void setShopPrices() {
+ try {
+ NARCArchive itemPriceNarc = this.readNARC(romEntry.getFile("ItemData"));
+ for (int i = 1; i < itemPriceNarc.files.size(); i++) {
+ writeWord(itemPriceNarc.files.get(i),0,Gen5Constants.balancedItemPrices.get(i));
+ }
+ writeNARC(romEntry.getFile("ItemData"),itemPriceNarc);
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ @Override
+ public List<PickupItem> getPickupItems() {
+ List<PickupItem> pickupItems = new ArrayList<>();
+ try {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("PickupOvlNumber"));
+
+ // If we haven't found the pickup table for this ROM already, find it.
+ if (pickupItemsTableOffset == 0) {
+ int offset = find(battleOverlay, Gen5Constants.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 < Gen5Constants.numberOfPickupItems; i++) {
+ int itemOffset = pickupItemsTableOffset + (2 * i);
+ int item = FileFunctions.read2ByteInt(battleOverlay, 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 startingRareItemOffset = levelRange;
+ int startingCommonItemOffset = 11 + 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;
+ }
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ return pickupItems;
+ }
+
+ @Override
+ public void setPickupItems(List<PickupItem> pickupItems) {
+ try {
+ if (pickupItemsTableOffset > 0) {
+ byte[] battleOverlay = readOverlay(romEntry.getInt("PickupOvlNumber"));
+ for (int i = 0; i < Gen5Constants.numberOfPickupItems; i++) {
+ int itemOffset = pickupItemsTableOffset + (2 * i);
+ int item = pickupItems.get(i).item;
+ FileFunctions.write2ByteInt(battleOverlay, itemOffset, item);
+ }
+ writeOverlay(romEntry.getInt("PickupOvlNumber"), battleOverlay);
+ }
+ } catch (IOException e) {
+ throw new RandomizerIOException(e);
+ }
+ }
+
+ private void computeCRC32sForRom() throws IOException {
+ this.actualOverlayCRC32s = new HashMap<>();
+ this.actualFileCRC32s = new HashMap<>();
+ this.actualArm9CRC32 = FileFunctions.getCRC32(arm9);
+ for (int overlayNumber : romEntry.overlayExpectedCRC32s.keySet()) {
+ byte[] overlay = readOverlay(overlayNumber);
+ long crc32 = FileFunctions.getCRC32(overlay);
+ this.actualOverlayCRC32s.put(overlayNumber, crc32);
+ }
+ 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() {
+ if (romEntry.arm9ExpectedCRC32 != actualArm9CRC32) {
+ return false;
+ }
+
+ for (int overlayNumber : romEntry.overlayExpectedCRC32s.keySet()) {
+ long expectedCRC32 = romEntry.overlayExpectedCRC32s.get(overlayNumber);
+ long actualCRC32 = actualOverlayCRC32s.get(overlayNumber);
+ if (expectedCRC32 != actualCRC32) {
+ return false;
+ }
+ }
+
+ for (String fileKey : romEntry.files.keySet()) {
+ long expectedCRC32 = romEntry.files.get(fileKey).expectedCRC32;
+ long actualCRC32 = actualFileCRC32s.get(fileKey);
+ if (expectedCRC32 != actualCRC32) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public BufferedImage getMascotImage() {
+ try {
+ Pokemon pk = randomPokemonInclFormes();
+ NARCArchive pokespritesNARC = this.readNARC(romEntry.getFile("PokemonGraphics"));
+
+ // First prepare the palette, it's the easy bit
+ int palIndex = pk.getSpriteIndex() * 20 + 18;
+ if (random.nextInt(10) == 0) {
+ // shiny
+ palIndex++;
+ }
+ byte[] rawPalette = pokespritesNARC.files.get(palIndex);
+ int[] palette = new int[16];
+ for (int i = 1; i < 16; i++) {
+ palette[i] = GFXFunctions.conv16BitColorToARGB(readWord(rawPalette, 40 + i * 2));
+ }
+
+ // Get the picture and uncompress it.
+ byte[] compressedPic = pokespritesNARC.files.get(pk.getSpriteIndex() * 20);
+ byte[] uncompressedPic = DSDecmp.Decompress(compressedPic);
+
+ // Output to 64x144 tiled image to prepare for unscrambling
+ BufferedImage bim = GFXFunctions.drawTiledImage(uncompressedPic, palette, 48, 64, 144, 4);
+
+ // Unscramble the above onto a 96x96 canvas
+ BufferedImage finalImage = new BufferedImage(96, 96, 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<Integer> getAllHeldItems() {
+ return Gen5Constants.allHeldItems;
+ }
+
+ @Override
+ public List<Integer> getAllConsumableHeldItems() {
+ return Gen5Constants.consumableHeldItems;
+ }
+
+ @Override
+ public List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, int[] pokeMoves) {
+ List<Integer> items = new ArrayList<>();
+ items.addAll(Gen5Constants.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(Gen5Constants.generalPurposeItems);
+ }
+ for (int moveIdx : pokeMoves) {
+ Move move = moves.get(moveIdx);
+ if (move == null) {
+ continue;
+ }
+ if (move.category == MoveCategory.PHYSICAL) {
+ items.add(Items.liechiBerry);
+ items.add(Gen5Constants.consumableTypeBoostingItems.get(move.type));
+ if (!consumableOnly) {
+ items.addAll(Gen5Constants.typeBoostingItems.get(move.type));
+ items.add(Items.choiceBand);
+ items.add(Items.muscleBand);
+ }
+ }
+ if (move.category == MoveCategory.SPECIAL) {
+ items.add(Items.petayaBerry);
+ items.add(Gen5Constants.consumableTypeBoostingItems.get(move.type));
+ if (!consumableOnly) {
+ items.addAll(Gen5Constants.typeBoostingItems.get(move.type));
+ items.add(Items.wiseGlasses);
+ items.add(Items.choiceSpecs);
+ }
+ }
+ if (!consumableOnly && Gen5Constants.moveBoostingItems.containsKey(moveIdx)) {
+ items.addAll(Gen5Constants.moveBoostingItems.get(moveIdx));
+ }
+ }
+ Map<Type, Effectiveness> byType = Effectiveness.against(tp.pokemon.primaryType, tp.pokemon.secondaryType, 5, effectivenessUpdated);
+ for(Map.Entry<Type, Effectiveness> entry : byType.entrySet()) {
+ Integer berry = Gen5Constants.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 (Gen5Constants.abilityBoostingItems.containsKey(ability)) {
+ items.addAll(Gen5Constants.abilityBoostingItems.get(ability));
+ }
+ if (tp.pokemon.primaryType == Type.POISON || tp.pokemon.secondaryType == Type.POISON) {
+ items.add(Items.blackSludge);
+ }
+ List<Integer> speciesItems = Gen5Constants.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;
+ }
+}
diff --git a/src/com/pkrandom/romhandlers/Gen6RomHandler.java b/src/com/pkrandom/romhandlers/Gen6RomHandler.java new file mode 100644 index 0000000..3148238 --- /dev/null +++ b/src/com/pkrandom/romhandlers/Gen6RomHandler.java @@ -0,0 +1,4270 @@ +package com.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 <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import com.pkrandom.*; +import com.pkrandom.constants.*; +import com.pkrandom.ctr.AMX; +import com.pkrandom.ctr.GARCArchive; +import com.pkrandom.ctr.Mini; +import com.pkrandom.exceptions.RandomizerIOException; +import com.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<String, RomFileEntry> files = new HashMap<>(); + private boolean staticPokemonSupport = true, copyStaticPokemon = true; + private Map<Integer, Integer> linkedStaticOffsets = new HashMap<>(); + private Map<String, String> strings = new HashMap<>(); + private Map<String, Integer> numbers = new HashMap<>(); + private Map<String, int[]> arrayEntries = new HashMap<>(); + private Map<String, OffsetWithinEntry[]> 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<RomEntry> 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<Integer,FormeInfo> formeMappings = new TreeMap<>(); + private Map<Integer,Map<Integer,Integer>> absolutePokeNumByBaseForme; + private Map<Integer,Integer> dummyAbsolutePokeNums; + private List<Pokemon> pokemonList; + private List<Pokemon> pokemonListInclFormes; + private List<MegaEvolution> megaEvolutions; + private Move[] moves; + private RomEntry romEntry; + private byte[] code; + private List<String> abilityNames; + private boolean loadedWildMapNames; + private Map<Integer, String> wildMapNames; + private int moveTutorMovesOffset; + private List<String> itemNames; + private List<String> shopNames; + private int shopItemsOffset; + private ItemList allowedItems, nonBadItems; + private int pickupItemsTableOffset; + private long actualCodeCRC32; + private Map<String, Long> 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<Integer,Integer> 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<Integer,FormeInfo> 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<String> 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<String> getStrings(boolean isStoryText, int index) { + GARCArchive baseGARC = isStoryText ? storyTextGarc : stringsGarc; + return getStrings(baseGARC, index); + } + + private List<String> 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<String> strings) { + GARCArchive baseGARC = isStoryText ? storyTextGarc : stringsGarc; + setStrings(baseGARC, index, strings); + } + + private void setStrings(GARCArchive textGARC, int index, List<String> 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<String> 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].effectIndex = readWord(moveData, 16); + 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].flinchPercentChance = moveData[15] & 0xFF; + moves[i].target = moveData[20] & 0xFF; + moves[i].category = Gen6Constants.moveCategoryIndices[moveData[2] & 0xFF]; + moves[i].priority = moveData[6]; + + int critStages = moveData[14] & 0xFF; + if (critStages == 6) { + moves[i].criticalChance = CriticalChance.GUARANTEED; + } else if (critStages > 0) { + moves[i].criticalChance = CriticalChance.INCREASED; + } + + int internalStatusType = readWord(moveData, 8); + int flags = FileFunctions.readFullInt(moveData, 32); + moves[i].makesContact = (flags & 0x001) != 0; + moves[i].isChargeMove = (flags & 0x002) != 0; + moves[i].isRechargeMove = (flags & 0x004) != 0; + moves[i].isPunchMove = (flags & 0x080) != 0; + moves[i].isSoundMove = (flags & 0x100) != 0; + moves[i].isTrapMove = internalStatusType == 8; + switch (moves[i].effectIndex) { + case Gen6Constants.noDamageTargetTrappingEffect: + case Gen6Constants.noDamageFieldTrappingEffect: + case Gen6Constants.damageAdjacentFoesTrappingEffect: + moves[i].isTrapMove = true; + break; + } + + int qualities = moveData[1]; + int recoilOrAbsorbPercent = moveData[18]; + if (qualities == Gen6Constants.damageAbsorbQuality) { + moves[i].absorbPercent = recoilOrAbsorbPercent; + } else { + moves[i].recoilPercent = -recoilOrAbsorbPercent; + } + + 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 + } + + switch (qualities) { + case Gen6Constants.noDamageStatChangeQuality: + case Gen6Constants.noDamageStatusAndStatChangeQuality: + // All Allies or Self + if (moves[i].target == 6 || moves[i].target == 7) { + moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER; + } else if (moves[i].target == 2) { + moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_ALLY; + } else if (moves[i].target == 8) { + moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_ALL; + } else { + moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET; + } + break; + case Gen6Constants.damageTargetDebuffQuality: + moves[i].statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET; + break; + case Gen6Constants.damageUserBuffQuality: + moves[i].statChangeMoveType = StatChangeMoveType.DAMAGE_USER; + break; + default: + moves[i].statChangeMoveType = StatChangeMoveType.NONE_OR_UNKNOWN; + break; + } + + for (int statChange = 0; statChange < 3; statChange++) { + moves[i].statChanges[statChange].type = StatChangeType.values()[moveData[21 + statChange]]; + moves[i].statChanges[statChange].stages = moveData[24 + statChange]; + moves[i].statChanges[statChange].percentChance = moveData[27 + statChange]; + } + + // Exclude status types that aren't in the StatusType enum. + if (internalStatusType < 7) { + moves[i].statusType = StatusType.values()[internalStatusType]; + if (moves[i].statusType == StatusType.POISON && (i == Moves.toxic || i == Moves.poisonFang)) { + moves[i].statusType = StatusType.TOXIC_POISON; + } + moves[i].statusPercentChance = moveData[10] & 0xFF; + switch (qualities) { + case Gen6Constants.noDamageStatusQuality: + case Gen6Constants.noDamageStatusAndStatChangeQuality: + moves[i].statusMoveType = StatusMoveType.NO_DAMAGE; + break; + case Gen6Constants.damageStatusQuality: + moves[i].statusMoveType = StatusMoveType.DAMAGE; + break; + } + } + } + } 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<String> 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<Integer> 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<Pokemon> getPokemon() { + return pokemonList; + } + + @Override + public List<Pokemon> getPokemonInclFormes() { + return pokemonListInclFormes; + } + + @Override + public List<Pokemon> getAltFormes() { + int formeCount = Gen6Constants.getFormeCount(romEntry.romType); + return pokemonListInclFormes.subList(Gen6Constants.pokemonCount + 1, Gen6Constants.pokemonCount + formeCount + 1); + } + + @Override + public List<MegaEvolution> 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<Pokemon> getIrregularFormes() { + return Gen6Constants.getIrregularFormes(romEntry.romType).stream().map(i -> pokes[i]).collect(Collectors.toList()); + } + + @Override + public boolean hasFunctionalFormes() { + return true; + } + + @Override + public List<Pokemon> getStarters() { + List<StaticEncounter> starters = new ArrayList<>(); + try { + byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); + + List<Integer> 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]; + 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<Pokemon> newStarters) { + try { + byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); + byte[] displayCRO = readFile(romEntry.getFile("StarterDisplay")); + + List<Integer> 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<Pokemon> starterIter = newStarters.iterator(); + + int displayIndex = 0; + + List<String> 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; + 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<Integer, StatChange> getUpdatedPokemonStats(int generation) { + Map<Integer, StatChange> map = GlobalConstants.getStatChanges(generation); + switch(generation) { + case 7: + map.put(Species.Gen6Formes.alakazamMega, new StatChange(Stat.SPDEF.val, 105)); + break; + case 8: + map.put(Species.Gen6Formes.aegislashB, new StatChange(Stat.ATK.val | Stat.SPATK.val, 140, 140)); + break; + } + return map; + } + + @Override + public boolean supportsStarterHeldItems() { + return true; + } + + @Override + public List<Integer> getStarterHeldItems() { + List<Integer> starterHeldItems = new ArrayList<>(); + try { + byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); + + List<Integer> 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; + int heldItem = FileFunctions.readFullInt(staticCRO,offset+i*size + 12); + if (heldItem < 0) { + heldItem = 0; + } + starterHeldItems.add(heldItem); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + return starterHeldItems; + } + + @Override + public void setStarterHeldItems(List<Integer> items) { + try { + byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); + + List<Integer> 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"); + + Iterator<Integer> itemsIter = items.iterator(); + + for (int i = 0; i < count; i++) { + if (!starterIndices.contains(i)) continue; + int item = itemsIter.next(); + if (item == 0) { + FileFunctions.writeFullInt(staticCRO,offset+i*size + 12,-1); + } else { + FileFunctions.writeFullInt(staticCRO,offset+i*size + 12,item); + } + } + writeFile(romEntry.getFile("StaticPokemon"),staticCRO); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public List<Move> getMoves() { + return Arrays.asList(moves); + } + + @Override + public List<EncounterSet> 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<EncounterSet> getEncountersXY() throws IOException { + GARCArchive encounterGarc = readGARC(romEntry.getFile("WildPokemon"), false); + List<EncounterSet> 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<EncounterSet> getEncountersORAS() throws IOException { + GARCArchive encounterGarc = readGARC(romEntry.getFile("WildPokemon"), false); + List<EncounterSet> 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<EncounterSet> encountersList) { + try { + if (romEntry.romType == Gen6Constants.Type_ORAS) { + setEncountersORAS(encountersList); + } else { + setEncountersXY(encountersList); + } + } catch (IOException ex) { + throw new RandomizerIOException(ex); + } + } + + private void setEncountersXY(List<EncounterSet> encountersList) throws IOException { + String encountersFile = romEntry.getFile("WildPokemon"); + GARCArchive encounterGarc = readGARC(encountersFile, false); + Iterator<EncounterSet> 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<EncounterSet> encountersList) throws IOException { + String encountersFile = romEntry.getFile("WildPokemon"); + GARCArchive encounterGarc = readGARC(encountersFile, false); + Iterator<EncounterSet> 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<Encounter> 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<Encounter> 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<String> 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<Trainer> getTrainers() { + List<Trainer> 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<String> tclasses = this.getTrainerClassNames(); + List<String> tnames = this.getTrainerNames(); + Map<Integer,String> 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.index = 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); + pokeOffs += 8; + if (tr.pokemonHaveItems()) { + tpk.heldItem = readWord(trpoke, pokeOffs); + pokeOffs += 2; + tpk.hasMegaStone = Gen6Constants.isMegaStone(tpk.heldItem); + } + if (tr.pokemonHaveCustomMoves()) { + for (int move = 0; move < 4; move++) { + tpk.moves[move] = readWord(trpoke, pokeOffs + (move*2)); + } + 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<Integer> getMainPlaythroughTrainers() { + return new ArrayList<>(); + } + + @Override + public List<Integer> getEliteFourTrainers(boolean isChallengeMode) { + return Arrays.stream(romEntry.arrayEntries.get("EliteFourIndices")).boxed().collect(Collectors.toList()); + } + + @Override + public List<Integer> getEvolutionItems() { + return Gen6Constants.evolutionItems; + } + + @Override + public void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode) { + Iterator<Trainer> 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<Integer, List<MoveLearnt>> 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<TrainerPokemon> 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(getAltFormeOfPokemon(tp.pokemon, tp.forme).number, movesets, tp.level); + for (int m = 0; m < 4; m++) { + writeWord(trpoke, pokeOffs + m * 2, pokeMoves[m]); + } + } else { + writeWord(trpoke, pokeOffs, tp.moves[0]); + writeWord(trpoke, pokeOffs + 2, tp.moves[1]); + writeWord(trpoke, pokeOffs + 4, tp.moves[2]); + writeWord(trpoke, pokeOffs + 6, tp.moves[3]); + } + 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<Integer, List<MoveLearnt>> getMovesLearnt() { + Map<Integer, List<MoveLearnt>> 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<MoveLearnt> 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<Integer, List<MoveLearnt>> 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<MoveLearnt> 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 Map<Integer, List<Integer>> getEggMoves() { + Map<Integer, List<Integer>> eggMoves = new TreeMap<>(); + try { + GARCArchive eggMovesGarc = this.readGARC(romEntry.getFile("EggMoves"),true); + for (int i = 1; i <= Gen6Constants.pokemonCount; i++) { + Pokemon pkmn = pokes[i]; + byte[] movedata = eggMovesGarc.files.get(i).get(0); + int numberOfEggMoves = readWord(movedata, 0); + List<Integer> moves = new ArrayList<>(); + for (int j = 0; j < numberOfEggMoves; j++) { + int move = readWord(movedata, 2 + (j * 2)); + moves.add(move); + } + eggMoves.put(pkmn.number, moves); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + return eggMoves; + } + + @Override + public void setEggMoves(Map<Integer, List<Integer>> eggMoves) { + try { + GARCArchive eggMovesGarc = this.readGARC(romEntry.getFile("EggMoves"), true); + for (int i = 1; i <= Gen6Constants.pokemonCount; i++) { + Pokemon pkmn = pokes[i]; + byte[] movedata = eggMovesGarc.files.get(i).get(0); + List<Integer> moves = eggMoves.get(pkmn.number); + for (int j = 0; j < moves.size(); j++) { + writeWord(movedata, 2 + (j * 2), moves.get(j)); + } + } + // Save + this.writeGARC(romEntry.getFile("EggMoves"), eggMovesGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public boolean canChangeStaticPokemon() { + return romEntry.staticPokemonSupport; + } + + @Override + public boolean hasStaticAltFormes() { + return true; + } + + @Override + public List<StaticEncounter> getStaticPokemon() { + List<StaticEncounter> 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<Integer> 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); + } + + // X/Y Trash Can Pokemon + if (romEntry.romType == Gen6Constants.Type_XY) { + int tableBaseOffset = find(code, Gen6Constants.xyTrashEncountersTablePrefix); + if (tableBaseOffset > 0) { + tableBaseOffset += Gen6Constants.xyTrashEncountersTablePrefix.length() / 2; // because it was a prefix + statics.addAll(readTrashCanEncounterSet(tableBaseOffset, Gen6Constants.pokemonVillageGarbadorOffset, Gen6Constants.pokemonVillageGarbadorCount, true)); + statics.addAll(readTrashCanEncounterSet(tableBaseOffset, Gen6Constants.pokemonVillageBanetteOffset, Gen6Constants.pokemonVillageBanetteCount, true)); + statics.addAll(readTrashCanEncounterSet(tableBaseOffset, Gen6Constants.lostHotelGarbadorOffset, Gen6Constants.lostHotelGarbadorCount, true)); + statics.addAll(readTrashCanEncounterSet(tableBaseOffset, Gen6Constants.lostHotelTrubbishOffset, Gen6Constants.lostHotelTrubbishCount, true)); + statics.addAll(readTrashCanEncounterSet(tableBaseOffset, Gen6Constants.lostHotelRotomOffset, Gen6Constants.lostHotelRotomCount, false)); + } + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + consolidateLinkedEncounters(statics); + return statics; + } + + private void consolidateLinkedEncounters(List<StaticEncounter> statics) { + List<StaticEncounter> encountersToRemove = new ArrayList<>(); + for (Map.Entry<Integer, Integer> 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); + } + } + + private List<StaticEncounter> readTrashCanEncounterSet(int tableBaseOffset, int offsetWithinTable, int count, + boolean consolidateSameSpeciesEncounters) { + List<StaticEncounter> statics = new ArrayList<>(); + Map<Pokemon, StaticEncounter> encounterSet = new HashMap<>(); + int offset = tableBaseOffset + (offsetWithinTable * Gen6Constants.xyTrashEncounterDataLength); + for (int i = offsetWithinTable; i < offsetWithinTable + count; i++) { + StaticEncounter se = readTrashCanEncounter(offset); + if (consolidateSameSpeciesEncounters && encounterSet.containsKey(se.pkmn)) { + StaticEncounter mainEncounter = encounterSet.get(se.pkmn); + mainEncounter.linkedEncounters.add(se); + } else { + statics.add(se); + encounterSet.put(se.pkmn, se); + } + offset += Gen6Constants.xyTrashEncounterDataLength; + } + return statics; + } + + private StaticEncounter readTrashCanEncounter(int offset) { + int species = FileFunctions.readFullInt(code, offset); + int forme = FileFunctions.readFullInt(code, offset + 4); + int level = FileFunctions.readFullInt(code, offset + 8); + StaticEncounter se = new StaticEncounter(); + Pokemon pokemon = pokes[species]; + 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 = level; + return se; + } + + @Override + public boolean setStaticPokemon(List<StaticEncounter> staticPokemon) { + // Static Pokemon + try { + byte[] staticCRO = readFile(romEntry.getFile("StaticPokemon")); + + unlinkStaticEncounters(staticPokemon); + Iterator<StaticEncounter> 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<Integer> 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) { + FileFunctions.writeFullInt(staticCRO,offset+i*size + 12,-1); + } else { + FileFunctions.writeFullInt(staticCRO,offset+i*size + 12,se.heldItem); + } + } + writeFile(romEntry.getFile("StaticPokemon"),staticCRO); + + // X/Y Trash Can Pokemon + if (romEntry.romType == Gen6Constants.Type_XY) { + offset = find(code, Gen6Constants.xyTrashEncountersTablePrefix); + if (offset > 0) { + offset += Gen6Constants.xyTrashEncountersTablePrefix.length() / 2; // because it was a prefix + int currentCount = 0; + while (currentCount != Gen6Constants.xyTrashCanEncounterCount) { + StaticEncounter se = staticIter.next(); + FileFunctions.writeFullInt(code, offset, se.pkmn.getBaseNumber()); + FileFunctions.writeFullInt(code, offset + 4, se.forme); + FileFunctions.writeFullInt(code, offset + 8, se.level); + offset += Gen6Constants.xyTrashEncounterDataLength; + currentCount++; + for (int i = 0; i < se.linkedEncounters.size(); i++) { + StaticEncounter linkedEncounter = se.linkedEncounters.get(i); + FileFunctions.writeFullInt(code, offset, linkedEncounter.pkmn.getBaseNumber()); + FileFunctions.writeFullInt(code, offset + 4, linkedEncounter.forme); + FileFunctions.writeFullInt(code, offset + 8, linkedEncounter.level); + offset += Gen6Constants.xyTrashEncounterDataLength; + currentCount++; + } + } + } + } + + 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<StaticEncounter> statics) { + List<Integer> offsetsToInsert = new ArrayList<>(); + for (Map.Entry<Integer, Integer> entry : romEntry.linkedStaticOffsets.entrySet()) { + offsetsToInsert.add(entry.getValue()); + } + Collections.sort(offsetsToInsert); + for (Integer offsetToInsert : offsetsToInsert) { + statics.add(offsetToInsert, new StaticEncounter()); + } + for (Map.Entry<Integer, Integer> 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<StaticEncounter> 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); + + // We also need to change the Sea Spirit's Den script in order for it to spawn + // the correct static version of the roamer. + try { + GARCArchive encounterGarc = readGARC(romEntry.getFile("WildPokemon"), false); + byte[] seaSpiritsDenAreaFile = encounterGarc.getFile(Gen6Constants.seaSpiritsDenEncounterFileXY); + AMX seaSpiritsDenAreaScript = new AMX(seaSpiritsDenAreaFile, 1); + for (int i = 0; i < roamers.length; i++) { + int offset = Gen6Constants.seaSpiritsDenScriptOffsetsXY[i]; + int species = roamers[i].pkmn.getBaseNumber(); + FileFunctions.write2ByteInt(seaSpiritsDenAreaScript.decData, offset, species); + } + byte[] modifiedScript = seaSpiritsDenAreaScript.getBytes(); + System.arraycopy(modifiedScript, 0, seaSpiritsDenAreaFile, Gen6Constants.seaSpiritsDenLocalScriptOffsetXY, modifiedScript.length); + encounterGarc.setFile(Gen6Constants.seaSpiritsDenEncounterFileXY, seaSpiritsDenAreaFile); + writeGARC(romEntry.getFile("WildPokemon"), encounterGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + 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(); + } + } + + @Override + public boolean isEffectivenessUpdated() { + return false; + } + + 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 void enableGuaranteedPokemonCatching() { + try { + byte[] battleCRO = readFile(romEntry.getFile("Battle")); + int offset = find(battleCRO, Gen6Constants.perfectOddsBranchLocator); + if (offset > 0) { + // The game checks to see if your odds are greater then or equal to 255 using the following + // code. Note that they compare to 0xFF000 instead of 0xFF; it looks like all catching code + // probabilities are shifted like this? + // cmp r6, #0xFF000 + // blt oddsLessThanOrEqualTo254 + // The below code just nops the branch out so it always acts like our odds are 255, and + // Pokemon are automatically caught no matter what. + battleCRO[offset] = 0x00; + battleCRO[offset + 1] = 0x00; + battleCRO[offset + 2] = 0x00; + battleCRO[offset + 3] = 0x00; + writeFile(romEntry.getFile("Battle"), battleCRO); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public List<Integer> getTMMoves() { + String tmDataPrefix = Gen6Constants.tmDataPrefix; + int offset = find(code, tmDataPrefix); + if (offset != 0) { + offset += Gen6Constants.tmDataPrefix.length() / 2; // because it was a prefix + List<Integer> 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<Integer> 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<Integer> 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<Integer> 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<String> itemDescriptions = getStrings(false, romEntry.getInt("ItemDescriptionsTextOffset")); + List<String> 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<Integer> 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<Pokemon, boolean[]> getTMHMCompatibility() { + Map<Pokemon, boolean[]> 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<Pokemon, boolean[]> compatData) { + for (Map.Entry<Pokemon, boolean[]> 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<Integer> getMoveTutorMoves() { + List<Integer> 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<Integer> 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<Pokemon, boolean[]> getMoveTutorCompatibility() { + Map<Pokemon, boolean[]> 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<Pokemon, boolean[]> 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<Integer> getMovesBannedFromLevelup() { + return Gen6Constants.bannedMoves; + } + + @Override + public boolean hasWildAltFormes() { + return true; + } + + @Override + public List<Pokemon> 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<Integer> getMainGameLegendaries() { + return Arrays.stream(romEntry.arrayEntries.get("MainGameLegendaries")).boxed().collect(Collectors.toList()); + } + + @Override + public List<Integer> getSpecialMusicStatics() { + return new ArrayList<>(); + } + + @Override + public void applyCorrectStaticMusic(Map<Integer, Integer> specialMusicStaticChanges) { + + } + + @Override + public boolean hasStaticMusicFix() { + return false; + } + + @Override + public List<TotemPokemon> getTotemPokemon() { + return new ArrayList<>(); + } + + @Override + public void setTotemPokemon(List<TotemPokemon> totemPokemon) { + + } + + @Override + public void removeImpossibleEvolutions(Settings settings) { + boolean changeMoveEvos = !(settings.getMovesetsMod() == Settings.MovesetsMod.UNCHANGED); + + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + Set<Evolution> 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); + + // Reduce the amount of happiness required to evolve. + int offset = find(code, Gen6Constants.friendshipValueForEvoLocator); + if (offset > 0) { + // Amount of required happiness for HAPPINESS evolutions. + if (code[offset] == (byte)220) { + code[offset] = (byte)160; + } + // Amount of required happiness for HAPPINESS_DAY evolutions. + if (code[offset + 12] == (byte)220) { + code[offset + 12] = (byte)160; + } + // Amount of required happiness for HAPPINESS_NIGHT evolutions. + if (code[offset + 36] == (byte)220) { + code[offset + 36] = (byte)160; + } + } + + 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<Evolution> 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<String> getTrainerNames() { + List<String> tnames = getStrings(false, romEntry.getInt("TrainerNamesTextOffset")); + tnames.remove(0); // blank one + + return tnames; + } + + @Override + public int maxTrainerNameLength() { + return 10; + } + + @Override + public void setTrainerNames(List<String> trainerNames) { + List<String> tnames = getStrings(false, romEntry.getInt("TrainerNamesTextOffset")); + List<String> 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<String> strings, int index) throws IOException { + List<String> 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<Integer> getTCNameLengthsByTrainer() { + return new ArrayList<>(); + } + + @Override + public List<String> getTrainerClassNames() { + return getStrings(false, romEntry.getInt("TrainerClassesTextOffset")); + } + + @Override + public void setTrainerClassNames(List<String> 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<Integer> getDoublesTrainerClasses() { + int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses"); + List<Integer> 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<Integer> getUniqueNoSellItems() { + return Gen6Constants.uniqueNoSellItems; + } + + @Override + public List<Integer> getRegularShopItems() { + return Gen6Constants.regularShopItems; + } + + @Override + public List<Integer> 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<Integer, List<Integer>> getAbilityVariations() { + return Gen5Constants.abilityVariations; + } + + @Override + public List<Integer> 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<Integer> 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<Integer> getCurrentFieldTMs() { + List<Integer> fieldItems = this.getFieldItems(); + List<Integer> 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<Integer> fieldTMs) { + List<Integer> fieldItems = this.getFieldItems(); + int fiLength = fieldItems.size(); + Iterator<Integer> 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<Integer> getRegularFieldItems() { + List<Integer> fieldItems = this.getFieldItems(); + List<Integer> 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<Integer> items) { + List<Integer> fieldItems = this.getFieldItems(); + int fiLength = fieldItems.size(); + Iterator<Integer> 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<Integer> getRequiredFieldTMs() { + return Gen6Constants.getRequiredFieldTMs(romEntry.romType); + } + + public List<Integer> getFieldItems() { + List<Integer> 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<Integer> fieldMegaStones = this.getFieldMegaStonesORAS(scriptGarc); + fieldItems.addAll(fieldMegaStones); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + return fieldItems; + } + + private List<Integer> getFieldMegaStonesORAS(GARCArchive scriptGarc) throws IOException { + List<Integer> 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<Integer> items) { + try { + Iterator<Integer> 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<Integer> fieldMegaStones = this.getFieldMegaStonesORAS(scriptGarc); + Map<Integer, Integer> 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<IngameTrade> getIngameTrades() { + List<IngameTrade> trades = new ArrayList<>(); + + int count = romEntry.getInt("IngameTradeCount"); + String prefix = Gen6Constants.getIngameTradesPrefix(romEntry.romType); + List<String> 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<IngameTrade> trades) { + List<IngameTrade> 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<String> 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<String> 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<Pokemon> pokemonIncluded = this.mainPokemonListInclFormes; + Set<Evolution> keepEvos = new HashSet<>(); + for (Pokemon pk : pokes) { + if (pk != null) { + keepEvos.clear(); + for (Evolution evol : pk.evolutionsFrom) { + if (pokemonIncluded.contains(evol.from) && pokemonIncluded.contains(evol.to)) { + keepEvos.add(evol); + } else { + evol.to.evolutionsTo.remove(evol); + } + } + pk.evolutionsFrom.retainAll(keepEvos); + } + } + + 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<Integer> getFieldMoves() { + if (romEntry.romType == Gen6Constants.Type_XY) { + return Gen6Constants.fieldMovesXY; + } else { + return Gen6Constants.fieldMovesORAS; + } + } + + @Override + public List<Integer> getEarlyRequiredHMMoves() { + return new ArrayList<>(); + } + + @Override + public Map<Integer, Shop> 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<Integer, Shop> 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<Integer> 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<Integer, Shop> 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<Integer> shopContents = shopItems.get(i).items; + Iterator<Integer> 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<PickupItem> getPickupItems() { + List<PickupItem> 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<PickupItem> 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) { + System.out.println(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<Integer> getAllHeldItems() { + return Gen6Constants.allHeldItems; + } + + @Override + public List<Integer> getAllConsumableHeldItems() { + return Gen6Constants.consumableHeldItems; + } + + @Override + public List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, int[] pokeMoves) { + List<Integer> 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 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<Type, Effectiveness> byType = Effectiveness.against(tp.pokemon.primaryType, tp.pokemon.secondaryType, 6); + for(Map.Entry<Type, Effectiveness> 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<Integer> 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; + } +} diff --git a/src/com/pkrandom/romhandlers/Gen7RomHandler.java b/src/com/pkrandom/romhandlers/Gen7RomHandler.java new file mode 100644 index 0000000..bfadd86 --- /dev/null +++ b/src/com/pkrandom/romhandlers/Gen7RomHandler.java @@ -0,0 +1,3821 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- Gen7RomHandler.java - randomizer handler for Su/Mo/US/UM. --*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import com.pkrandom.FileFunctions; +import com.pkrandom.MiscTweak; +import com.pkrandom.RomFunctions; +import com.pkrandom.Settings; +import com.pkrandom.constants.*; +import com.pkrandom.ctr.AMX; +import com.pkrandom.ctr.BFLIM; +import com.pkrandom.ctr.GARCArchive; +import com.pkrandom.ctr.Mini; +import com.pkrandom.exceptions.RandomizerIOException; +import com.pkrandom.pokemon.*; +import pptxt.N3DSTxtHandler; + +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.*; +import java.util.stream.Collectors; + +public class Gen7RomHandler extends Abstract3DSRomHandler { + + public static class Factory extends RomHandler.Factory { + + @Override + public Gen7RomHandler create(Random random, PrintStream logStream) { + return new Gen7RomHandler(random, logStream); + } + + public boolean isLoadable(String filename) { + return detect3DSRomInner(getProductCodeFromFile(filename), getTitleIdFromFile(filename)); + } + } + + public Gen7RomHandler(Random random) { + super(random, null); + } + + public Gen7RomHandler(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<String, RomFileEntry> files = new HashMap<>(); + private Map<Integer, Integer> linkedStaticOffsets = new HashMap<>(); + private Map<String, String> strings = new HashMap<>(); + private Map<String, Integer> numbers = new HashMap<>(); + private Map<String, int[]> arrayEntries = new HashMap<>(); + private Map<String, OffsetWithinEntry[]> 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<RomEntry> roms; + + static { + loadROMInfo(); + } + + private static void loadROMInfo() { + roms = new ArrayList<>(); + RomEntry current = null; + try { + Scanner sc = new Scanner(FileFunctions.openConfig("gen7_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("USUM")) { + current.romType = Gen7Constants.Type_USUM; + } else { + current.romType = Gen7Constants.Type_SM; + } + } else if (r[0].equals("TitleId")) { + current.titleId = r[1]; + } else if (r[0].equals("Acronym")) { + current.acronym = r[1]; + } 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[0].endsWith("Offset") || r[0].endsWith("Count") || r[0].endsWith("Number")) { + int offs = parseRIInt(r[1]); + current.numbers.put(r[0], offs); + } 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].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 { + 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<Integer,FormeInfo> formeMappings = new TreeMap<>(); + private Map<Integer,Map<Integer,Integer>> absolutePokeNumByBaseForme; + private Map<Integer,Integer> dummyAbsolutePokeNums; + private List<Pokemon> pokemonList; + private List<Pokemon> pokemonListInclFormes; + private List<MegaEvolution> megaEvolutions; + private List<AreaData> areaDataList; + private Move[] moves; + private RomEntry romEntry; + private byte[] code; + private List<String> itemNames; + private List<String> shopNames; + private List<String> abilityNames; + private ItemList allowedItems, nonBadItems; + private long actualCodeCRC32; + private Map<String, Long> actualFileCRC32s; + + private GARCArchive pokeGarc, moveGarc, encounterGarc, 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); + areaDataList = getAreaData(); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + loadPokemonStats(); + loadMoves(); + + pokemonListInclFormes = Arrays.asList(pokes); + pokemonList = Arrays.asList(Arrays.copyOfRange(pokes,0,Gen7Constants.getPokemonCount(romEntry.romType) + 1)); + + itemNames = getStrings(false,romEntry.getInt("ItemNamesTextOffset")); + abilityNames = getStrings(false,romEntry.getInt("AbilityNamesTextOffset")); + shopNames = Gen7Constants.getShopNames(romEntry.romType); + + allowedItems = Gen7Constants.getAllowedItems(romEntry.romType).copy(); + nonBadItems = Gen7Constants.nonBadItems.copy(); + + if (romEntry.romType == Gen7Constants.Type_SM) { + isSM = true; + } + + try { + computeCRC32sForRom(); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + private List<String> getStrings(boolean isStoryText, int index) { + GARCArchive baseGARC = isStoryText ? storyTextGarc : stringsGarc; + return getStrings(baseGARC, index); + } + + private List<String> 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<String> strings) { + GARCArchive baseGARC = isStoryText ? storyTextGarc : stringsGarc; + setStrings(baseGARC, index, strings); + } + + private void setStrings(GARCArchive textGARC, int index, List<String> 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 loadPokemonStats() { + try { + pokeGarc = this.readGARC(romEntry.getFile("PokemonStats"),true); + String[] pokeNames = readPokemonNames(); + int pokemonCount = Gen7Constants.getPokemonCount(romEntry.romType); + int formeCount = Gen7Constants.getFormeCount(romEntry.romType); + pokes = new Pokemon[pokemonCount + formeCount + 1]; + for (int i = 1; i <= 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 = pokemonCount + 1; + int formNum = 1; + int prevSpecies = 0; + Map<Integer,Integer> 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); + int realBaseForme = pokes[fi.baseForme].baseForme == null ? fi.baseForme : pokes[fi.baseForme].baseForme.number; + pokes[i].name = pokeNames[realBaseForme]; + pokes[i].baseForme = pokes[fi.baseForme]; + pokes[i].formeNumber = fi.formeNumber; + if (pokes[i].actuallyCosmetic) { + pokes[i].formeSuffix = pokes[i].baseForme.formeSuffix; + } else { + pokes[i].formeSuffix = Gen7Constants.getFormeSuffixByBaseForme(fi.baseForme,fi.formeNumber); + } + if (realBaseForme == prevSpecies) { + formNum++; + currentMap.put(formNum,i); + } else { + if (prevSpecies != 0) { + absolutePokeNumByBaseForme.put(prevSpecies,currentMap); + } + prevSpecies = realBaseForme; + 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<Integer,FormeInfo> altFormes) { + pkmn.hp = stats[Gen7Constants.bsHPOffset] & 0xFF; + pkmn.attack = stats[Gen7Constants.bsAttackOffset] & 0xFF; + pkmn.defense = stats[Gen7Constants.bsDefenseOffset] & 0xFF; + pkmn.speed = stats[Gen7Constants.bsSpeedOffset] & 0xFF; + pkmn.spatk = stats[Gen7Constants.bsSpAtkOffset] & 0xFF; + pkmn.spdef = stats[Gen7Constants.bsSpDefOffset] & 0xFF; + // Type + pkmn.primaryType = Gen7Constants.typeTable[stats[Gen7Constants.bsPrimaryTypeOffset] & 0xFF]; + pkmn.secondaryType = Gen7Constants.typeTable[stats[Gen7Constants.bsSecondaryTypeOffset] & 0xFF]; + // Only one type? + if (pkmn.secondaryType == pkmn.primaryType) { + pkmn.secondaryType = null; + } + pkmn.catchRate = stats[Gen7Constants.bsCatchRateOffset] & 0xFF; + pkmn.growthCurve = ExpCurve.fromByte(stats[Gen7Constants.bsGrowthCurveOffset]); + + pkmn.ability1 = stats[Gen7Constants.bsAbility1Offset] & 0xFF; + pkmn.ability2 = stats[Gen7Constants.bsAbility2Offset] & 0xFF; + pkmn.ability3 = stats[Gen7Constants.bsAbility3Offset] & 0xFF; + if (pkmn.ability1 == pkmn.ability2) { + pkmn.ability2 = 0; + } + + pkmn.callRate = stats[Gen7Constants.bsCallRateOffset] & 0xFF; + + // Held Items? + int item1 = FileFunctions.read2ByteInt(stats, Gen7Constants.bsCommonHeldItemOffset); + int item2 = FileFunctions.read2ByteInt(stats, Gen7Constants.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[Gen7Constants.bsFormeCountOffset] & 0xFF; + if (formeCount > 1) { + if (!altFormes.keySet().contains(pkmn.number)) { + int firstFormeOffset = FileFunctions.read2ByteInt(stats, Gen7Constants.bsFormeOffset); + if (firstFormeOffset != 0) { + int j = 0; + int jMax = 0; + int theAltForme = 0; + Set<Integer> altFormesWithCosmeticForms = Gen7Constants.getAltFormesWithCosmeticForms(romEntry.romType).keySet(); + for (int i = 1; i < formeCount; i++) { + if (j == 0 || j > jMax) { + altFormes.put(firstFormeOffset + i - 1,new FormeInfo(pkmn.number,i,FileFunctions.read2ByteInt(stats,Gen7Constants.bsFormeSpriteOffset))); // Assumes that formes are in memory in the same order as their numbers + if (Gen7Constants.getActuallyCosmeticForms(romEntry.romType).contains(firstFormeOffset+i-1)) { + if (!Gen7Constants.getIgnoreForms(romEntry.romType).contains(firstFormeOffset+i-1)) { // Skip ignored forms (identical or confusing cosmetic forms) + pkmn.cosmeticForms += 1; + pkmn.realCosmeticFormNumbers.add(i); + } + } + } else { + altFormes.put(firstFormeOffset + i - 1,new FormeInfo(theAltForme,j,FileFunctions.read2ByteInt(stats,Gen7Constants.bsFormeSpriteOffset))); + j++; + } + if (altFormesWithCosmeticForms.contains(firstFormeOffset + i - 1)) { + j = 1; + jMax = Gen7Constants.getAltFormesWithCosmeticForms(romEntry.romType).get(firstFormeOffset + i - 1); + theAltForme = firstFormeOffset + i - 1; + } + } + } else { + if (pkmn.number != Species.arceus && pkmn.number != Species.genesect && pkmn.number != Species.xerneas && pkmn.number != Species.silvally) { + // Reason for exclusions: + // Arceus/Genesect/Silvally: to avoid confusion + // Xerneas: Should be handled automatically? + pkmn.cosmeticForms = formeCount; + } + } + } else { + if (!Gen7Constants.getIgnoreForms(romEntry.romType).contains(pkmn.number)) { + pkmn.cosmeticForms = Gen7Constants.getAltFormesWithCosmeticForms(romEntry.romType).getOrDefault(pkmn.number,0); + } + if (Gen7Constants.getActuallyCosmeticForms(romEntry.romType).contains(pkmn.number)) { + pkmn.actuallyCosmetic = true; + } + } + } + + // The above code will add all alternate cosmetic forms to realCosmeticFormNumbers as necessary, but it will + // NOT add the base form. For example, if we are currently looking at Mimikyu, it will add Totem Mimikyu to + // the list of realCosmeticFormNumbers, but it will not add normal-sized Mimikyu. Without any corrections, + // this will make base Mimikyu impossible to randomly select. The simplest way to fix this is to just add + // the base form to the realCosmeticFormNumbers here if that list was populated above. + if (pkmn.realCosmeticFormNumbers.size() > 0) { + pkmn.realCosmeticFormNumbers.add(0); + pkmn.cosmeticForms += 1; + } + } + + private String[] readPokemonNames() { + int pokemonCount = Gen7Constants.getPokemonCount(romEntry.romType); + String[] pokeNames = new String[pokemonCount + 1]; + List<String> nameList = getStrings(false, romEntry.getInt("PokemonNamesTextOffset")); + for (int i = 1; i <= 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 <= Gen7Constants.getPokemonCount(romEntry.romType) + Gen7Constants.getFormeCount(romEntry.romType); i++) { + Pokemon pk = pokes[i]; + byte[] evoEntry = evoGARC.files.get(i).get(0); + boolean skipNext = false; + for (int evo = 0; evo < 8; evo++) { + int method = readWord(evoEntry, evo * 8); + int species = readWord(evoEntry, evo * 8 + 4); + if (method >= 1 && method <= Gen7Constants.evolutionMethodCount && species >= 1) { + EvolutionType et = EvolutionType.fromIndex(7, method); + if (et.skipSplitEvo()) continue; // Remove Feebas "split" evolution + if (skipNext) { + skipNext = false; + continue; + } + if (et == EvolutionType.LEVEL_GAME) { + skipNext = true; + } + + int extraInfo = readWord(evoEntry, evo * 8 + 2); + int forme = evoEntry[evo * 8 + 6]; + int level = evoEntry[evo * 8 + 7]; + Evolution evol = new Evolution(pk, getPokemonForEncounter(species,forme), true, et, extraInfo); + evol.forme = forme; + evol.level = level; + if (et.usesLevel()) { + evol.extraInfo = level; + } + switch (et) { + case LEVEL_GAME: + evol.type = EvolutionType.LEVEL; + evol.to = pokes[romEntry.getInt("CosmoemEvolutionNumber")]; + break; + case LEVEL_DAY_GAME: + evol.type = EvolutionType.LEVEL_DAY; + break; + case LEVEL_NIGHT_GAME: + evol.type = EvolutionType.LEVEL_NIGHT; + break; + default: + break; + } + if (pk.baseForme != null && pk.baseForme.number == Species.rockruff && pk.formeNumber > 0) { + evol.from = pk.baseForme; + pk.baseForme.evolutionsFrom.add(evol); + pokes[absolutePokeNumByBaseForme.get(species).get(evol.forme)].evolutionsTo.add(evol); + } + if (!pk.evolutionsFrom.contains(evol)) { + pk.evolutionsFrom.add(evol); + if (!pk.actuallyCosmetic) { + if (evol.forme > 0) { + // The forme number for the evolution might represent an actual alt forme, or it + // might simply represent a cosmetic forme. If it represents an actual alt forme, + // we'll need to figure out what the absolute species ID for that alt forme is + // and update its evolutions. If it instead represents a cosmetic forme, then the + // absolutePokeNumByBaseFormeMap will be null, since there's no secondary species + // entry for this forme. + Map<Integer, Integer> absolutePokeNumByBaseFormeMap = absolutePokeNumByBaseForme.get(species); + if (absolutePokeNumByBaseFormeMap != null) { + species = absolutePokeNumByBaseFormeMap.get(evol.forme); + } + } + 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 and 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); + evol.forme = -1; + evol.level = 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 <= Gen7Constants.getPokemonCount(romEntry.romType); i++) { + Pokemon pk = pokes[i]; + byte[] megaEvoEntry = megaEvoGARC.files.get(i).get(0); + for (int evo = 0; evo < 2; 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 void loadMoves() { + try { + moveGarc = this.readGARC(romEntry.getFile("MoveData"),true); + int moveCount = Gen7Constants.getMoveCount(romEntry.romType); + moves = new Move[moveCount + 1]; + List<String> moveNames = getStrings(false, romEntry.getInt("MoveNamesTextOffset")); + byte[][] movesData = Mini.UnpackMini(moveGarc.files.get(0).get(0), "WD"); + for (int i = 1; i <= moveCount; i++) { + byte[] moveData = movesData[i]; + moves[i] = new Move(); + moves[i].name = moveNames.get(i); + moves[i].number = i; + moves[i].internalId = i; + moves[i].effectIndex = readWord(moveData, 16); + moves[i].hitratio = (moveData[4] & 0xFF); + moves[i].power = moveData[3] & 0xFF; + moves[i].pp = moveData[5] & 0xFF; + moves[i].type = Gen7Constants.typeTable[moveData[0] & 0xFF]; + moves[i].flinchPercentChance = moveData[15] & 0xFF; + moves[i].target = moveData[20] & 0xFF; + moves[i].category = Gen7Constants.moveCategoryIndices[moveData[2] & 0xFF]; + moves[i].priority = moveData[6]; + + int critStages = moveData[14] & 0xFF; + if (critStages == 6) { + moves[i].criticalChance = CriticalChance.GUARANTEED; + } else if (critStages > 0) { + moves[i].criticalChance = CriticalChance.INCREASED; + } + + int internalStatusType = readWord(moveData, 8); + int flags = FileFunctions.readFullInt(moveData, 36); + moves[i].makesContact = (flags & 0x001) != 0; + moves[i].isChargeMove = (flags & 0x002) != 0; + moves[i].isRechargeMove = (flags & 0x004) != 0; + moves[i].isPunchMove = (flags & 0x080) != 0; + moves[i].isSoundMove = (flags & 0x100) != 0; + moves[i].isTrapMove = internalStatusType == 8; + switch (moves[i].effectIndex) { + case Gen7Constants.noDamageTargetTrappingEffect: + case Gen7Constants.noDamageFieldTrappingEffect: + case Gen7Constants.damageAdjacentFoesTrappingEffect: + case Gen7Constants.damageTargetTrappingEffect: + moves[i].isTrapMove = true; + break; + } + + int qualities = moveData[1]; + int recoilOrAbsorbPercent = moveData[18]; + if (qualities == Gen7Constants.damageAbsorbQuality) { + moves[i].absorbPercent = recoilOrAbsorbPercent; + } else { + moves[i].recoilPercent = -recoilOrAbsorbPercent; + } + + 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 + } + + switch (qualities) { + case Gen7Constants.noDamageStatChangeQuality: + case Gen7Constants.noDamageStatusAndStatChangeQuality: + // All Allies or Self + if (moves[i].target == 6 || moves[i].target == 7) { + moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER; + } else if (moves[i].target == 2) { + moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_ALLY; + } else if (moves[i].target == 8) { + moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_ALL; + } else { + moves[i].statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET; + } + break; + case Gen7Constants.damageTargetDebuffQuality: + moves[i].statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET; + break; + case Gen7Constants.damageUserBuffQuality: + moves[i].statChangeMoveType = StatChangeMoveType.DAMAGE_USER; + break; + default: + moves[i].statChangeMoveType = StatChangeMoveType.NONE_OR_UNKNOWN; + break; + } + + for (int statChange = 0; statChange < 3; statChange++) { + moves[i].statChanges[statChange].type = StatChangeType.values()[moveData[21 + statChange]]; + moves[i].statChanges[statChange].stages = moveData[24 + statChange]; + moves[i].statChanges[statChange].percentChance = moveData[27 + statChange]; + } + + // Exclude status types that aren't in the StatusType enum. + if (internalStatusType < 7) { + moves[i].statusType = StatusType.values()[internalStatusType]; + if (moves[i].statusType == StatusType.POISON && (i == Moves.toxic || i == Moves.poisonFang)) { + moves[i].statusType = StatusType.TOXIC_POISON; + } + moves[i].statusPercentChance = moveData[10] & 0xFF; + switch (qualities) { + case Gen7Constants.noDamageStatusQuality: + case Gen7Constants.noDamageStatusAndStatChangeQuality: + moves[i].statusMoveType = StatusMoveType.NO_DAMAGE; + break; + case Gen7Constants.damageStatusQuality: + moves[i].statusMoveType = StatusMoveType.DAMAGE; + break; + } + } + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + protected void savingROM() { + savePokemonStats(); + saveMoves(); + try { + writeCode(code); + writeGARC(romEntry.getFile("WildPokemon"), encounterGarc); + writeGARC(romEntry.getFile("TextStrings"), stringsGarc); + writeGARC(romEntry.getFile("StoryText"), storyTextGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + private void savePokemonStats() { + int k = Gen7Constants.bsSize; + int pokemonCount = Gen7Constants.getPokemonCount(romEntry.romType); + int formeCount = Gen7Constants.getFormeCount(romEntry.romType); + byte[] duplicateData = pokeGarc.files.get(pokemonCount + formeCount + 1).get(0); + for (int i = 1; i <= pokemonCount + formeCount; 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[Gen7Constants.bsHPOffset] = (byte) pkmn.hp; + stats[Gen7Constants.bsAttackOffset] = (byte) pkmn.attack; + stats[Gen7Constants.bsDefenseOffset] = (byte) pkmn.defense; + stats[Gen7Constants.bsSpeedOffset] = (byte) pkmn.speed; + stats[Gen7Constants.bsSpAtkOffset] = (byte) pkmn.spatk; + stats[Gen7Constants.bsSpDefOffset] = (byte) pkmn.spdef; + stats[Gen7Constants.bsPrimaryTypeOffset] = Gen7Constants.typeToByte(pkmn.primaryType); + if (pkmn.secondaryType == null) { + stats[Gen7Constants.bsSecondaryTypeOffset] = stats[Gen7Constants.bsPrimaryTypeOffset]; + } else { + stats[Gen7Constants.bsSecondaryTypeOffset] = Gen7Constants.typeToByte(pkmn.secondaryType); + } + stats[Gen7Constants.bsCatchRateOffset] = (byte) pkmn.catchRate; + stats[Gen7Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte(); + + stats[Gen7Constants.bsAbility1Offset] = (byte) pkmn.ability1; + stats[Gen7Constants.bsAbility2Offset] = pkmn.ability2 != 0 ? (byte) pkmn.ability2 : (byte) pkmn.ability1; + stats[Gen7Constants.bsAbility3Offset] = (byte) pkmn.ability3; + + stats[Gen7Constants.bsCallRateOffset] = (byte) pkmn.callRate; + + // Held items + if (pkmn.guaranteedHeldItem > 0) { + FileFunctions.write2ByteInt(stats, Gen7Constants.bsCommonHeldItemOffset, pkmn.guaranteedHeldItem); + FileFunctions.write2ByteInt(stats, Gen7Constants.bsRareHeldItemOffset, pkmn.guaranteedHeldItem); + FileFunctions.write2ByteInt(stats, Gen7Constants.bsDarkGrassHeldItemOffset, 0); + } else { + FileFunctions.write2ByteInt(stats, Gen7Constants.bsCommonHeldItemOffset, pkmn.commonHeldItem); + FileFunctions.write2ByteInt(stats, Gen7Constants.bsRareHeldItemOffset, pkmn.rareHeldItem); + FileFunctions.write2ByteInt(stats, Gen7Constants.bsDarkGrassHeldItemOffset, 0); + } + + if (pkmn.fullName().equals("Meowstic")) { + stats[Gen7Constants.bsGenderOffset] = 0; + } else if (pkmn.fullName().equals("Meowstic-F")) { + stats[Gen7Constants.bsGenderOffset] = (byte)0xFE; + } + } + + private void writeEvolutions() { + try { + GARCArchive evoGARC = readGARC(romEntry.getFile("PokemonEvolutions"),true); + for (int i = 1; i <= Gen7Constants.getPokemonCount(romEntry.romType) + Gen7Constants.getFormeCount(romEntry.romType); i++) { + byte[] evoEntry = evoGARC.files.get(i).get(0); + Pokemon pk = pokes[i]; + if (pk.number == Species.nincada) { + writeShedinjaEvolution(); + } + int evosWritten = 0; + for (Evolution evo : pk.evolutionsFrom) { + Pokemon toPK = evo.to; + writeWord(evoEntry, evosWritten * 8, evo.type.toIndex(7)); + writeWord(evoEntry, evosWritten * 8 + 2, evo.type.usesLevel() ? 0 : evo.extraInfo); + writeWord(evoEntry, evosWritten * 8 + 4, toPK.getBaseNumber()); + evoEntry[evosWritten * 8 + 6] = (byte)evo.forme; + evoEntry[evosWritten * 8 + 7] = evo.type.usesLevel() ? (byte)evo.extraInfo : (byte)evo.level; + evosWritten++; + if (evosWritten == 8) { + break; + } + } + while (evosWritten < 8) { + writeWord(evoEntry, evosWritten * 8, 0); + writeWord(evoEntry, evosWritten * 8 + 2, 0); + writeWord(evoEntry, evosWritten * 8 + 4, 0); + writeWord(evoEntry, evosWritten * 8 + 6, 0); + evosWritten++; + } + } + writeGARC(romEntry.getFile("PokemonEvolutions"), evoGARC); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + private void writeShedinjaEvolution() { + 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 game's executable, there's a hardcoded check to see if the Pokemon + // that just evolved is now a Ninjask after evolving; if it is, then we start + // going down the path of creating a Shedinja. To accomplish this check, they + // hardcoded Ninjask's species ID as a constant. We replace this constant + // with the species ID of Nincada's new primary evolution; that way, evolving + // Nincada will still produce an "extra" Pokemon like in older generations. + int offset = find(code, Gen7Constants.ninjaskSpeciesPrefix); + if (offset > 0) { + offset += Gen7Constants.ninjaskSpeciesPrefix.length() / 2; // because it was a prefix + FileFunctions.writeFullInt(code, offset, primaryEvolution.getBaseNumber()); + } + + // 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, Gen7Constants.shedinjaPrefix); + if (offset > 0) { + offset += Gen7Constants.shedinjaPrefix.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 = 84; i < 120; 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<Integer> blOffsetsToPatch = Arrays.asList(84, 96, 108); + for (int blOffsetToPatch : blOffsetsToPatch) { + code[offset + blOffsetToPatch] += 1; + } + + // Write Nincada's new extra evolution in the new free space. + writeLong(code, offset + 120, extraEvolution.getBaseNumber()); + + // Second parameter of pml::pokepara::CoreParam::ChangeMonsNo is the + // new forme number + code[offset] = (byte) extraEvolution.formeNumber; + + // First parameter of pml::pokepara::CoreParam::ChangeMonsNo is the + // new species number. Write a pc-relative load to what we wrote before. + code[offset + 4] = (byte) 0x6C; + code[offset + 5] = 0x10; + code[offset + 6] = (byte) 0x9F; + code[offset + 7] = (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); + } + + private void saveMoves() { + int moveCount = Gen7Constants.getMoveCount(romEntry.romType); + byte[][] movesData = Mini.UnpackMini(moveGarc.files.get(0).get(0), "WD"); + for (int i = 1; i <= moveCount; i++) { + byte[] moveData = movesData[i]; + moveData[2] = Gen7Constants.moveCategoryToByte(moves[i].category); + moveData[3] = (byte) moves[i].power; + moveData[0] = Gen7Constants.typeToByte(moves[i].type); + int hitratio = (int) Math.round(moves[i].hitratio); + if (hitratio < 0) { + hitratio = 0; + } + if (hitratio > 101) { + hitratio = 100; + } + moveData[4] = (byte) hitratio; + moveData[5] = (byte) moves[i].pp; + } + try { + moveGarc.setFile(0, Mini.PackMini(movesData, "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, all Primal Reversions, + // all Greninja-A, all Zygarde-C, and all Necrozma-U in the player's + // party are set back to their base forme. This patches .code such + // that this reversion does not happen. + String saveLoadFormeReversionPrefix = Gen7Constants.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 8 bytes from the end of + // the prefix. We have to do this because these 8 bytes differ between the + // base game and all game updates, so we cannot use them as part of our prefix. + offset += 8; + + // 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; + + if (romEntry.romType == Gen7Constants.Type_USUM) { + // In Sun/Moon, Greninja-A and Zygarde-C are treated as Mega Pokemon + // and handled by the function above. In USUM, they are handled by a + // different function, along with Necrozma-U. This stubs the call + // to that function. + code[offset + 8] = 0x00; + code[offset + 9] = 0x00; + code[offset + 10] = 0x00; + code[offset + 11] = 0x00; + } + } + + // Additionally, upon completing a battle, Kyogre-P, Groudon-P, + // and Wishiwashi-S are forcibly returned to their base forme. + // Minior is also forcibly set to the "correct" Core forme. + // This patches the Battle CRO to prevent this from happening. + byte[] battleCRO = readFile(romEntry.getFile("Battle")); + offset = find(battleCRO, Gen7Constants.afterBattleFormeReversionPrefix); + if (offset > 0) { + offset += Gen7Constants.afterBattleFormeReversionPrefix.length() / 2; // because it was a prefix + + // Stubs the call to pml::pokepara::CoreParam::ChangeFormNo for Kyogre + battleCRO[offset] = 0x00; + battleCRO[offset + 1] = 0x00; + battleCRO[offset + 2] = 0x00; + battleCRO[offset + 3] = 0x00; + + // Stubs the call to pml::pokepara::CoreParam::ChangeFormNo for Groudon + battleCRO[offset + 60] = 0x00; + battleCRO[offset + 61] = 0x00; + battleCRO[offset + 62] = 0x00; + battleCRO[offset + 63] = 0x00; + + // Stubs the call to pml::pokepara::CoreParam::ChangeFormNo for Wishiwashi + battleCRO[offset + 92] = 0x00; + battleCRO[offset + 93] = 0x00; + battleCRO[offset + 94] = 0x00; + battleCRO[offset + 95] = 0x00; + + // Stubs the call to pml::pokepara::CoreParam::ChangeFormNo for Minior + battleCRO[offset + 148] = 0x00; + battleCRO[offset + 149] = 0x00; + battleCRO[offset + 150] = 0x00; + battleCRO[offset + 151] = 0x00; + + writeFile(romEntry.getFile("Battle"), battleCRO); + } + } + + @Override + protected String getGameAcronym() { + return romEntry.acronym; + } + + @Override + protected boolean isGameUpdateSupported(int version) { + return version == romEntry.numbers.get("FullyUpdatedVersionNumber"); + } + + @Override + protected String getGameVersion() { + List<String> 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"; + } + + @Override + public List<Pokemon> getPokemon() { + return pokemonList; + } + + @Override + public List<Pokemon> getPokemonInclFormes() { + return pokemonListInclFormes; + } + + @Override + public List<Pokemon> getAltFormes() { + int formeCount = Gen7Constants.getFormeCount(romEntry.romType); + int pokemonCount = Gen7Constants.getPokemonCount(romEntry.romType); + return pokemonListInclFormes.subList(pokemonCount + 1, pokemonCount + formeCount + 1); + } + + @Override + public List<MegaEvolution> 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].actuallyCosmetic ? pokes[pokeNum] : pokes[pokeNum].baseForme : pk; + } + + @Override + public List<Pokemon> getIrregularFormes() { + return Gen7Constants.getIrregularFormes(romEntry.romType).stream().map(i -> pokes[i]).collect(Collectors.toList()); + } + + @Override + public boolean hasFunctionalFormes() { + return true; + } + + @Override + public List<Pokemon> getStarters() { + List<StaticEncounter> starters = new ArrayList<>(); + try { + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + byte[] giftsFile = staticGarc.files.get(0).get(0); + for (int i = 0; i < 3; i++) { + int offset = i * 0x14; + StaticEncounter se = new StaticEncounter(); + int species = FileFunctions.read2ByteInt(giftsFile, offset); + Pokemon pokemon = pokes[species]; + int forme = giftsFile[offset + 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 = giftsFile[offset + 3]; + 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<Pokemon> newStarters) { + try { + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + byte[] giftsFile = staticGarc.files.get(0).get(0); + for (int i = 0; i < 3; i++) { + int offset = i * 0x14; + Pokemon starter = newStarters.get(i); + int forme = 0; + boolean checkCosmetics = true; + if (starter.formeNumber > 0) { + forme = starter.formeNumber; + starter = starter.baseForme; + checkCosmetics = false; + } + if (checkCosmetics && starter.cosmeticForms > 0) { + forme = starter.getCosmeticFormNumber(this.random.nextInt(starter.cosmeticForms)); + } else if (!checkCosmetics && starter.cosmeticForms > 0) { + forme += starter.getCosmeticFormNumber(this.random.nextInt(starter.cosmeticForms)); + } + writeWord(giftsFile, offset, starter.number); + giftsFile[offset + 2] = (byte) forme; + } + writeGARC(romEntry.getFile("StaticPokemon"), staticGarc); + setStarterText(newStarters); + return true; + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + // TODO: We should be editing the script file so that the game reads in our new + // starters; this way, strings that depend on the starter defined in the script + // would work without any modification. Instead, we're just manually editing all + // strings here, and if a string originally referred to the starter in the script, + // we just hardcode the starter's name if we can get away with it. + private void setStarterText(List<Pokemon> newStarters) { + int starterTextIndex = romEntry.getInt("StarterTextOffset"); + List<String> starterText = getStrings(true, starterTextIndex); + if (romEntry.romType == Gen7Constants.Type_USUM) { + String rowletDescriptor = newStarters.get(0).name + starterText.get(1).substring(6); + String littenDescriptor = newStarters.get(1).name + starterText.get(2).substring(6); + String popplioDescriptor = newStarters.get(2).name + starterText.get(3).substring(7); + starterText.set(1, rowletDescriptor); + starterText.set(2, littenDescriptor); + starterText.set(3, popplioDescriptor); + for (int i = 0; i < 3; i++) { + int confirmationOffset = i + 7; + int optionOffset = i + 14; + Pokemon starter = newStarters.get(i); + String confirmationText = String.format("So, you wanna go with the %s-type Pokémon\\n%s?[VAR 0114(0005)]", + starter.primaryType.camelCase(), starter.name); + String optionText = starter.name; + starterText.set(confirmationOffset, confirmationText); + starterText.set(optionOffset, optionText); + } + } else { + String rowletDescriptor = newStarters.get(0).name + starterText.get(11).substring(6); + String littenDescriptor = newStarters.get(1).name + starterText.get(12).substring(6); + String popplioDescriptor = newStarters.get(2).name + starterText.get(13).substring(7); + starterText.set(11, rowletDescriptor); + starterText.set(12, littenDescriptor); + starterText.set(13, popplioDescriptor); + for (int i = 0; i < 3; i++) { + int optionOffset = i + 1; + int confirmationOffset = i + 4; + int flavorOffset = i + 35; + Pokemon starter = newStarters.get(i); + String optionText = String.format("The %s-type %s", starter.primaryType.camelCase(), starter.name); + String confirmationText = String.format("Will you choose the %s-type Pokémon\\n%s?[VAR 0114(0008)]", + starter.primaryType.camelCase(), starter.name); + String flavorSubstring = starterText.get(flavorOffset).substring(starterText.get(flavorOffset).indexOf("\\n")); + String flavorText = String.format("The %s-type %s", starter.primaryType.camelCase(), starter.name) + flavorSubstring; + starterText.set(optionOffset, optionText); + starterText.set(confirmationOffset, confirmationText); + starterText.set(flavorOffset, flavorText); + } + } + setStrings(true, starterTextIndex, starterText); + } + + @Override + public boolean hasStarterAltFormes() { + return true; + } + + @Override + public int starterCount() { + return 3; + } + + @Override + public Map<Integer, StatChange> getUpdatedPokemonStats(int generation) { + Map<Integer, StatChange> map = GlobalConstants.getStatChanges(generation); + int aegislashBlade = Species.SMFormes.aegislashB; + if (romEntry.romType == Gen7Constants.Type_USUM) { + aegislashBlade = Species.USUMFormes.aegislashB; + } + switch(generation) { + case 8: + map.put(aegislashBlade, new StatChange(Stat.ATK.val | Stat.SPATK.val, 140, 140)); + break; + } + return map; + } + + @Override + public boolean supportsStarterHeldItems() { + return true; + } + + @Override + public List<Integer> getStarterHeldItems() { + List<Integer> starterHeldItems = new ArrayList<>(); + try { + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + byte[] giftsFile = staticGarc.files.get(0).get(0); + for (int i = 0; i < 3; i++) { + int offset = i * 0x14; + int item = FileFunctions.read2ByteInt(giftsFile, offset + 8); + starterHeldItems.add(item); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + return starterHeldItems; + } + + @Override + public void setStarterHeldItems(List<Integer> items) { + try { + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + byte[] giftsFile = staticGarc.files.get(0).get(0); + for (int i = 0; i < 3; i++) { + int offset = i * 0x14; + int item = items.get(i); + FileFunctions.write2ByteInt(giftsFile, offset + 8, item); + } + writeGARC(romEntry.getFile("StaticPokemon"), staticGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public List<Move> getMoves() { + return Arrays.asList(moves); + } + + @Override + public List<EncounterSet> getEncounters(boolean useTimeOfDay) { + List<EncounterSet> encounters = new ArrayList<>(); + for (AreaData areaData : areaDataList) { + if (!areaData.hasTables) { + continue; + } + for (int i = 0; i < areaData.encounterTables.size(); i++) { + byte[] encounterTable = areaData.encounterTables.get(i); + byte[] dayTable = new byte[0x164]; + System.arraycopy(encounterTable, 0, dayTable, 0, 0x164); + EncounterSet dayEncounters = readEncounterTable(dayTable); + if (!useTimeOfDay) { + dayEncounters.displayName = areaData.name + ", Table " + (i + 1); + encounters.add(dayEncounters); + } else { + dayEncounters.displayName = areaData.name + ", Table " + (i + 1) + " (Day)"; + encounters.add(dayEncounters); + byte[] nightTable = new byte[0x164]; + System.arraycopy(encounterTable, 0x164, nightTable, 0, 0x164); + EncounterSet nightEncounters = readEncounterTable(nightTable); + nightEncounters.displayName = areaData.name + ", Table " + (i + 1) + " (Night)"; + encounters.add(nightEncounters); + } + } + } + return encounters; + } + + private EncounterSet readEncounterTable(byte[] encounterTable) { + int minLevel = encounterTable[0]; + int maxLevel = encounterTable[1]; + EncounterSet es = new EncounterSet(); + es.rate = 1; + for (int i = 0; i < 10; i++) { + int offset = 0xC + (i * 4); + int speciesAndFormeData = readWord(encounterTable, offset); + int species = speciesAndFormeData & 0x7FF; + int forme = speciesAndFormeData >> 11; + if (species != 0) { + Encounter e = new Encounter(); + e.pokemon = getPokemonForEncounter(species, forme); + e.formeNumber = forme; + e.level = minLevel; + e.maxLevel = maxLevel; + es.encounters.add(e); + + // Get all the SOS encounters for this non-SOS encounter + for (int j = 1; j < 8; j++) { + species = readWord(encounterTable, offset + (40 * j)) & 0x7FF; + forme = readWord(encounterTable, offset + (40 * j)) >> 11; + Encounter sos = new Encounter(); + sos.pokemon = getPokemonForEncounter(species, forme); + sos.formeNumber = forme; + sos.level = minLevel; + sos.maxLevel = maxLevel; + sos.isSOS = true; + sos.sosType = SOSType.GENERIC; + es.encounters.add(sos); + } + } + } + + // Get the weather SOS encounters for this area + for (int i = 0; i < 6; i++) { + int offset = 0x14C + (i * 4); + int species = readWord(encounterTable, offset) & 0x7FF; + int forme = readWord(encounterTable, offset) >> 11; + if (species != 0) { + Encounter weatherSOS = new Encounter(); + weatherSOS.pokemon = getPokemonForEncounter(species, forme); + weatherSOS.formeNumber = forme; + weatherSOS.level = minLevel; + weatherSOS.maxLevel = maxLevel; + weatherSOS.isSOS = true; + weatherSOS.sosType = getSOSTypeForIndex(i); + es.encounters.add(weatherSOS); + } + } + return es; + } + + private SOSType getSOSTypeForIndex(int index) { + if (index / 2 == 0) { + return SOSType.RAIN; + } else if (index / 2 == 1) { + return SOSType.HAIL; + } else { + return SOSType.SAND; + } + } + + private Pokemon getPokemonForEncounter(int species, int forme) { + Pokemon pokemon = 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). + if (forme <= pokemon.cosmeticForms || forme == 30 || forme == 31) { + return pokemon; + } else { + int speciesWithForme = absolutePokeNumByBaseForme + .getOrDefault(species, dummyAbsolutePokeNums) + .getOrDefault(forme, 0); + return pokes[speciesWithForme]; + } + } + + @Override + public void setEncounters(boolean useTimeOfDay, List<EncounterSet> encountersList) { + Iterator<EncounterSet> encounters = encountersList.iterator(); + for (AreaData areaData : areaDataList) { + if (!areaData.hasTables) { + continue; + } + + for (int i = 0; i < areaData.encounterTables.size(); i++) { + byte[] encounterTable = areaData.encounterTables.get(i); + if (useTimeOfDay) { + EncounterSet dayEncounters = encounters.next(); + EncounterSet nightEncounters = encounters.next(); + writeEncounterTable(encounterTable, 0, dayEncounters.encounters); + writeEncounterTable(encounterTable, 0x164, nightEncounters.encounters); + } else { + EncounterSet dayEncounters = encounters.next(); + writeEncounterTable(encounterTable, 0, dayEncounters.encounters); + writeEncounterTable(encounterTable, 0x164, dayEncounters.encounters); + } + } + } + + try { + saveAreaData(); + patchMiniorEncounterCode(); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + private void writeEncounterTable(byte[] encounterTable, int offset, List<Encounter> encounters) { + Iterator<Encounter> encounter = encounters.iterator(); + Encounter firstEncounter = encounters.get(0); + encounterTable[offset] = (byte) firstEncounter.level; + encounterTable[offset + 1] = (byte) firstEncounter.maxLevel; + int numberOfEncounterSlots = encounters.size() / 8; + for (int i = 0; i < numberOfEncounterSlots; i++) { + int currentOffset = offset + 0xC + (i * 4); + Encounter enc = encounter.next(); + int speciesAndFormeData = (enc.formeNumber << 11) + enc.pokemon.getBaseNumber(); + writeWord(encounterTable, currentOffset, speciesAndFormeData); + + // SOS encounters for this encounter + for (int j = 1; j < 8; j++) { + Encounter sosEncounter = encounter.next(); + speciesAndFormeData = (sosEncounter.formeNumber << 11) + sosEncounter.pokemon.getBaseNumber(); + writeWord(encounterTable, currentOffset + (40 * j), speciesAndFormeData); + } + } + + // Weather SOS encounters + if (encounters.size() != numberOfEncounterSlots * 8) { + for (int i = 0; i < 6; i++) { + int currentOffset = offset + 0x14C + (i * 4); + Encounter weatherSOSEncounter = encounter.next(); + int speciesAndFormeData = (weatherSOSEncounter.formeNumber << 11) + weatherSOSEncounter.pokemon.getBaseNumber(); + writeWord(encounterTable, currentOffset, speciesAndFormeData); + } + } + } + + private List<AreaData> getAreaData() throws IOException { + GARCArchive worldDataGarc = readGARC(romEntry.getFile("WorldData"), false); + List<byte[]> worlds = new ArrayList<>(); + for (Map<Integer, byte[]> file : worldDataGarc.files) { + byte[] world = Mini.UnpackMini(file.get(0), "WD")[0]; + worlds.add(world); + } + GARCArchive zoneDataGarc = readGARC(romEntry.getFile("ZoneData"), false); + byte[] zoneDataBytes = zoneDataGarc.getFile(0); + byte[] worldData = zoneDataGarc.getFile(1); + List<String> locationList = createGoodLocationList(); + ZoneData[] zoneData = getZoneData(zoneDataBytes, worldData, locationList, worlds); + encounterGarc = readGARC(romEntry.getFile("WildPokemon"), Gen7Constants.getRelevantEncounterFiles(romEntry.romType)); + int fileCount = encounterGarc.files.size(); + int numberOfAreas = fileCount / 11; + AreaData[] areaData = new AreaData[numberOfAreas]; + for (int i = 0; i < numberOfAreas; i++) { + int areaOffset = i; + areaData[i] = new AreaData(); + areaData[i].fileNumber = 9 + (11 * i); + areaData[i].zones = Arrays.stream(zoneData).filter((zone -> zone.areaIndex == areaOffset)).collect(Collectors.toList()); + areaData[i].name = getAreaNameFromZones(areaData[i].zones); + byte[] encounterData = encounterGarc.getFile(areaData[i].fileNumber); + if (encounterData.length == 0) { + areaData[i].hasTables = false; + } else { + byte[][] encounterTables = Mini.UnpackMini(encounterData, "EA"); + areaData[i].hasTables = Arrays.stream(encounterTables).anyMatch(t -> t.length > 0); + if (!areaData[i].hasTables) { + continue; + } + + for (byte[] encounterTable : encounterTables) { + byte[] trimmedEncounterTable = new byte[0x2C8]; + System.arraycopy(encounterTable, 4, trimmedEncounterTable, 0, 0x2C8); + areaData[i].encounterTables.add(trimmedEncounterTable); + } + } + } + + return Arrays.asList(areaData); + } + + private void saveAreaData() throws IOException { + for (AreaData areaData : areaDataList) { + if (areaData.hasTables) { + byte[] encounterData = encounterGarc.getFile(areaData.fileNumber); + byte[][] encounterTables = Mini.UnpackMini(encounterData, "EA"); + for (int i = 0; i < encounterTables.length; i++) { + byte[] originalEncounterTable = encounterTables[i]; + byte[] newEncounterTable = areaData.encounterTables.get(i); + System.arraycopy(newEncounterTable, 0, originalEncounterTable, 4, newEncounterTable.length); + } + byte[] newEncounterData = Mini.PackMini(encounterTables, "EA"); + encounterGarc.setFile(areaData.fileNumber, newEncounterData); + } + } + } + + private List<String> createGoodLocationList() { + List<String> locationList = getStrings(false, romEntry.getInt("MapNamesTextOffset")); + List<String> goodLocationList = new ArrayList<>(locationList); + for (int i = 0; i < locationList.size(); i += 2) { + // The location list contains both areas and subareas. If a subarea is associated with an area, it will + // appear directly after it. This code combines these subarea and area names. + String subarea = locationList.get(i + 1); + if (!subarea.isEmpty() && subarea.charAt(0) != '[') { + String updatedLocation = goodLocationList.get(i) + " (" + subarea + ")"; + goodLocationList.set(i, updatedLocation); + } + + // Some areas appear in the location list multiple times and don't have any subarea name to distinguish + // them. This code distinguishes them by appending the number of times they've appeared previously to + // the area name. + if (i > 0) { + List<String> goodLocationUpToCurrent = goodLocationList.stream().limit(i - 1).collect(Collectors.toList()); + if (!goodLocationList.get(i).isEmpty() && goodLocationUpToCurrent.contains(goodLocationList.get(i))) { + int numberOfUsages = Collections.frequency(goodLocationUpToCurrent, goodLocationList.get(i)); + String updatedLocation = goodLocationList.get(i) + " (" + (numberOfUsages + 1) + ")"; + goodLocationList.set(i, updatedLocation); + } + } + } + return goodLocationList; + } + + private ZoneData[] getZoneData(byte[] zoneDataBytes, byte[] worldData, List<String> locationList, List<byte[]> worlds) { + ZoneData[] zoneData = new ZoneData[zoneDataBytes.length / ZoneData.size]; + for (int i = 0; i < zoneData.length; i++) { + zoneData[i] = new ZoneData(zoneDataBytes, i); + zoneData[i].worldIndex = FileFunctions.read2ByteInt(worldData, i * 0x2); + zoneData[i].locationName = locationList.get(zoneData[i].parentMap); + + byte[] world = worlds.get(zoneData[i].worldIndex); + int mappingOffset = FileFunctions.readFullInt(world, 0x8); + for (int offset = mappingOffset; offset < world.length; offset += 4) { + int potentialZoneIndex = FileFunctions.read2ByteInt(world, offset); + if (potentialZoneIndex == i) { + zoneData[i].areaIndex = FileFunctions.read2ByteInt(world, offset + 0x2); + break; + } + } + } + return zoneData; + } + + private String getAreaNameFromZones(List<ZoneData> zoneData) { + Set<String> uniqueZoneNames = new HashSet<>(); + for (ZoneData zone : zoneData) { + uniqueZoneNames.add(zone.locationName); + } + return String.join(" / ", uniqueZoneNames); + } + + private void patchMiniorEncounterCode() { + int offset = find(code, Gen7Constants.miniorWildEncounterPatchPrefix); + if (offset > 0) { + offset += Gen7Constants.miniorWildEncounterPatchPrefix.length() / 2; + + // When deciding the *actual* forme for a wild encounter (versus the forme stored + // in the encounter data), the game has a hardcoded check for Minior's species ID. + // If the species is Minior, then it branches to code that randomly selects a forme + // for one of Minior's seven Meteor forms. As a consequence, you can't directly + // spawn Minior's Core forms; the forme number will just be replaced. The below + // code nops out the beq instruction so that Minior-C can be spawned directly. + code[offset] = 0x00; + code[offset + 1] = 0x00; + code[offset + 2] = 0x00; + code[offset + 3] = 0x00; + } + } + + @Override + public List<Trainer> getTrainers() { + List<Trainer> allTrainers = new ArrayList<>(); + try { + GARCArchive trainers = this.readGARC(romEntry.getFile("TrainerData"),true); + GARCArchive trpokes = this.readGARC(romEntry.getFile("TrainerPokemon"),true); + int trainernum = trainers.files.size(); + List<String> tclasses = this.getTrainerClassNames(); + List<String> tnames = this.getTrainerNames(); + Map<Integer,String> tnamesMap = new TreeMap<>(); + for (int i = 0; i < tnames.size(); i++) { + tnamesMap.put(i,tnames.get(i)); + } + for (int i = 1; i < trainernum; i++) { + byte[] trainer = trainers.files.get(i).get(0); + byte[] trpoke = trpokes.files.get(i).get(0); + Trainer tr = new Trainer(); + tr.poketype = trainer[13] & 0xFF; + tr.index = i; + tr.trainerclass = trainer[0] & 0xFF; + int battleType = trainer[2] & 0xFF; + int numPokes = trainer[3] & 0xFF; + int trainerAILevel = trainer[12] & 0xFF; + boolean healer = trainer[15] != 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 + // IV SB LV LV SP SP FRM FRM + // (HI HI) + // (M1 M1 M2 M2 M3 M3 M4 M4) + // where 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 secondbyte = trpoke[pokeOffs + 1] & 0xFF; + int abilityAndFlag = trpoke[pokeOffs]; + int level = readWord(trpoke, pokeOffs + 14); + int species = readWord(trpoke, pokeOffs + 16); + int formnum = readWord(trpoke, pokeOffs + 18); + TrainerPokemon tpk = new TrainerPokemon(); + tpk.abilitySlot = (abilityAndFlag >>> 4) & 0xF; + tpk.forcedGenderFlag = (abilityAndFlag & 0xF); + tpk.nature = trpoke[pokeOffs + 1]; + tpk.hpEVs = trpoke[pokeOffs + 2]; + tpk.atkEVs = trpoke[pokeOffs + 3]; + tpk.defEVs = trpoke[pokeOffs + 4]; + tpk.spatkEVs = trpoke[pokeOffs + 5]; + tpk.spdefEVs = trpoke[pokeOffs + 6]; + tpk.speedEVs = trpoke[pokeOffs + 7]; + tpk.IVs = FileFunctions.readFullInt(trpoke, pokeOffs + 8); + tpk.level = level; + if (romEntry.romType == Gen7Constants.Type_USUM) { + if (i == 78) { + if (poke == 3 && tpk.level == 16 && tr.pokemon.get(0).level == 16) { + tpk.level = 14; + } + } + } + tpk.pokemon = pokes[species]; + tpk.forme = formnum; + tpk.formeSuffix = Gen7Constants.getFormeSuffixByBaseForme(species,formnum); + pokeOffs += 20; + tpk.heldItem = readWord(trpoke, pokeOffs); + tpk.hasMegaStone = Gen6Constants.isMegaStone(tpk.heldItem); + tpk.hasZCrystal = Gen7Constants.isZCrystal(tpk.heldItem); + pokeOffs += 4; + for (int move = 0; move < 4; move++) { + tpk.moves[move] = readWord(trpoke, pokeOffs + (move*2)); + } + pokeOffs += 8; + tr.pokemon.add(tpk); + } + allTrainers.add(tr); + } + if (romEntry.romType == Gen7Constants.Type_SM) { + Gen7Constants.tagTrainersSM(allTrainers); + Gen7Constants.setMultiBattleStatusSM(allTrainers); + } else { + Gen7Constants.tagTrainersUSUM(allTrainers); + Gen7Constants.setMultiBattleStatusUSUM(allTrainers); + Gen7Constants.setForcedRivalStarterPositionsUSUM(allTrainers); + } + } catch (IOException ex) { + throw new RandomizerIOException(ex); + } + return allTrainers; + } + + @Override + public List<Integer> getMainPlaythroughTrainers() { + return new ArrayList<>(); + } + + @Override + public List<Integer> getEliteFourTrainers(boolean isChallengeMode) { + return Arrays.stream(romEntry.arrayEntries.get("EliteFourIndices")).boxed().collect(Collectors.toList()); + } + + @Override + public void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode) { + Iterator<Trainer> allTrainers = trainerData.iterator(); + 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<Integer, List<MoveLearnt>> 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(); + int offset = 0; + trainer[13] = (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] |= 0x8; // Flag that needs to be set for trainers not to attack their own pokes + } + } + } + + int bytesNeeded = 32 * numPokes; + byte[] trpoke = new byte[bytesNeeded]; + int pokeOffs = 0; + Iterator<TrainerPokemon> 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] = abilityAndFlag; + trpoke[pokeOffs + 1] = tp.nature; + trpoke[pokeOffs + 2] = tp.hpEVs; + trpoke[pokeOffs + 3] = tp.atkEVs; + trpoke[pokeOffs + 4] = tp.defEVs; + trpoke[pokeOffs + 5] = tp.spatkEVs; + trpoke[pokeOffs + 6] = tp.spdefEVs; + trpoke[pokeOffs + 7] = tp.speedEVs; + FileFunctions.writeFullInt(trpoke, pokeOffs + 8, tp.IVs); + writeWord(trpoke, pokeOffs + 14, tp.level); + writeWord(trpoke, pokeOffs + 16, tp.pokemon.number); + writeWord(trpoke, pokeOffs + 18, tp.forme); + pokeOffs += 20; + writeWord(trpoke, pokeOffs, tp.heldItem); + pokeOffs += 4; + if (tp.resetMoves) { + int[] pokeMoves = RomFunctions.getMovesAtLevel(getAltFormeOfPokemon(tp.pokemon, tp.forme).number, movesets, tp.level); + for (int m = 0; m < 4; m++) { + writeWord(trpoke, pokeOffs + m * 2, pokeMoves[m]); + } + if (Gen7Constants.heldZCrystals.contains(tp.heldItem)) { // Choose a new Z-Crystal at random based on the types of the Pokemon's moves + int chosenMove = this.random.nextInt(Arrays.stream(pokeMoves).filter(mv -> mv != 0).toArray().length); + int newZCrystal = Gen7Constants.heldZCrystals.get((int)Gen7Constants.typeToByte(moves[pokeMoves[chosenMove]].type)); + writeWord(trpoke, pokeOffs - 4, newZCrystal); + } + } else { + writeWord(trpoke, pokeOffs, tp.moves[0]); + writeWord(trpoke, pokeOffs + 2, tp.moves[1]); + writeWord(trpoke, pokeOffs + 4, tp.moves[2]); + writeWord(trpoke, pokeOffs + 6, tp.moves[3]); + if (Gen7Constants.heldZCrystals.contains(tp.heldItem)) { // Choose a new Z-Crystal at random based on the types of the Pokemon's moves + int chosenMove = this.random.nextInt(Arrays.stream(tp.moves).filter(mv -> mv != 0).toArray().length); + int newZCrystal = Gen7Constants.heldZCrystals.get((int)Gen7Constants.typeToByte(moves[tp.moves[chosenMove]].type)); + writeWord(trpoke, pokeOffs - 4, newZCrystal); + } + } + pokeOffs += 8; + } + trpokes.setFile(i,trpoke); + } + this.writeGARC(romEntry.getFile("TrainerData"), trainers); + this.writeGARC(romEntry.getFile("TrainerPokemon"), trpokes); + + // In Sun/Moon, Beast Lusamine's Pokemon have aura boosts that are hardcoded. + if (romEntry.romType == Gen7Constants.Type_SM) { + Trainer beastLusamine = trainerData.get(Gen7Constants.beastLusamineTrainerIndex); + setBeastLusaminePokemonBuffs(beastLusamine); + } + } catch (IOException ex) { + throw new RandomizerIOException(ex); + } + } + + private void setBeastLusaminePokemonBuffs(Trainer beastLusamine) throws IOException { + byte[] battleCRO = readFile(romEntry.getFile("Battle")); + int offset = find(battleCRO, Gen7Constants.beastLusaminePokemonBoostsPrefix); + if (offset > 0) { + offset += Gen7Constants.beastLusaminePokemonBoostsPrefix.length() / 2; // because it was a prefix + + // The game only has room for five boost entries, where each boost entry is determined by species ID. + // However, Beast Lusamine might have duplicates in her party, meaning that two Pokemon can share the + // same boost entry. First, figure out all the unique Pokemon in her party. We avoid using a Set here + // in order to preserve the original ordering; we want to make sure to boost the *first* five Pokemon + List<Pokemon> uniquePokemon = new ArrayList<>(); + for (int i = 0; i < beastLusamine.pokemon.size(); i++) { + if (!uniquePokemon.contains(beastLusamine.pokemon.get(i).pokemon)) { + uniquePokemon.add(beastLusamine.pokemon.get(i).pokemon); + } + } + int numberOfBoostEntries = Math.min(uniquePokemon.size(), 5); + for (int i = 0; i < numberOfBoostEntries; i++) { + Pokemon boostedPokemon = uniquePokemon.get(i); + int auraNumber = getAuraNumberForHighestStat(boostedPokemon); + int speciesNumber = boostedPokemon.getBaseNumber(); + FileFunctions.write2ByteInt(battleCRO, offset + (i * 0x10), speciesNumber); + battleCRO[offset + (i * 0x10) + 2] = (byte) auraNumber; + } + writeFile(romEntry.getFile("Battle"), battleCRO); + } + } + + // Finds the highest stat for the purposes of setting the aura boost on Beast Lusamine's Pokemon. + // In the case where two or more stats are tied for the highest stat, it randomly selects one. + private int getAuraNumberForHighestStat(Pokemon boostedPokemon) { + int currentBestStat = boostedPokemon.attack; + int auraNumber = 1; + boolean useDefenseAura = boostedPokemon.defense > currentBestStat || (boostedPokemon.defense == currentBestStat && random.nextBoolean()); + if (useDefenseAura) { + currentBestStat = boostedPokemon.defense; + auraNumber = 2; + } + boolean useSpAtkAura = boostedPokemon.spatk > currentBestStat || (boostedPokemon.spatk == currentBestStat && random.nextBoolean()); + if (useSpAtkAura) { + currentBestStat = boostedPokemon.spatk; + auraNumber = 3; + } + boolean useSpDefAura = boostedPokemon.spdef > currentBestStat || (boostedPokemon.spdef == currentBestStat && random.nextBoolean()); + if (useSpDefAura) { + currentBestStat = boostedPokemon.spdef; + auraNumber = 4; + } + boolean useSpeedAura = boostedPokemon.speed > currentBestStat || (boostedPokemon.speed == currentBestStat && random.nextBoolean()); + if (useSpeedAura) { + auraNumber = 5; + } + return auraNumber; + } + + @Override + public List<Integer> getEvolutionItems() { + return Gen7Constants.evolutionItems; + } + + @Override + public Map<Integer, List<MoveLearnt>> getMovesLearnt() { + Map<Integer, List<MoveLearnt>> movesets = new TreeMap<>(); + try { + GARCArchive movesLearnt = this.readGARC(romEntry.getFile("PokemonMovesets"),true); + int formeCount = Gen7Constants.getFormeCount(romEntry.romType); + for (int i = 1; i <= Gen7Constants.getPokemonCount(romEntry.romType) + formeCount; i++) { + Pokemon pkmn = pokes[i]; + byte[] movedata; + movedata = movesLearnt.files.get(i).get(0); + int moveDataLoc = 0; + List<MoveLearnt> 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<Integer, List<MoveLearnt>> movesets) { + try { + GARCArchive movesLearnt = readGARC(romEntry.getFile("PokemonMovesets"),true); + int formeCount = Gen7Constants.getFormeCount(romEntry.romType); + for (int i = 1; i <= Gen7Constants.getPokemonCount(romEntry.romType) + formeCount; i++) { + Pokemon pkmn = pokes[i]; + List<MoveLearnt> 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); + movesLearnt.setFile(i, moveset); + } + // Save + this.writeGARC(romEntry.getFile("PokemonMovesets"), movesLearnt); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + } + + @Override + public Map<Integer, List<Integer>> getEggMoves() { + Map<Integer, List<Integer>> eggMoves = new TreeMap<>(); + try { + GARCArchive eggMovesGarc = this.readGARC(romEntry.getFile("EggMoves"),true); + TreeMap<Pokemon, Integer> altFormeEggMoveFiles = new TreeMap<>(); + for (int i = 1; i <= Gen7Constants.getPokemonCount(romEntry.romType); i++) { + Pokemon pkmn = pokes[i]; + byte[] movedata = eggMovesGarc.files.get(i).get(0); + int formeReference = readWord(movedata, 0); + if (formeReference != pkmn.number) { + altFormeEggMoveFiles.put(pkmn, formeReference); + } + int numberOfEggMoves = readWord(movedata, 2); + List<Integer> moves = new ArrayList<>(); + for (int j = 0; j < numberOfEggMoves; j++) { + int move = readWord(movedata, 4 + (j * 2)); + moves.add(move); + } + eggMoves.put(pkmn.number, moves); + } + Iterator<Pokemon> iter = altFormeEggMoveFiles.keySet().iterator(); + while (iter.hasNext()) { + Pokemon originalForme = iter.next(); + int formeNumber = 1; + int fileNumber = altFormeEggMoveFiles.get(originalForme); + Pokemon altForme = getAltFormeOfPokemon(originalForme, formeNumber); + while (!originalForme.equals(altForme)) { + byte[] movedata = eggMovesGarc.files.get(fileNumber).get(0); + int numberOfEggMoves = readWord(movedata, 2); + List<Integer> moves = new ArrayList<>(); + for (int j = 0; j < numberOfEggMoves; j++) { + int move = readWord(movedata, 4 + (j * 2)); + moves.add(move); + } + eggMoves.put(altForme.number, moves); + formeNumber++; + fileNumber++; + altForme = getAltFormeOfPokemon(originalForme, formeNumber); + } + iter.remove(); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + return eggMoves; + } + + @Override + public void setEggMoves(Map<Integer, List<Integer>> eggMoves) { + try { + GARCArchive eggMovesGarc = this.readGARC(romEntry.getFile("EggMoves"), true); + TreeMap<Pokemon, Integer> altFormeEggMoveFiles = new TreeMap<>(); + for (int i = 1; i <= Gen7Constants.getPokemonCount(romEntry.romType); i++) { + Pokemon pkmn = pokes[i]; + byte[] movedata = eggMovesGarc.files.get(i).get(0); + int formeReference = readWord(movedata, 0); + if (formeReference != pkmn.number) { + altFormeEggMoveFiles.put(pkmn, formeReference); + } + List<Integer> moves = eggMoves.get(pkmn.number); + for (int j = 0; j < moves.size(); j++) { + writeWord(movedata, 4 + (j * 2), moves.get(j)); + } + } + Iterator<Pokemon> iter = altFormeEggMoveFiles.keySet().iterator(); + while (iter.hasNext()) { + Pokemon originalForme = iter.next(); + int formeNumber = 1; + int fileNumber = altFormeEggMoveFiles.get(originalForme); + Pokemon altForme = getAltFormeOfPokemon(originalForme, formeNumber); + while (!originalForme.equals(altForme)) { + byte[] movedata = eggMovesGarc.files.get(fileNumber).get(0); + List<Integer> moves = eggMoves.get(altForme.number); + for (int j = 0; j < moves.size(); j++) { + writeWord(movedata, 4 + (j * 2), moves.get(j)); + } + formeNumber++; + fileNumber++; + altForme = getAltFormeOfPokemon(originalForme, formeNumber); + } + iter.remove(); + } + // Save + this.writeGARC(romEntry.getFile("EggMoves"), eggMovesGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public boolean canChangeStaticPokemon() { + return true; + } + + @Override + public boolean hasStaticAltFormes() { + return true; + } + + @Override + public boolean hasMainGameLegendaries() { + return true; + } + + @Override + public List<Integer> getMainGameLegendaries() { + return Arrays.stream(romEntry.arrayEntries.get("MainGameLegendaries")).boxed().collect(Collectors.toList()); + } + + @Override + public List<Integer> getSpecialMusicStatics() { + return new ArrayList<>(); + } + + @Override + public void applyCorrectStaticMusic(Map<Integer, Integer> specialMusicStaticChanges) { + + } + + @Override + public boolean hasStaticMusicFix() { + return false; + } + + @Override + public List<TotemPokemon> getTotemPokemon() { + List<TotemPokemon> totems = new ArrayList<>(); + try { + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + List<Integer> totemIndices = + Arrays.stream(romEntry.arrayEntries.get("TotemPokemonIndices")).boxed().collect(Collectors.toList()); + + // Static encounters + byte[] staticEncountersFile = staticGarc.files.get(1).get(0); + for (int i: totemIndices) { + int offset = i * 0x38; + TotemPokemon totem = new TotemPokemon(); + int species = FileFunctions.read2ByteInt(staticEncountersFile, offset); + Pokemon pokemon = pokes[species]; + int forme = staticEncountersFile[offset + 2]; + if (forme > pokemon.cosmeticForms && forme != 30 && forme != 31) { + int speciesWithForme = absolutePokeNumByBaseForme + .getOrDefault(species, dummyAbsolutePokeNums) + .getOrDefault(forme, 0); + pokemon = pokes[speciesWithForme]; + } + totem.pkmn = pokemon; + totem.forme = forme; + totem.level = staticEncountersFile[offset + 3]; + int heldItem = FileFunctions.read2ByteInt(staticEncountersFile, offset + 4); + if (heldItem == 0xFFFF) { + heldItem = 0; + } + totem.heldItem = heldItem; + totem.aura = new Aura(staticEncountersFile[offset + 0x25]); + int allies = staticEncountersFile[offset + 0x27]; + for (int j = 0; j < allies; j++) { + int allyIndex = (staticEncountersFile[offset + 0x28 + 4*j] - 1) & 0xFF; + totem.allies.put(allyIndex,readStaticEncounter(staticEncountersFile, allyIndex * 0x38)); + } + totems.add(totem); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + return totems; + } + + @Override + public void setTotemPokemon(List<TotemPokemon> totemPokemon) { + try { + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + List<Integer> totemIndices = + Arrays.stream(romEntry.arrayEntries.get("TotemPokemonIndices")).boxed().collect(Collectors.toList()); + Iterator<TotemPokemon> totemIter = totemPokemon.iterator(); + + // Static encounters + byte[] staticEncountersFile = staticGarc.files.get(1).get(0); + for (int i: totemIndices) { + int offset = i * 0x38; + TotemPokemon totem = totemIter.next(); + if (totem.pkmn.formeNumber > 0) { + totem.forme = totem.pkmn.formeNumber; + totem.pkmn = totem.pkmn.baseForme; + } + writeWord(staticEncountersFile, offset, totem.pkmn.number); + staticEncountersFile[offset + 2] = (byte) totem.forme; + staticEncountersFile[offset + 3] = (byte) totem.level; + if (totem.heldItem == 0) { + writeWord(staticEncountersFile, offset + 4, -1); + } else { + writeWord(staticEncountersFile, offset + 4, totem.heldItem); + } + if (totem.resetMoves) { + writeWord(staticEncountersFile, offset + 12, 0); + writeWord(staticEncountersFile, offset + 14, 0); + writeWord(staticEncountersFile, offset + 16, 0); + writeWord(staticEncountersFile, offset + 18, 0); + } + staticEncountersFile[offset + 0x25] = totem.aura.toByte(); + for (Integer allyIndex: totem.allies.keySet()) { + offset = allyIndex * 0x38; + StaticEncounter ally = totem.allies.get(allyIndex); + if (ally.pkmn.formeNumber > 0) { + ally.forme = ally.pkmn.formeNumber; + ally.pkmn = ally.pkmn.baseForme; + } + writeWord(staticEncountersFile, offset, ally.pkmn.number); + staticEncountersFile[offset + 2] = (byte) ally.forme; + staticEncountersFile[offset + 3] = (byte) ally.level; + if (ally.heldItem == 0) { + writeWord(staticEncountersFile, offset + 4, -1); + } else { + writeWord(staticEncountersFile, offset + 4, ally.heldItem); + } + if (ally.resetMoves) { + writeWord(staticEncountersFile, offset + 12, 0); + writeWord(staticEncountersFile, offset + 14, 0); + writeWord(staticEncountersFile, offset + 16, 0); + writeWord(staticEncountersFile, offset + 18, 0); + } + } + } + + writeGARC(romEntry.getFile("StaticPokemon"), staticGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + } + + @Override + public List<StaticEncounter> getStaticPokemon() { + List<StaticEncounter> statics = new ArrayList<>(); + try { + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + List<Integer> skipIndices = + Arrays.stream(romEntry.arrayEntries.get("TotemPokemonIndices")).boxed().collect(Collectors.toList()); + skipIndices.addAll(Arrays.stream(romEntry.arrayEntries.get("AllyPokemonIndices")).boxed().collect(Collectors.toList())); + + // Gifts, start at 3 to skip the starters + byte[] giftsFile = staticGarc.files.get(0).get(0); + int numberOfGifts = giftsFile.length / 0x14; + for (int i = 3; i < numberOfGifts; i++) { + int offset = i * 0x14; + StaticEncounter se = new StaticEncounter(); + int species = FileFunctions.read2ByteInt(giftsFile, offset); + Pokemon pokemon = pokes[species]; + int forme = giftsFile[offset + 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 = giftsFile[offset + 3]; + se.heldItem = FileFunctions.read2ByteInt(giftsFile, offset + 8); + se.isEgg = giftsFile[offset + 10] == 1; + statics.add(se); + } + + // Static encounters + byte[] staticEncountersFile = staticGarc.files.get(1).get(0); + int numberOfStaticEncounters = staticEncountersFile.length / 0x38; + for (int i = 0; i < numberOfStaticEncounters; i++) { + if (skipIndices.contains(i)) continue; + int offset = i * 0x38; + StaticEncounter se = readStaticEncounter(staticEncountersFile, offset); + statics.add(se); + } + + // Zygarde created via Assembly on Route 16 is hardcoded + readAssemblyZygarde(statics); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + consolidateLinkedEncounters(statics); + return statics; + } + + private StaticEncounter readStaticEncounter(byte[] staticEncountersFile, int offset) { + StaticEncounter se = new StaticEncounter(); + int species = FileFunctions.read2ByteInt(staticEncountersFile, offset); + Pokemon pokemon = pokes[species]; + int forme = staticEncountersFile[offset + 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 = staticEncountersFile[offset + 3]; + int heldItem = FileFunctions.read2ByteInt(staticEncountersFile, offset + 4); + if (heldItem == 0xFFFF) { + heldItem = 0; + } + se.heldItem = heldItem; + return se; + } + + private void consolidateLinkedEncounters(List<StaticEncounter> statics) { + List<StaticEncounter> encountersToRemove = new ArrayList<>(); + for (Map.Entry<Integer, Integer> 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); + } + } + + private void readAssemblyZygarde(List<StaticEncounter> statics) throws IOException { + GARCArchive scriptGarc = readGARC(romEntry.getFile("Scripts"), true); + int[] scriptLevelOffsets = romEntry.arrayEntries.get("ZygardeScriptLevelOffsets"); + int[] levels = new int[scriptLevelOffsets.length]; + byte[] zygardeAssemblyScriptBytes = scriptGarc.getFile(Gen7Constants.zygardeAssemblyScriptFile); + AMX zygardeAssemblyScript = new AMX(zygardeAssemblyScriptBytes); + for (int i = 0; i < scriptLevelOffsets.length; i++) { + levels[i] = zygardeAssemblyScript.decData[scriptLevelOffsets[i]]; + } + + int speciesOffset = find(code, Gen7Constants.zygardeAssemblySpeciesPrefix); + int formeOffset = find(code, Gen7Constants.zygardeAssemblyFormePrefix); + if (speciesOffset > 0 && formeOffset > 0) { + speciesOffset += Gen7Constants.zygardeAssemblySpeciesPrefix.length() / 2; // because it was a prefix + formeOffset += Gen7Constants.zygardeAssemblyFormePrefix.length() / 2; // because it was a prefix + int species = FileFunctions.read2ByteInt(code, speciesOffset); + + // The original code for this passed in the forme via a parameter, stored that onto + // the stack, then did a ldr to put that stack variable into r0 before finally + // storing that value in the right place. If we already modified this code, then we + // don't care about all of this; we just wrote a "mov r0, #forme" over the ldr instead. + // Thus, if the original ldr instruction is still there, assume we haven't touched it. + int forme = 0; + if (FileFunctions.readFullInt(code, formeOffset) == 0xE59D0040) { + // Since we haven't modified the code yet, this is Zygarde. For SM, use 10%, + // since you can get it fairly early. For USUM, use 50%, since it's only + // obtainable in the postgame. + forme = isSM ? 1 : 0; + } else { + // We have modified the code, so just read the constant forme number we wrote. + forme = code[formeOffset]; + } + + StaticEncounter lowLevelAssembly = new StaticEncounter(); + Pokemon pokemon = pokes[species]; + if (forme > pokemon.cosmeticForms && forme != 30 && forme != 31) { + int speciesWithForme = absolutePokeNumByBaseForme + .getOrDefault(species, dummyAbsolutePokeNums) + .getOrDefault(forme, 0); + pokemon = pokes[speciesWithForme]; + } + lowLevelAssembly.pkmn = pokemon; + lowLevelAssembly.forme = forme; + lowLevelAssembly.level = levels[0]; + for (int i = 1; i < levels.length; i++) { + StaticEncounter higherLevelAssembly = new StaticEncounter(); + higherLevelAssembly.pkmn = pokemon; + higherLevelAssembly.forme = forme; + higherLevelAssembly.level = levels[i]; + lowLevelAssembly.linkedEncounters.add(higherLevelAssembly); + } + + statics.add(lowLevelAssembly); + } + } + + @Override + public boolean setStaticPokemon(List<StaticEncounter> staticPokemon) { + try { + unlinkStaticEncounters(staticPokemon); + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + List<Integer> skipIndices = + Arrays.stream(romEntry.arrayEntries.get("TotemPokemonIndices")).boxed().collect(Collectors.toList()); + skipIndices.addAll(Arrays.stream(romEntry.arrayEntries.get("AllyPokemonIndices")).boxed().collect(Collectors.toList())); + Iterator<StaticEncounter> staticIter = staticPokemon.iterator(); + + // Gifts, start at 3 to skip the starters + byte[] giftsFile = staticGarc.files.get(0).get(0); + int numberOfGifts = giftsFile.length / 0x14; + for (int i = 3; i < numberOfGifts; i++) { + int offset = i * 0x14; + StaticEncounter se = staticIter.next(); + writeWord(giftsFile, offset, se.pkmn.number); + giftsFile[offset + 2] = (byte) se.forme; + giftsFile[offset + 3] = (byte) se.level; + writeWord(giftsFile, offset + 8, se.heldItem); + } + + // Static encounters + byte[] staticEncountersFile = staticGarc.files.get(1).get(0); + int numberOfStaticEncounters = staticEncountersFile.length / 0x38; + for (int i = 0; i < numberOfStaticEncounters; i++) { + if (skipIndices.contains(i)) continue; + int offset = i * 0x38; + StaticEncounter se = staticIter.next(); + writeWord(staticEncountersFile, offset, se.pkmn.number); + staticEncountersFile[offset + 2] = (byte) se.forme; + staticEncountersFile[offset + 3] = (byte) se.level; + if (se.heldItem == 0) { + writeWord(staticEncountersFile, offset + 4, -1); + } else { + writeWord(staticEncountersFile, offset + 4, se.heldItem); + } + if (se.resetMoves) { + writeWord(staticEncountersFile, offset + 12, 0); + writeWord(staticEncountersFile, offset + 14, 0); + writeWord(staticEncountersFile, offset + 16, 0); + writeWord(staticEncountersFile, offset + 18, 0); + } + } + + // Zygarde created via Assembly on Route 16 is hardcoded + writeAssemblyZygarde(staticIter.next()); + + writeGARC(romEntry.getFile("StaticPokemon"), staticGarc); + return true; + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + private void unlinkStaticEncounters(List<StaticEncounter> statics) { + List<Integer> offsetsToInsert = new ArrayList<>(); + for (Map.Entry<Integer, Integer> entry : romEntry.linkedStaticOffsets.entrySet()) { + offsetsToInsert.add(entry.getValue()); + } + Collections.sort(offsetsToInsert); + for (Integer offsetToInsert : offsetsToInsert) { + statics.add(offsetToInsert, new StaticEncounter()); + } + for (Map.Entry<Integer, Integer> entry : romEntry.linkedStaticOffsets.entrySet()) { + StaticEncounter baseEncounter = statics.get(entry.getKey()); + statics.set(entry.getValue(), baseEncounter.linkedEncounters.get(0)); + } + } + + private void writeAssemblyZygarde(StaticEncounter se) throws IOException { + int[] levels = new int[se.linkedEncounters.size() + 1]; + levels[0] = se.level; + for (int i = 0; i < se.linkedEncounters.size(); i++) { + levels[i + 1] = se.linkedEncounters.get(i).level; + } + + GARCArchive scriptGarc = readGARC(romEntry.getFile("Scripts"), true); + int[] scriptLevelOffsets = romEntry.arrayEntries.get("ZygardeScriptLevelOffsets"); + byte[] zygardeAssemblyScriptBytes = scriptGarc.getFile(Gen7Constants.zygardeAssemblyScriptFile); + AMX zygardeAssemblyScript = new AMX(zygardeAssemblyScriptBytes); + for (int i = 0; i < scriptLevelOffsets.length; i++) { + zygardeAssemblyScript.decData[scriptLevelOffsets[i]] = (byte) levels[i]; + } + scriptGarc.setFile(Gen7Constants.zygardeAssemblyScriptFile, zygardeAssemblyScript.getBytes()); + writeGARC(romEntry.getFile("Scripts"), scriptGarc); + + int speciesOffset = find(code, Gen7Constants.zygardeAssemblySpeciesPrefix); + int formeOffset = find(code, Gen7Constants.zygardeAssemblyFormePrefix); + if (speciesOffset > 0 && formeOffset > 0) { + speciesOffset += Gen7Constants.zygardeAssemblySpeciesPrefix.length() / 2; // because it was a prefix + formeOffset += Gen7Constants.zygardeAssemblyFormePrefix.length() / 2; // because it was a prefix + FileFunctions.write2ByteInt(code, speciesOffset, se.pkmn.getBaseNumber()); + + // Just write "mov r0, #forme" to where the game originally loaded the forme. + code[formeOffset] = (byte) se.forme; + code[formeOffset + 1] = 0x00; + code[formeOffset + 2] = (byte) 0xA0; + code[formeOffset + 3] = (byte) 0xE3; + } + } + + @Override + public int miscTweaksAvailable() { + int available = 0; + available |= MiscTweak.FASTEST_TEXT.getValue(); + available |= MiscTweak.BAN_LUCKY_EGG.getValue(); + available |= MiscTweak.SOS_BATTLES_FOR_ALL.getValue(); + available |= MiscTweak.RETAIN_ALT_FORMES.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.SOS_BATTLES_FOR_ALL) { + positiveCallRates(); + } else if (tweak == MiscTweak.RETAIN_ALT_FORMES) { + try { + patchFormeReversion(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public boolean isEffectivenessUpdated() { + return false; + } + + private void applyFastestText() { + int offset = find(code, Gen7Constants.fastestTextPrefixes[0]); + if (offset > 0) { + offset += Gen7Constants.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, Gen7Constants.fastestTextPrefixes[1]); + if (offset > 0) { + offset += Gen7Constants.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 positiveCallRates() { + for (Pokemon pk: pokes) { + if (pk == null) continue; + if (pk.callRate <= 0) { + pk.callRate = 5; + } + } + } + + public void enableGuaranteedPokemonCatching() { + try { + byte[] battleCRO = readFile(romEntry.getFile("Battle")); + int offset = find(battleCRO, Gen7Constants.perfectOddsBranchLocator); + if (offset > 0) { + // The game checks to see if your odds are greater then or equal to 255 using the following + // code. Note that they compare to 0xFF000 instead of 0xFF; it looks like all catching code + // probabilities are shifted like this? + // cmp r7, #0xFF000 + // blt oddsLessThanOrEqualTo254 + // The below code just nops the branch out so it always acts like our odds are 255, and + // Pokemon are automatically caught no matter what. + battleCRO[offset] = 0x00; + battleCRO[offset + 1] = 0x00; + battleCRO[offset + 2] = 0x00; + battleCRO[offset + 3] = 0x00; + writeFile(romEntry.getFile("Battle"), battleCRO); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public List<Integer> getTMMoves() { + String tmDataPrefix = Gen7Constants.getTmDataPrefix(romEntry.romType); + int offset = find(code, tmDataPrefix); + if (offset != 0) { + offset += tmDataPrefix.length() / 2; // because it was a prefix + List<Integer> tms = new ArrayList<>(); + for (int i = 0; i < Gen7Constants.tmCount; i++) { + tms.add(readWord(code, offset + i * 2)); + } + return tms; + } else { + return null; + } + } + + @Override + public List<Integer> getHMMoves() { + // Gen 7 does not have any HMs + return new ArrayList<>(); + } + + @Override + public void setTMMoves(List<Integer> moveIndexes) { + String tmDataPrefix = Gen7Constants.getTmDataPrefix(romEntry.romType); + int offset = find(code, tmDataPrefix); + if (offset > 0) { + offset += tmDataPrefix.length() / 2; // because it was a prefix + for (int i = 0; i < Gen7Constants.tmCount; i++) { + writeWord(code, offset + i * 2, moveIndexes.get(i)); + } + + // Update TM item descriptions + List<String> itemDescriptions = getStrings(false, romEntry.getInt("ItemDescriptionsTextOffset")); + List<String> moveDescriptions = getStrings(false, romEntry.getInt("MoveDescriptionsTextOffset")); + // TM01 is item 328 and so on + for (int i = 0; i < Gen7Constants.tmBlockOneCount; i++) { + itemDescriptions.set(i + Gen7Constants.tmBlockOneOffset, moveDescriptions.get(moveIndexes.get(i))); + } + // TM93-95 are 618-620 + for (int i = 0; i < Gen7Constants.tmBlockTwoCount; i++) { + itemDescriptions.set(i + Gen7Constants.tmBlockTwoOffset, + moveDescriptions.get(moveIndexes.get(i + Gen7Constants.tmBlockOneCount))); + } + // TM96-100 are 690 and so on + for (int i = 0; i < Gen7Constants.tmBlockThreeCount; i++) { + itemDescriptions.set(i + Gen7Constants.tmBlockThreeOffset, + moveDescriptions.get(moveIndexes.get(i + Gen7Constants.tmBlockOneCount + Gen7Constants.tmBlockTwoCount))); + } + // Save the new item descriptions + setStrings(false, romEntry.getInt("ItemDescriptionsTextOffset"), itemDescriptions); + // Palettes + String palettePrefix = Gen7Constants.itemPalettesPrefix; + int offsPals = find(code, palettePrefix); + if (offsPals > 0) { + offsPals += Gen7Constants.itemPalettesPrefix.length() / 2; // because it was a prefix + // Write pals + for (int i = 0; i < Gen7Constants.tmBlockOneCount; i++) { + int itmNum = Gen7Constants.tmBlockOneOffset + i; + Move m = this.moves[moveIndexes.get(i)]; + int pal = this.typeTMPaletteNumber(m.type, true); + writeWord(code, offsPals + itmNum * 4, pal); + } + for (int i = 0; i < (Gen7Constants.tmBlockTwoCount); i++) { + int itmNum = Gen7Constants.tmBlockTwoOffset + i; + Move m = this.moves[moveIndexes.get(i + Gen7Constants.tmBlockOneCount)]; + int pal = this.typeTMPaletteNumber(m.type, true); + writeWord(code, offsPals + itmNum * 4, pal); + } + for (int i = 0; i < (Gen7Constants.tmBlockThreeCount); i++) { + int itmNum = Gen7Constants.tmBlockThreeOffset + i; + Move m = this.moves[moveIndexes.get(i + Gen7Constants.tmBlockOneCount + Gen7Constants.tmBlockTwoCount)]; + int pal = this.typeTMPaletteNumber(m.type, true); + 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<Integer> 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 Gen7Constants.tmCount; + } + + @Override + public int getHMCount() { + // Gen 7 does not have any HMs + return 0; + } + + @Override + public Map<Pokemon, boolean[]> getTMHMCompatibility() { + Map<Pokemon, boolean[]> compat = new TreeMap<>(); + int pokemonCount = Gen7Constants.getPokemonCount(romEntry.romType); + int formeCount = Gen7Constants.getFormeCount(romEntry.romType); + for (int i = 1; i <= pokemonCount + formeCount; i++) { + byte[] data; + data = pokeGarc.files.get(i).get(0); + Pokemon pkmn = pokes[i]; + boolean[] flags = new boolean[Gen7Constants.tmCount + 1]; + for (int j = 0; j < 13; j++) { + readByteIntoFlags(data, flags, j * 8 + 1, Gen7Constants.bsTMHMCompatOffset + j); + } + compat.put(pkmn, flags); + } + return compat; + } + + @Override + public void setTMHMCompatibility(Map<Pokemon, boolean[]> compatData) { + for (Map.Entry<Pokemon, boolean[]> 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 < 13; j++) { + data[Gen7Constants.bsTMHMCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1); + } + } + } + + @Override + public boolean hasMoveTutors() { + return romEntry.romType == Gen7Constants.Type_USUM; + } + + @Override + public List<Integer> getMoveTutorMoves() { + List<Integer> mtMoves = new ArrayList<>(); + + int mtOffset = find(code, Gen7Constants.tutorsPrefix); + if (mtOffset > 0) { + mtOffset += Gen7Constants.tutorsPrefix.length() / 2; + int val = 0; + while (val != 0xFFFF) { + val = FileFunctions.read2ByteInt(code, mtOffset); + mtOffset += 2; + if (val == 0xFFFF) continue; + mtMoves.add(val); + } + } + + return mtMoves; + } + + @Override + public void setMoveTutorMoves(List<Integer> moves) { + int mtOffset = find(code, Gen7Constants.tutorsPrefix); + if (mtOffset > 0) { + mtOffset += Gen7Constants.tutorsPrefix.length() / 2; + for (int move: moves) { + FileFunctions.write2ByteInt(code,mtOffset, move); + mtOffset += 2; + } + } + + try { + byte[] tutorCRO = readFile(romEntry.getFile("ShopsAndTutors")); + for (int i = 0; i < moves.size(); i++) { + int offset = Gen7Constants.tutorsOffset + i * 4; + FileFunctions.write2ByteInt(tutorCRO, offset, moves.get(i)); + } + writeFile(romEntry.getFile("ShopsAndTutors"), tutorCRO); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public Map<Pokemon, boolean[]> getMoveTutorCompatibility() { + Map<Pokemon, boolean[]> compat = new TreeMap<>(); + int pokemonCount = Gen7Constants.getPokemonCount(romEntry.romType); + int formeCount = Gen7Constants.getFormeCount(romEntry.romType); + for (int i = 1; i <= pokemonCount + formeCount; i++) { + byte[] data; + data = pokeGarc.files.get(i).get(0); + Pokemon pkmn = pokes[i]; + boolean[] flags = new boolean[Gen7Constants.tutorMoveCount + 1]; + for (int j = 0; j < 10; j++) { + readByteIntoFlags(data, flags, j * 8 + 1, Gen7Constants.bsMTCompatOffset + j); + } + compat.put(pkmn, flags); + } + return compat; + } + + @Override + public void setMoveTutorCompatibility(Map<Pokemon, boolean[]> compatData) { + if (!hasMoveTutors()) return; + int pokemonCount = Gen7Constants.getPokemonCount(romEntry.romType); + int formeCount = Gen7Constants.getFormeCount(romEntry.romType); + for (int i = 1; i <= pokemonCount + formeCount; i++) { + byte[] data; + data = pokeGarc.files.get(i).get(0); + Pokemon pkmn = pokes[i]; + boolean[] flags = compatData.get(pkmn); + for (int j = 0; j < 10; j++) { + data[Gen7Constants.bsMTCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1); + } + } + } + + @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 true; + } + + @Override + public List<Integer> getMovesBannedFromLevelup() { + return Gen7Constants.bannedMoves; + } + + @Override + public boolean hasWildAltFormes() { + return true; + } + + @Override + public void removeImpossibleEvolutions(Settings settings) { + boolean changeMoveEvos = !(settings.getMovesetsMod() == Settings.MovesetsMod.UNCHANGED); + + Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt(); + Set<Evolution> 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); + extraEntry.forme = evo.forme; + 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); + + // Reduce the amount of happiness required to evolve. + int offset = find(code, Gen7Constants.friendshipValueForEvoLocator); + if (offset > 0) { + // Amount of required happiness for HAPPINESS evolutions. + if (code[offset] == (byte)220) { + code[offset] = (byte)160; + } + // Amount of required happiness for HAPPINESS_DAY evolutions. + if (code[offset + 12] == (byte)220) { + code[offset + 12] = (byte)160; + } + // Amount of required happiness for HAPPINESS_NIGHT evolutions. + if (code[offset + 36] == (byte)220) { + code[offset + 36] = (byte)160; + } + } + + for (Pokemon pkmn : pokes) { + if (pkmn != null) { + Evolution extraEntry = null; + for (Evolution evo : pkmn.evolutionsFrom) { + if (wildsRandomized) { + if (evo.type == EvolutionType.LEVEL_WITH_OTHER) { + // Replace w/ level 35 + evo.type = EvolutionType.LEVEL; + evo.extraInfo = 35; + addEvoUpdateCondensed(easierEvolutionUpdates, evo, false); + } + } + if (romEntry.romType == Gen7Constants.Type_SM) { + if (evo.type == EvolutionType.LEVEL_SNOWY) { + extraEntry = new Evolution(evo.from, evo.to, true, + EvolutionType.LEVEL, 35); + extraEntry.forme = evo.forme; + addEvoUpdateCondensed(easierEvolutionUpdates, extraEntry, true); + } else if (evo.type == EvolutionType.LEVEL_ELECTRIFIED_AREA) { + extraEntry = new Evolution(evo.from, evo.to, true, + EvolutionType.LEVEL, 35); + extraEntry.forme = evo.forme; + addEvoUpdateCondensed(easierEvolutionUpdates, extraEntry, true); + } + } + } + if (extraEntry != null) { + pkmn.evolutionsFrom.add(extraEntry); + extraEntry.to.evolutionsTo.add(extraEntry); + } + } + } + + } + + @Override + public void removeTimeBasedEvolutions() { + Set<Evolution> 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); + extraEntry.forme = evo.forme; + 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); + extraEntry.forme = evo.forme; + 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); + extraEntry.forme = evo.forme; + 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); + extraEntry.forme = evo.forme; + extraEvolutions.add(extraEntry); + } + } else if (evo.type == EvolutionType.LEVEL_DAY) { + if (evo.from.number == Species.rockruff) { + // We can't set Rockruff to evolve into Lycanroc-Midday with level at night because that's how + // Lycanroc-Midnight works in the original game. Instead, make Rockruff: == sun stone => Lycanroc-Midday + evo.type = EvolutionType.STONE; + evo.extraInfo = Items.sunStone; + addEvoUpdateStone(timeBasedEvolutionUpdates, evo, itemNames.get(evo.extraInfo)); + } else { + addEvoUpdateLevel(timeBasedEvolutionUpdates, evo); + evo.type = EvolutionType.LEVEL; + } + } else if (evo.type == EvolutionType.LEVEL_NIGHT) { + if (evo.from.number == Species.rockruff) { + // We can't set Rockruff to evolve into Lycanroc-Midnight with level at night because that's how + // Lycanroc-Midday works in the original game. Instead, make Rockruff: == moon stone => Lycanroc-Midnight + evo.type = EvolutionType.STONE; + evo.extraInfo = Items.moonStone; + addEvoUpdateStone(timeBasedEvolutionUpdates, evo, itemNames.get(evo.extraInfo)); + } else { + addEvoUpdateLevel(timeBasedEvolutionUpdates, evo); + evo.type = EvolutionType.LEVEL; + } + } else if (evo.type == EvolutionType.LEVEL_DUSK) { + // This is the Rockruff => Lycanroc-Dusk evolution. We can't set it to evolve with level at other + // times because the other Lycanroc formes work like that in the original game. Instead, make + // Rockruff: == dusk stone => Lycanroc-Dusk + evo.type = EvolutionType.STONE; + evo.extraInfo = Items.duskStone; + addEvoUpdateStone(timeBasedEvolutionUpdates, evo, itemNames.get(evo.extraInfo)); + } + } + pkmn.evolutionsFrom.addAll(extraEvolutions); + for (Evolution ev : extraEvolutions) { + ev.to.evolutionsTo.add(ev); + } + } + } + } + + @Override + public boolean altFormesCanHaveDifferentEvolutions() { + return true; + } + + @Override + public boolean hasShopRandomization() { + return true; + } + + @Override + public boolean canChangeTrainerText() { + return true; + } + + @Override + public List<String> getTrainerNames() { + List<String> tnames = getStrings(false, romEntry.getInt("TrainerNamesTextOffset")); + tnames.remove(0); // blank one + + return tnames; + } + + @Override + public int maxTrainerNameLength() { + return 10; + } + + @Override + public void setTrainerNames(List<String> trainerNames) { + List<String> tnames = getStrings(false, romEntry.getInt("TrainerNamesTextOffset")); + List<String> 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<String> strings, int index) throws IOException { + List<String> nonEnglishLanguages = Arrays.asList("JaKana", "JaKanji", "Fr", "It", "De", "Es", "Ko", "ZhSimplified", "ZhTraditional"); + 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<Integer> getTCNameLengthsByTrainer() { + return new ArrayList<>(); + } + + @Override + public List<String> getTrainerClassNames() { + return getStrings(false, romEntry.getInt("TrainerClassesTextOffset")); + } + + @Override + public void setTrainerClassNames(List<String> 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; + } + + @Override + public boolean fixedTrainerClassNamesLength() { + return false; + } + + @Override + public List<Integer> getDoublesTrainerClasses() { + int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses"); + List<Integer> doubles = new ArrayList<>(); + for (int tClass : doublesClasses) { + doubles.add(tClass); + } + return doubles; + } + + @Override + public String getDefaultExtension() { + return "cxi"; + } + + @Override + public int abilitiesPerPokemon() { + return 3; + } + + @Override + public int highestAbilityIndex() { + return Gen7Constants.getHighestAbilityIndex(romEntry.romType); + } + + @Override + public int internalStringLength(String string) { + return string.length(); + } + + @Override + public void randomizeIntroPokemon() { + // For now, do nothing. + } + + @Override + public ItemList getAllowedItems() { + return allowedItems; + } + + @Override + public ItemList getNonBadItems() { + return nonBadItems; + } + + @Override + public List<Integer> getUniqueNoSellItems() { + return new ArrayList<>(); + } + + @Override + public List<Integer> getRegularShopItems() { + return Gen7Constants.getRegularShopItems(romEntry.romType); + } + + @Override + public List<Integer> getOPShopItems() { + return Gen7Constants.opShopItems; + } + + @Override + public String[] getItemNames() { + return itemNames.toArray(new String[0]); + } + + @Override + public String abilityName(int number) { + return abilityNames.get(number); + } + + @Override + public Map<Integer, List<Integer>> getAbilityVariations() { + return Gen7Constants.abilityVariations; + } + + @Override + public List<Integer> getUselessAbilities() { + return new ArrayList<>(Gen7Constants.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<Integer> 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 >= Gen7Constants.tmBlockOneOffset + && index < Gen7Constants.tmBlockOneOffset + Gen7Constants.tmBlockOneCount) { + return index - (Gen7Constants.tmBlockOneOffset - 1); + } else if (index >= Gen7Constants.tmBlockTwoOffset + && index < Gen7Constants.tmBlockTwoOffset + Gen7Constants.tmBlockTwoCount) { + return (index + Gen7Constants.tmBlockOneCount) - (Gen7Constants.tmBlockTwoOffset - 1); + } else { + return (index + Gen7Constants.tmBlockOneCount + Gen7Constants.tmBlockTwoCount) - (Gen7Constants.tmBlockThreeOffset - 1); + } + } + + private int indexFromTM(int tm) { + if (tm >= 1 && tm <= Gen7Constants.tmBlockOneCount) { + return tm + (Gen7Constants.tmBlockOneOffset - 1); + } else if (tm > Gen7Constants.tmBlockOneCount && tm <= Gen7Constants.tmBlockOneCount + Gen7Constants.tmBlockTwoCount) { + return tm + (Gen7Constants.tmBlockTwoOffset - 1 - Gen7Constants.tmBlockOneCount); + } else { + return tm + (Gen7Constants.tmBlockThreeOffset - 1 - (Gen7Constants.tmBlockOneCount + Gen7Constants.tmBlockTwoCount)); + } + } + + @Override + public List<Integer> getCurrentFieldTMs() { + List<Integer> fieldItems = this.getFieldItems(); + List<Integer> fieldTMs = new ArrayList<>(); + + ItemList allowedItems = Gen7Constants.getAllowedItems(romEntry.romType); + for (int item : fieldItems) { + if (allowedItems.isTM(item)) { + fieldTMs.add(tmFromIndex(item)); + } + } + + return fieldTMs.stream().distinct().collect(Collectors.toList()); + } + + @Override + public void setFieldTMs(List<Integer> fieldTMs) { + List<Integer> fieldItems = this.getFieldItems(); + int fiLength = fieldItems.size(); + Iterator<Integer> iterTMs = fieldTMs.iterator(); + Map<Integer,Integer> tmMap = new HashMap<>(); + + ItemList allowedItems = Gen7Constants.getAllowedItems(romEntry.romType); + for (int i = 0; i < fiLength; i++) { + int oldItem = fieldItems.get(i); + if (allowedItems.isTM(oldItem)) { + if (tmMap.get(oldItem) != null) { + fieldItems.set(i,tmMap.get(oldItem)); + continue; + } + int newItem = indexFromTM(iterTMs.next()); + fieldItems.set(i, newItem); + tmMap.put(oldItem,newItem); + } + } + + this.setFieldItems(fieldItems); + } + + @Override + public List<Integer> getRegularFieldItems() { + List<Integer> fieldItems = this.getFieldItems(); + List<Integer> fieldRegItems = new ArrayList<>(); + + ItemList allowedItems = Gen7Constants.getAllowedItems(romEntry.romType); + for (int item : fieldItems) { + if (allowedItems.isAllowed(item) && !(allowedItems.isTM(item))) { + fieldRegItems.add(item); + } + } + + return fieldRegItems; + } + + @Override + public void setRegularFieldItems(List<Integer> items) { + List<Integer> fieldItems = this.getFieldItems(); + int fiLength = fieldItems.size(); + Iterator<Integer> iterNewItems = items.iterator(); + + ItemList allowedItems = Gen7Constants.getAllowedItems(romEntry.romType); + for (int i = 0; i < fiLength; i++) { + int oldItem = fieldItems.get(i); + if (!(allowedItems.isTM(oldItem)) && allowedItems.isAllowed(oldItem)) { + int newItem = iterNewItems.next(); + fieldItems.set(i, newItem); + } + } + + this.setFieldItems(fieldItems); + } + + @Override + public List<Integer> getRequiredFieldTMs() { + return Gen7Constants.getRequiredFieldTMs(romEntry.romType); + } + + public List<Integer> getFieldItems() { + List<Integer> fieldItems = new ArrayList<>(); + int numberOfAreas = encounterGarc.files.size() / 11; + for (int i = 0; i < numberOfAreas; i++) { + byte[][] environmentData = Mini.UnpackMini(encounterGarc.getFile(i * 11),"ED"); + if (environmentData == null) continue; + + byte[][] itemDataFull = Mini.UnpackMini(environmentData[10],"EI"); + + byte[][] berryPileDataFull = Mini.UnpackMini(environmentData[11],"EB"); + + // Field/hidden items + for (byte[] itemData: itemDataFull) { + if (itemData.length > 0) { + int itemCount = itemData[0]; + + for (int j = 0; j < itemCount; j++) { + fieldItems.add(FileFunctions.read2ByteInt(itemData,(j * 64) + 52)); + } + } + } + + // Berry piles + for (byte[] berryPileData: berryPileDataFull) { + if (berryPileData.length > 0) { + int pileCount = berryPileData[0]; + for (int j = 0; j < pileCount; j++) { + for (int k = 0; k < 7; k++) { + fieldItems.add(FileFunctions.read2ByteInt(berryPileData,4 + j*68 + 54 + k*2)); + } + } + } + } + } + return fieldItems; + } + + public void setFieldItems(List<Integer> items) { + try { + int numberOfAreas = encounterGarc.files.size() / 11; + Iterator<Integer> iterItems = items.iterator(); + for (int i = 0; i < numberOfAreas; i++) { + byte[][] environmentData = Mini.UnpackMini(encounterGarc.getFile(i * 11),"ED"); + if (environmentData == null) continue; + + byte[][] itemDataFull = Mini.UnpackMini(environmentData[10],"EI"); + + byte[][] berryPileDataFull = Mini.UnpackMini(environmentData[11],"EB"); + + // Field/hidden items + for (byte[] itemData: itemDataFull) { + if (itemData.length > 0) { + int itemCount = itemData[0]; + + for (int j = 0; j < itemCount; j++) { + FileFunctions.write2ByteInt(itemData,(j * 64) + 52,iterItems.next()); + } + } + } + + byte[] itemDataPacked = Mini.PackMini(itemDataFull,"EI"); + environmentData[10] = itemDataPacked; + + // Berry piles + for (byte[] berryPileData: berryPileDataFull) { + if (berryPileData.length > 0) { + int pileCount = berryPileData[0]; + + for (int j = 0; j < pileCount; j++) { + for (int k = 0; k < 7; k++) { + FileFunctions.write2ByteInt(berryPileData,4 + j*68 + 54 + k*2,iterItems.next()); + } + } + } + } + + byte[] berryPileDataPacked = Mini.PackMini(berryPileDataFull,"EB"); + environmentData[11] = berryPileDataPacked; + + encounterGarc.setFile(i * 11, Mini.PackMini(environmentData,"ED")); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public List<IngameTrade> getIngameTrades() { + List<IngameTrade> ingameTrades = new ArrayList<>(); + try { + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + List<String> tradeStrings = getStrings(true, romEntry.getInt("IngameTradesTextOffset")); + byte[] tradesFile = staticGarc.files.get(4).get(0); + int numberOfIngameTrades = tradesFile.length / 0x34; + for (int i = 0; i < numberOfIngameTrades; i++) { + int offset = i * 0x34; + IngameTrade trade = new IngameTrade(); + int givenSpecies = FileFunctions.read2ByteInt(tradesFile, offset); + int requestedSpecies = FileFunctions.read2ByteInt(tradesFile, offset + 0x2C); + Pokemon givenPokemon = pokes[givenSpecies]; + Pokemon requestedPokemon = pokes[requestedSpecies]; + int forme = tradesFile[offset + 4]; + if (forme > givenPokemon.cosmeticForms && forme != 30 && forme != 31) { + int speciesWithForme = absolutePokeNumByBaseForme + .getOrDefault(givenSpecies, dummyAbsolutePokeNums) + .getOrDefault(forme, 0); + givenPokemon = pokes[speciesWithForme]; + } + trade.givenPokemon = givenPokemon; + trade.requestedPokemon = requestedPokemon; + trade.nickname = tradeStrings.get(FileFunctions.read2ByteInt(tradesFile, offset + 2)); + trade.otName = tradeStrings.get(FileFunctions.read2ByteInt(tradesFile, offset + 0x18)); + trade.otId = FileFunctions.readFullInt(tradesFile, offset + 0x10); + trade.ivs = new int[6]; + for (int iv = 0; iv < 6; iv++) { + trade.ivs[iv] = tradesFile[offset + 6 + iv]; + } + trade.item = FileFunctions.read2ByteInt(tradesFile, offset + 0x14); + if (trade.item < 0) { + trade.item = 0; + } + ingameTrades.add(trade); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + return ingameTrades; + } + + @Override + public void setIngameTrades(List<IngameTrade> trades) { + try { + List<IngameTrade> oldTrades = this.getIngameTrades(); + GARCArchive staticGarc = readGARC(romEntry.getFile("StaticPokemon"), true); + List<String> tradeStrings = getStrings(true, romEntry.getInt("IngameTradesTextOffset")); + Map<Integer, List<Integer>> hardcodedTradeTextOffsets = Gen7Constants.getHardcodedTradeTextOffsets(romEntry.romType); + byte[] tradesFile = staticGarc.files.get(4).get(0); + int numberOfIngameTrades = tradesFile.length / 0x34; + for (int i = 0; i < numberOfIngameTrades; i++) { + IngameTrade trade = trades.get(i); + int offset = i * 0x34; + Pokemon givenPokemon = trade.givenPokemon; + int forme = 0; + if (givenPokemon.formeNumber > 0) { + forme = givenPokemon.formeNumber; + givenPokemon = givenPokemon.baseForme; + } + FileFunctions.write2ByteInt(tradesFile, offset, givenPokemon.number); + tradesFile[offset + 4] = (byte) forme; + FileFunctions.write2ByteInt(tradesFile, offset + 0x2C, trade.requestedPokemon.number); + tradeStrings.set(FileFunctions.read2ByteInt(tradesFile, offset + 2), trade.nickname); + tradeStrings.set(FileFunctions.read2ByteInt(tradesFile, offset + 0x18), trade.otName); + FileFunctions.writeFullInt(tradesFile, offset + 0x10, trade.otId); + for (int iv = 0; iv < 6; iv++) { + tradesFile[offset + 6 + iv] = (byte) trade.ivs[iv]; + } + FileFunctions.write2ByteInt(tradesFile, offset + 0x14, trade.item); + + List<Integer> hardcodedTextOffsetsForThisTrade = hardcodedTradeTextOffsets.get(i); + if (hardcodedTextOffsetsForThisTrade != null) { + updateHardcodedTradeText(oldTrades.get(i), trade, tradeStrings, hardcodedTextOffsetsForThisTrade); + } + } + writeGARC(romEntry.getFile("StaticPokemon"), staticGarc); + setStrings(true, romEntry.getInt("IngameTradesTextOffset"), tradeStrings); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + // 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, List<String> tradeStrings, List<Integer> hardcodedTextOffsets) { + for (int offset : hardcodedTextOffsets) { + String hardcodedText = tradeStrings.get(offset); + String oldRequestedName = oldTrade.requestedPokemon.name; + String oldGivenName = oldTrade.givenPokemon.name; + String newRequestedName = newTrade.requestedPokemon.name; + String newGivenName = newTrade.givenPokemon.name; + hardcodedText = hardcodedText.replace(oldRequestedName, newRequestedName); + hardcodedText = hardcodedText.replace(oldGivenName, newGivenName); + tradeStrings.set(offset, hardcodedText); + } + } + + @Override + public boolean hasDVs() { + return false; + } + + @Override + public int generationOfPokemon() { + return 7; + } + + @Override + public void removeEvosForPokemonPool() { + // slightly more complicated than gen2/3 + // we have to update a "baby table" too + List<Pokemon> pokemonIncluded = this.mainPokemonListInclFormes; + Set<Evolution> keepEvos = new HashSet<>(); + for (Pokemon pk : pokes) { + if (pk != null) { + keepEvos.clear(); + for (Evolution evol : pk.evolutionsFrom) { + if (pokemonIncluded.contains(evol.from) && pokemonIncluded.contains(evol.to)) { + keepEvos.add(evol); + } else { + evol.to.evolutionsTo.remove(evol); + } + } + pk.evolutionsFrom.retainAll(keepEvos); + } + } + + try { + // baby pokemon + GARCArchive babyGarc = readGARC(romEntry.getFile("BabyPokemon"), true); + int pokemonCount = Gen7Constants.getPokemonCount(romEntry.romType); + byte[] masterFile = babyGarc.getFile(pokemonCount + 1); + for (int i = 1; i <= 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(pokemonCount + 1, masterFile); + writeGARC(romEntry.getFile("BabyPokemon"), babyGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public boolean supportsFourStartingMoves() { + return true; + } + + @Override + public List<Integer> getFieldMoves() { + // Gen 7 does not have field moves + return new ArrayList<>(); + } + + @Override + public List<Integer> getEarlyRequiredHMMoves() { + // Gen 7 does not have any HMs + return new ArrayList<>(); + } + + @Override + public Map<Integer, Shop> 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<Integer, Shop> shopItemsMap = new TreeMap<>(); + try { + byte[] shopsCRO = readFile(romEntry.getFile("ShopsAndTutors")); + int offset = Gen7Constants.getShopItemsOffset(romEntry.romType); + 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<Integer> items = new ArrayList<>(); + for (int j = 0; j < shopItemSizes[i]; j++) { + items.add(FileFunctions.read2ByteInt(shopsCRO, offset)); + offset += 2; + } + Shop shop = new Shop(); + shop.items = items; + shop.name = shopNames.get(i); + shop.isMainGame = Gen7Constants.getMainGameShops(romEntry.romType).contains(i); + shopItemsMap.put(i, shop); + } + } + return shopItemsMap; + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public void setShopItems(Map<Integer, Shop> shopItems) { + int[] tmShops = romEntry.arrayEntries.get("TMShops"); + int[] regularShops = romEntry.arrayEntries.get("RegularShops"); + int[] shopItemSizes = romEntry.arrayEntries.get("ShopItemSizes"); + int shopCount = romEntry.getInt("ShopCount"); + try { + byte[] shopsCRO = readFile(romEntry.getFile("ShopsAndTutors")); + int offset = Gen7Constants.getShopItemsOffset(romEntry.romType); + 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<Integer> shopContents = shopItems.get(i).items; + Iterator<Integer> iterItems = shopContents.iterator(); + for (int j = 0; j < shopItemSizes[i]; j++) { + Integer item = iterItems.next(); + FileFunctions.write2ByteInt(shopsCRO, offset, item); + offset += 2; + } + } + } + writeFile(romEntry.getFile("ShopsAndTutors"), shopsCRO); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @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, Gen7Constants.balancedItemPrices.get(i)); + } + writeGARC(romEntry.getFile("ItemData"),itemPriceGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + @Override + public List<PickupItem> getPickupItems() { + List<PickupItem> pickupItems = new ArrayList<>(); + try { + GARCArchive pickupGarc = this.readGARC(romEntry.getFile("PickupData"), false); + byte[] pickupData = pickupGarc.getFile(0); + int numberOfPickupItems = FileFunctions.readFullInt(pickupData, 0) - 1; // GameFreak why??? + for (int i = 0; i < numberOfPickupItems; i++) { + int offset = 4 + (i * 0xC); + int item = FileFunctions.read2ByteInt(pickupData, offset); + PickupItem pickupItem = new PickupItem(item); + for (int levelRange = 0; levelRange < 10; levelRange++) { + pickupItem.probabilities[levelRange] = pickupData[offset + levelRange + 2]; + } + pickupItems.add(pickupItem); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + return pickupItems; + } + + @Override + public void setPickupItems(List<PickupItem> pickupItems) { + try { + GARCArchive pickupGarc = this.readGARC(romEntry.getFile("PickupData"), false); + byte[] pickupData = pickupGarc.getFile(0); + for (int i = 0; i < pickupItems.size(); i++) { + int offset = 4 + (i * 0xC); + int item = pickupItems.get(i).item; + FileFunctions.write2ByteInt(pickupData, offset, item); + } + this.writeGARC(romEntry.getFile("PickupData"), pickupGarc); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + 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() - 1) + 1; + if (romEntry.romType == Gen7Constants.Type_SM) { + while (pkIndex == 1109 || pkIndex == 1117) { + pkIndex = this.random.nextInt(pokespritesGARC.files.size() - 1) + 1; + } + } + byte[] iconBytes = pokespritesGARC.files.get(pkIndex).get(0); + BFLIM icon = new BFLIM(iconBytes); + return icon.getImage(); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + private class ZoneData { + public int worldIndex; + public int areaIndex; + public int parentMap; + public String locationName; + private byte[] data; + + public static final int size = 0x54; + + public ZoneData(byte[] zoneDataBytes, int index) { + data = new byte[size]; + System.arraycopy(zoneDataBytes, index * size, data, 0, size); + parentMap = FileFunctions.readFullInt(data, 0x1C); + } + } + + private class AreaData { + public int fileNumber; + public boolean hasTables; + public List<byte[]> encounterTables; + public List<ZoneData> zones; + public String name; + + public AreaData() { + encounterTables = new ArrayList<>(); + } + } + + @Override + public List<Integer> getAllConsumableHeldItems() { + return Gen7Constants.consumableHeldItems; + } + + @Override + public List<Integer> getAllHeldItems() { + return Gen7Constants.allHeldItems; + } + + @Override + public boolean hasRivalFinalBattle() { + return true; + } + + @Override + public List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, int[] pokeMoves) { + List<Integer> items = new ArrayList<>(); + items.addAll(Gen7Constants.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(Gen7Constants.generalPurposeItems); + } + 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(Gen7Constants.consumableTypeBoostingItems.get(move.type)); + if (!consumableOnly) { + items.addAll(Gen7Constants.typeBoostingItems.get(move.type)); + items.add(Items.choiceBand); + items.add(Items.muscleBand); + } + } + if (move.category == MoveCategory.SPECIAL) { + numDamagingMoves++; + items.add(Items.petayaBerry); + items.add(Gen7Constants.consumableTypeBoostingItems.get(move.type)); + if (!consumableOnly) { + items.addAll(Gen7Constants.typeBoostingItems.get(move.type)); + items.add(Items.wiseGlasses); + items.add(Items.choiceSpecs); + } + } + if (!consumableOnly && Gen7Constants.moveBoostingItems.containsKey(moveIdx)) { + items.addAll(Gen7Constants.moveBoostingItems.get(moveIdx)); + } + } + if (numDamagingMoves >= 2) { + items.add(Items.assaultVest); + } + Map<Type, Effectiveness> byType = Effectiveness.against(tp.pokemon.primaryType, tp.pokemon.secondaryType, 7); + for(Map.Entry<Type, Effectiveness> entry : byType.entrySet()) { + Integer berry = Gen7Constants.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 (Gen7Constants.consumableAbilityBoostingItems.containsKey(ability)) { + items.add(Gen7Constants.consumableAbilityBoostingItems.get(ability)); + } + + if (!consumableOnly) { + if (Gen7Constants.abilityBoostingItems.containsKey(ability)) { + items.addAll(Gen7Constants.abilityBoostingItems.get(ability)); + } + if (tp.pokemon.primaryType == Type.POISON || tp.pokemon.secondaryType == Type.POISON) { + items.add(Items.blackSludge); + } + List<Integer> speciesItems = Gen7Constants.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; + } +} diff --git a/src/com/pkrandom/romhandlers/RomHandler.java b/src/com/pkrandom/romhandlers/RomHandler.java new file mode 100755 index 0000000..4d2e2c8 --- /dev/null +++ b/src/com/pkrandom/romhandlers/RomHandler.java @@ -0,0 +1,660 @@ +package com.pkrandom.romhandlers;
+
+/*----------------------------------------------------------------------------*/
+/*-- RomHandler.java - defines the functionality that each randomization --*/
+/*-- handler must implement. --*/
+/*-- --*/
+/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/
+/*-- Pokemon and any associated names and the like are --*/
+/*-- trademark and (C) Nintendo 1996-2020. --*/
+/*-- --*/
+/*-- The custom code written here is licensed under the terms of the GPL: --*/
+/*-- --*/
+/*-- This program is free software: you can redistribute it and/or modify --*/
+/*-- it under the terms of the GNU General Public License as published by --*/
+/*-- the Free Software Foundation, either version 3 of the License, or --*/
+/*-- (at your option) any later version. --*/
+/*-- --*/
+/*-- This program is distributed in the hope that it will be useful, --*/
+/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/
+/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/
+/*-- GNU General Public License for more details. --*/
+/*-- --*/
+/*-- You should have received a copy of the GNU General Public License --*/
+/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/
+/*----------------------------------------------------------------------------*/
+
+import java.awt.image.BufferedImage;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import com.pkrandom.MiscTweak;
+import com.pkrandom.Settings;
+import com.pkrandom.pokemon.*;
+
+public interface RomHandler {
+
+ abstract class Factory {
+ public RomHandler create(Random random) {
+ return create(random, null);
+ }
+
+ public abstract RomHandler create(Random random, PrintStream log);
+
+ public abstract boolean isLoadable(String filename);
+ }
+
+ // =======================
+ // Basic load/save methods
+ // =======================
+
+ boolean loadRom(String filename);
+
+ boolean saveRomFile(String filename, long seed);
+
+ boolean saveRomDirectory(String filename);
+
+ String loadedFilename();
+
+ // =============================================================
+ // Methods relating to game updates for the 3DS and Switch games
+ // =============================================================
+
+ boolean hasGameUpdateLoaded();
+
+ boolean loadGameUpdate(String filename);
+
+ void removeGameUpdate();
+
+ String getGameUpdateVersion();
+
+ // ===========
+ // Log methods
+ // ===========
+
+ void setLog(PrintStream logStream);
+
+ void printRomDiagnostics(PrintStream logStream);
+
+ boolean isRomValid();
+
+ // ======================================================
+ // Methods for retrieving a list of Pokemon objects.
+ // Note that for many of these lists, index 0 is null.
+ // Instead, you use index on the species' National Dex ID
+ // ======================================================
+
+ List<Pokemon> getPokemon();
+
+ List<Pokemon> getPokemonInclFormes();
+
+ List<Pokemon> getAltFormes();
+
+ List<MegaEvolution> getMegaEvolutions();
+
+ Pokemon getAltFormeOfPokemon(Pokemon pk, int forme);
+
+ List<Pokemon> getIrregularFormes();
+
+ // ==================================
+ // Methods to set up Gen Restrictions
+ // ==================================
+
+ void setPokemonPool(Settings settings);
+
+ void removeEvosForPokemonPool();
+
+ // ===============
+ // Starter Pokemon
+ // ===============
+
+ List<Pokemon> getStarters();
+
+ boolean setStarters(List<Pokemon> newStarters);
+
+ boolean hasStarterAltFormes();
+
+ int starterCount();
+
+ void customStarters(Settings settings);
+
+ void randomizeStarters(Settings settings);
+
+ void randomizeBasicTwoEvosStarters(Settings settings);
+
+ List<Pokemon> getPickedStarters();
+
+ boolean supportsStarterHeldItems();
+
+ List<Integer> getStarterHeldItems();
+
+ void setStarterHeldItems(List<Integer> items);
+
+ void randomizeStarterHeldItems(Settings settings);
+
+ // =======================
+ // Pokemon Base Statistics
+ // =======================
+
+ // Run the stats shuffler on each Pokemon.
+ void shufflePokemonStats(Settings settings);
+
+ // Randomize stats following evolutions for proportions or not (see
+ // tooltips)
+ void randomizePokemonStats(Settings settings);
+
+ // Update base stats to specified generation
+ void updatePokemonStats(Settings settings);
+
+ Map<Integer,StatChange> getUpdatedPokemonStats(int generation);
+
+ void standardizeEXPCurves(Settings settings);
+
+ // ====================================
+ // Methods for selecting random Pokemon
+ // ====================================
+
+ // Give a random Pokemon who's in this game
+ Pokemon randomPokemon();
+
+ Pokemon randomPokemonInclFormes();
+
+ // Give a random non-legendary Pokemon who's in this game
+ // Business rules for who's legendary are in Pokemon class
+ Pokemon randomNonLegendaryPokemon();
+
+ // Give a random legendary Pokemon who's in this game
+ // Business rules for who's legendary are in Pokemon class
+ Pokemon randomLegendaryPokemon();
+
+ // Give a random Pokemon who has 2 evolution stages
+ // Should make a good starter Pokemon
+ Pokemon random2EvosPokemon(boolean allowAltFormes);
+
+ // =============
+ // Pokemon Types
+ // =============
+
+ // return a random type valid in this game.
+ Type randomType();
+
+ boolean typeInGame(Type type);
+
+ // randomize Pokemon types, with a switch on whether evolutions
+ // should follow the same types or not.
+ // some evolutions dont anyway, e.g. Eeveelutions, Hitmons
+ void randomizePokemonTypes(Settings settings);
+
+ // =================
+ // Pokemon Abilities
+ // =================
+
+ int abilitiesPerPokemon();
+
+ int highestAbilityIndex();
+
+ String abilityName(int number);
+
+ void randomizeAbilities(Settings settings);
+
+ Map<Integer,List<Integer>> getAbilityVariations();
+
+ List<Integer> getUselessAbilities();
+
+ int getAbilityForTrainerPokemon(TrainerPokemon tp);
+
+ boolean hasMegaEvolutions();
+
+ // ============
+ // Wild Pokemon
+ // ============
+
+ List<EncounterSet> getEncounters(boolean useTimeOfDay);
+
+ void setEncounters(boolean useTimeOfDay, List<EncounterSet> encounters);
+
+ void randomEncounters(Settings settings);
+
+ void area1to1Encounters(Settings settings);
+
+ void game1to1Encounters(Settings settings);
+
+ void onlyChangeWildLevels(Settings settings);
+
+ boolean hasTimeBasedEncounters();
+
+ boolean hasWildAltFormes();
+
+ List<Pokemon> bannedForWildEncounters();
+
+ void randomizeWildHeldItems(Settings settings);
+
+ void changeCatchRates(Settings settings);
+
+ void minimumCatchRate(int rateNonLegendary, int rateLegendary);
+
+ void enableGuaranteedPokemonCatching();
+
+ // ===============
+ // Trainer Pokemon
+ // ===============
+
+ List<Trainer> getTrainers();
+
+ List<Integer> getMainPlaythroughTrainers();
+
+ List<Integer> getEliteFourTrainers(boolean isChallengeMode);
+
+ void setTrainers(List<Trainer> trainerData, boolean doubleBattleMode);
+
+ void randomizeTrainerPokes(Settings settings);
+
+ void randomizeTrainerHeldItems(Settings settings);
+
+ List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, int[] pokeMoves);
+
+ List<Integer> getAllConsumableHeldItems();
+
+ List<Integer> getAllHeldItems();
+
+ void rivalCarriesStarter();
+
+ boolean hasRivalFinalBattle();
+
+ void forceFullyEvolvedTrainerPokes(Settings settings);
+
+ void onlyChangeTrainerLevels(Settings settings);
+
+ void addTrainerPokemon(Settings settings);
+
+ void doubleBattleMode();
+
+ List<Move> getMoveSelectionPoolAtLevel(TrainerPokemon tp, boolean cyclicEvolutions);
+
+ void pickTrainerMovesets(Settings settings);
+
+ // =========
+ // Move Data
+ // =========
+
+ void randomizeMovePowers();
+
+ void randomizeMovePPs();
+
+ void randomizeMoveAccuracies();
+
+ void randomizeMoveTypes();
+
+ boolean hasPhysicalSpecialSplit();
+
+ void randomizeMoveCategory();
+
+ void updateMoves(Settings settings);
+
+ // stuff for printing move changes
+ void initMoveUpdates();
+
+ Map<Integer, boolean[]> getMoveUpdates();
+
+ // return all the moves valid in this game.
+ List<Move> getMoves();
+
+ // ================
+ // Pokemon Movesets
+ // ================
+
+ Map<Integer, List<MoveLearnt>> getMovesLearnt();
+
+ void setMovesLearnt(Map<Integer, List<MoveLearnt>> movesets);
+
+ List<Integer> getMovesBannedFromLevelup();
+
+ Map<Integer, List<Integer>> getEggMoves();
+
+ void setEggMoves(Map<Integer, List<Integer>> eggMoves);
+
+ void randomizeMovesLearnt(Settings settings);
+
+ void randomizeEggMoves(Settings settings);
+
+ void orderDamagingMovesByDamage();
+
+ void metronomeOnlyMode();
+
+ boolean supportsFourStartingMoves();
+
+ // ==============
+ // Static Pokemon
+ // ==============
+
+ List<StaticEncounter> getStaticPokemon();
+
+ boolean setStaticPokemon(List<StaticEncounter> staticPokemon);
+
+ void randomizeStaticPokemon(Settings settings);
+
+ boolean canChangeStaticPokemon();
+
+ boolean hasStaticAltFormes();
+
+ List<Pokemon> bannedForStaticPokemon();
+
+ boolean forceSwapStaticMegaEvos();
+
+ void onlyChangeStaticLevels(Settings settings);
+
+ boolean hasMainGameLegendaries();
+
+ List<Integer> getMainGameLegendaries();
+
+ List<Integer> getSpecialMusicStatics();
+
+ void applyCorrectStaticMusic(Map<Integer,Integer> specialMusicStaticChanges);
+
+ boolean hasStaticMusicFix();
+
+ // =============
+ // Totem Pokemon
+ // =============
+
+ List<TotemPokemon> getTotemPokemon();
+
+ void setTotemPokemon(List<TotemPokemon> totemPokemon);
+
+ void randomizeTotemPokemon(Settings settings);
+
+ // =========
+ // TMs & HMs
+ // =========
+
+ List<Integer> getTMMoves();
+
+ List<Integer> getHMMoves();
+
+ void setTMMoves(List<Integer> moveIndexes);
+
+ void randomizeTMMoves(Settings settings);
+
+ int getTMCount();
+
+ int getHMCount();
+
+ /**
+ * Get TM/HM compatibility data from this rom. The result should contain a
+ * boolean array for each Pokemon indexed as such:
+ *
+ * 0: blank (false) / 1 - (getTMCount()) : TM compatibility /
+ * (getTMCount()+1) - (getTMCount()+getHMCount()) - HM compatibility
+ *
+ * @return Map of TM/HM compatibility
+ */
+
+ Map<Pokemon, boolean[]> getTMHMCompatibility();
+
+ void setTMHMCompatibility(Map<Pokemon, boolean[]> compatData);
+
+ void randomizeTMHMCompatibility(Settings settings);
+
+ void fullTMHMCompatibility();
+
+ void ensureTMCompatSanity();
+
+ void ensureTMEvolutionSanity();
+
+ void fullHMCompatibility();
+
+ // ===========
+ // Move Tutors
+ // ===========
+
+ void copyTMCompatibilityToCosmeticFormes();
+
+ boolean hasMoveTutors();
+
+ List<Integer> getMoveTutorMoves();
+
+ void setMoveTutorMoves(List<Integer> moves);
+
+ void randomizeMoveTutorMoves(Settings settings);
+
+ Map<Pokemon, boolean[]> getMoveTutorCompatibility();
+
+ void setMoveTutorCompatibility(Map<Pokemon, boolean[]> compatData);
+
+ void randomizeMoveTutorCompatibility(Settings settings);
+
+ void fullMoveTutorCompatibility();
+
+ void ensureMoveTutorCompatSanity();
+
+ void ensureMoveTutorEvolutionSanity();
+
+ // =============
+ // Trainer Names
+ // =============
+
+ void copyMoveTutorCompatibilityToCosmeticFormes();
+
+ boolean canChangeTrainerText();
+
+ List<String> getTrainerNames();
+
+ void setTrainerNames(List<String> trainerNames);
+
+ enum TrainerNameMode {
+ SAME_LENGTH, MAX_LENGTH, MAX_LENGTH_WITH_CLASS
+ }
+
+ TrainerNameMode trainerNameMode();
+
+ // Returns this with or without the class
+ int maxTrainerNameLength();
+
+ // Only relevant for gen2, which has fluid trainer name length but
+ // only a certain amount of space in the ROM bank.
+ int maxSumOfTrainerNameLengths();
+
+ // Only needed if above mode is "MAX LENGTH WITH CLASS"
+ List<Integer> getTCNameLengthsByTrainer();
+
+ void randomizeTrainerNames(Settings settings);
+
+ // ===============
+ // Trainer Classes
+ // ===============
+
+ List<String> getTrainerClassNames();
+
+ void setTrainerClassNames(List<String> trainerClassNames);
+
+ boolean fixedTrainerClassNamesLength();
+
+ int maxTrainerClassNameLength();
+
+ void randomizeTrainerClassNames(Settings settings);
+
+ List<Integer> getDoublesTrainerClasses();
+
+ // =====
+ // Items
+ // =====
+
+ ItemList getAllowedItems();
+
+ ItemList getNonBadItems();
+
+ List<Integer> getEvolutionItems();
+
+ List<Integer> getXItems();
+
+ List<Integer> getUniqueNoSellItems();
+
+ List<Integer> getRegularShopItems();
+
+ List<Integer> getOPShopItems();
+
+ String[] getItemNames();
+
+ // ===========
+ // Field Items
+ // ===========
+
+ // TMs on the field
+
+ List<Integer> getRequiredFieldTMs();
+
+ List<Integer> getCurrentFieldTMs();
+
+ void setFieldTMs(List<Integer> fieldTMs);
+
+ // Everything else
+
+ List<Integer> getRegularFieldItems();
+
+ void setRegularFieldItems(List<Integer> items);
+
+ // Randomizer methods
+
+ void shuffleFieldItems();
+
+ void randomizeFieldItems(Settings settings);
+
+ // ============
+ // Special Shops
+ // =============
+
+ boolean hasShopRandomization();
+
+ void shuffleShopItems();
+
+ void randomizeShopItems(Settings settings);
+
+ Map<Integer, Shop> getShopItems();
+
+ void setShopItems(Map<Integer, Shop> shopItems);
+
+ void setShopPrices();
+
+ // ============
+ // Pickup Items
+ // ============
+
+ List<PickupItem> getPickupItems();
+
+ void setPickupItems(List<PickupItem> pickupItems);
+
+ void randomizePickupItems(Settings settings);
+
+ // ==============
+ // In-Game Trades
+ // ==============
+
+ List<IngameTrade> getIngameTrades();
+
+ void setIngameTrades(List<IngameTrade> trades);
+
+ void randomizeIngameTrades(Settings settings);
+
+ boolean hasDVs();
+
+ int maxTradeNicknameLength();
+
+ int maxTradeOTNameLength();
+
+ // ==================
+ // Pokemon Evolutions
+ // ==================
+
+ void removeImpossibleEvolutions(Settings settings);
+
+ void condenseLevelEvolutions(int maxLevel, int maxIntermediateLevel);
+
+ void makeEvolutionsEasier(Settings settings);
+
+ void removeTimeBasedEvolutions();
+
+ Set<EvolutionUpdate> getImpossibleEvoUpdates();
+
+ Set<EvolutionUpdate> getEasierEvoUpdates();
+
+ Set<EvolutionUpdate> getTimeBasedEvoUpdates();
+
+ void randomizeEvolutions(Settings settings);
+
+ void randomizeEvolutionsEveryLevel(Settings settings);
+
+ // In the earlier games, alt formes use the same evolutions as the base forme.
+ // In later games, this was changed so that alt formes can have unique evolutions
+ // compared to the base forme.
+ boolean altFormesCanHaveDifferentEvolutions();
+
+ // ==================================
+ // (Mostly) unchanging lists of moves
+ // ==================================
+
+ List<Integer> getGameBreakingMoves();
+
+ List<Integer> getIllegalMoves();
+
+ // includes game or gen-specific moves like Secret Power
+ // but NOT healing moves (Softboiled, Milk Drink)
+ List<Integer> getFieldMoves();
+
+ // any HMs required to obtain 4 badges
+ // (excluding Gameshark codes or early drink in RBY)
+ List<Integer> getEarlyRequiredHMMoves();
+
+
+ // ====
+ // Misc
+ // ====
+
+ boolean isYellow();
+
+ String getROMName();
+
+ String getROMCode();
+
+ String getSupportLevel();
+
+ String getDefaultExtension();
+
+ int internalStringLength(String string);
+
+ void randomizeIntroPokemon();
+
+ BufferedImage getMascotImage();
+
+ int generationOfPokemon();
+
+ void writeCheckValueToROM(int value);
+
+ // ===========
+ // code tweaks
+ // ===========
+
+ int miscTweaksAvailable();
+
+ void applyMiscTweaks(Settings settings);
+
+ void applyMiscTweak(MiscTweak tweak);
+
+ boolean isEffectivenessUpdated();
+
+ void renderPlacementHistory();
+
+ // ==========================
+ // Misc forme-related methods
+ // ==========================
+
+ boolean hasFunctionalFormes();
+
+ List<Pokemon> getAbilityDependentFormes();
+
+ List<Pokemon> getBannedFormesForPlayerPokemon();
+
+ List<Pokemon> getBannedFormesForTrainerPokemon();
+}
\ No newline at end of file |