summaryrefslogtreecommitdiff
path: root/src/com/pkrandom/romhandlers
diff options
context:
space:
mode:
authorRafael Marçalo <public@rafaelmarcalo.xyz>2024-09-05 16:31:33 +0100
committerRafael Marçalo <public@rafaelmarcalo.xyz>2024-09-05 16:31:33 +0100
commit8b67572ad7e1508341345dc46a2597e9fa170cbb (patch)
tree8f37c4d60ce0f07b9eaf30be34f39298da97b242 /src/com/pkrandom/romhandlers
parentb65f4a80da28e7ec4de16c8b1abf906e8d7be2c5 (diff)
Removed invasive brandingHEADmaster
Diffstat (limited to 'src/com/pkrandom/romhandlers')
-rw-r--r--src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java350
-rwxr-xr-xsrc/com/pkrandom/romhandlers/AbstractDSRomHandler.java390
-rw-r--r--src/com/pkrandom/romhandlers/AbstractGBCRomHandler.java224
-rwxr-xr-xsrc/com/pkrandom/romhandlers/AbstractGBRomHandler.java210
-rwxr-xr-xsrc/com/pkrandom/romhandlers/AbstractRomHandler.java7558
-rwxr-xr-xsrc/com/pkrandom/romhandlers/Gen1RomHandler.java2918
-rwxr-xr-xsrc/com/pkrandom/romhandlers/Gen2RomHandler.java2999
-rwxr-xr-xsrc/com/pkrandom/romhandlers/Gen3RomHandler.java4473
-rwxr-xr-xsrc/com/pkrandom/romhandlers/Gen4RomHandler.java5841
-rwxr-xr-xsrc/com/pkrandom/romhandlers/Gen5RomHandler.java4343
-rw-r--r--src/com/pkrandom/romhandlers/Gen6RomHandler.java4270
-rw-r--r--src/com/pkrandom/romhandlers/Gen7RomHandler.java3821
-rwxr-xr-xsrc/com/pkrandom/romhandlers/RomHandler.java660
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