package com.sneed.pkrandom.romhandlers;
/*----------------------------------------------------------------------------*/
/*-- Gen4RomHandler.java - randomizer handler for D/P/Pt/HG/SS. --*/
/*-- --*/
/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/
/*-- Originally part of "Universal Pokemon Randomizer" by sneed --*/
/*-- Pokemon and any associated names and the like are --*/
/*-- trademark and (C) Nintendo 1996-2020. --*/
/*-- --*/
/*-- The custom code written here is licensed under the terms of the GPL: --*/
/*-- --*/
/*-- This program is free software: you can redistribute it and/or modify --*/
/*-- it under the terms of the GNU General Public License as published by --*/
/*-- the Free Software Foundation, either version 3 of the License, or --*/
/*-- (at your option) any later version. --*/
/*-- --*/
/*-- This program is distributed in the hope that it will be useful, --*/
/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/
/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/
/*-- GNU General Public License for more details. --*/
/*-- --*/
/*-- You should have received a copy of the GNU General Public License --*/
/*-- along with this program. If not, see . --*/
/*----------------------------------------------------------------------------*/
import 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.sneed.pkrandom.*;
import com.sneed.pkrandom.constants.*;
import com.sneed.pkrandom.exceptions.RandomizationException;
import com.sneed.pkrandom.pokemon.*;
import thenewpoketext.PokeTextData;
import thenewpoketext.TextToPoke;
import com.sneed.pkrandom.exceptions.RandomizerIOException;
import com.sneed.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 strings = new HashMap<>();
private Map tweakFiles = new HashMap<>();
private Map numbers = new HashMap<>();
private Map arrayEntries = new HashMap<>();
private Map files = new HashMap<>();
private Map overlayExpectedCRC32s = new HashMap<>();
private List staticPokemon = new ArrayList<>();
private List roamingPokemon = new ArrayList<>();
private List marillCryScriptEntries = new ArrayList<>();
private Map> tmTexts = new HashMap<>();
private Map tmTextsGameCorner = new HashMap<>();
private Map tmScriptOffsetsFrontier = new HashMap<>();
private Map 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 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> 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 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 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 pokemonListInclFormes;
private List pokemonList;
private Move[] moves;
private NARCArchive pokeNarc, moveNarc;
private NARCArchive msgNarc;
private NARCArchive scriptNarc;
private NARCArchive eventNarc;
private byte[] arm9;
private List abilityNames;
private List itemNames;
private boolean loadedWildMapNames;
private Map wildMapNames, headbuttMapNames;
private ItemList allowedItems, nonBadItems;
private boolean roamerRandomizationEnabled;
private boolean effectivenessUpdated;
private int pickupItemsTableOffset, rarePickupItemsTableOffset;
private long actualArm9CRC32;
private Map actualOverlayCRC32s;
private Map 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"));
// 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");
}
try {
computeCRC32sForRom();
} catch (IOException e) {
throw new RandomizerIOException(e);
}
}
private void loadMoves() {
try {
moveNarc = this.readNARC(romEntry.getFile("MoveData"));
moves = new Move[Gen4Constants.moveCount + 1];
List 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 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 namesList = getStrings(romEntry.getInt("PokemonNamesTextOffset"));
int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
if (romEntry.getString("HasExtraPokemonNames").equalsIgnoreCase("Yes")) {
List 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 getPokemon() {
return pokemonList;
}
@Override
public List getPokemonInclFormes() {
return pokemonListInclFormes; // No formes for now
}
@Override
public List getAltFormes() {
int formeCount = Gen4Constants.getFormeCount(romEntry.romType);
return pokemonListInclFormes.subList(Gen4Constants.pokemonCount + 1, Gen4Constants.pokemonCount + formeCount + 1);
}
@Override
public List 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 getIrregularFormes() {
return new ArrayList<>();
}
@Override
public boolean hasFunctionalFormes() {
return romEntry.romType != Gen4Constants.Type_DP;
}
@Override
public List getStarters() {
if (romEntry.romType == Gen4Constants.Type_HGSS) {
List 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 newStarters) {
if (newStarters.size() != 3) {
return false;
}
if (romEntry.romType == Gen4Constants.Type_HGSS) {
List 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_!=
//
byte[] magic = Gen4Constants.hgssRivalScriptMagic;
NARCArchive scriptNARC = scriptNarc;
for (int fileCheck : filesWithRivalScript) {
byte[] file = scriptNARC.files.get(fileCheck);
List 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 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 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 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 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 spStrings = getStrings(romEntry.getInt("StarterScreenTextOffset"));
// Get pokedex info
List 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 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 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 List getStarterHeldItems() {
// do nothing
return new ArrayList<>();
}
@Override
public void setStarterHeldItems(List items) {
// do nothing
}
@Override
public List getMoves() {
return Arrays.asList(moves);
}
@Override
public List getEncounters(boolean useTimeOfDay) {
if (!loadedWildMapNames) {
loadWildMapNames();
}
try {
if (romEntry.romType == Gen4Constants.Type_HGSS) {
return getEncountersHGSS(useTimeOfDay);
} else {
return getEncountersDPPt(useTimeOfDay);
}
} catch (IOException ex) {
throw new RandomizerIOException(ex);
}
}
private List getEncountersDPPt(boolean useTimeOfDay) throws IOException {
// Determine file to use
String encountersFile = romEntry.getFile("WildPokemon");
NARCArchive encounterData = readNARC(encountersFile);
List 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 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 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 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 readEncountersDPPt(byte[] data, int offset, int amount) {
List 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 readSeaEncountersDPPt(byte[] data, int offset, int amount) {
List 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 getEncountersHGSS(boolean useTimeOfDay) throws IOException {
String encountersFile = romEntry.getFile("WildPokemon");
NARCArchive encounterData = readNARC(encountersFile);
List 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 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 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 readSeaEncountersHGSS(byte[] data, int offset, int amount) {
List 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 readTimeBasedRodEncountersHGSS(byte[] data, int offset, Pokemon replacement, int replacementIndex) {
List encounters = new ArrayList<>();
List rodMorningDayEncounters = readSeaEncountersHGSS(data, offset, 5);
EncounterSet rodMorningDay = new EncounterSet();
rodMorningDay.encounters = rodMorningDayEncounters;
encounters.add(rodMorningDay);
List 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 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 encounterList) throws IOException {
// Determine file to use
String encountersFile = romEntry.getFile("WildPokemon");
NARCArchive encounterData = readNARC(encountersFile);
Iterator 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 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 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 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 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 encounterList) throws IOException {
String encountersFile = romEntry.getFile("WildPokemon");
NARCArchive encounterData = readNARC(encountersFile);
Iterator 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 encounters) {
Iterator 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 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 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 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 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 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 stitchEncsToLevels(Pokemon[] pokemon, int[] levels) {
List 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 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 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[][] target;
Set[] 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 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 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 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[][] target;
Set[] 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 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 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 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 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 getTrainers() {
List allTrainers = new ArrayList<>();
try {
NARCArchive trainers = this.readNARC(romEntry.getFile("TrainerData"));
NARCArchive trpokes = this.readNARC(romEntry.getFile("TrainerPokemon"));
List tclasses = this.getTrainerClassNames();
List 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 getMainPlaythroughTrainers() {
return new ArrayList<>(); // Not implemented
}
@Override
public List getEliteFourTrainers(boolean isChallengeMode) {
return Arrays.stream(romEntry.arrayEntries.get("EliteFourIndices")).boxed().collect(Collectors.toList());
}
@Override
public List getEvolutionItems() {
return Gen4Constants.evolutionItems;
}
@Override
public void setTrainers(List trainerData, boolean doubleBattleMode) {
if (romEntry.romType == Gen4Constants.Type_HGSS) {
fixAbilitySlotValuesForHGSS(trainerData);
}
Iterator 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> 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 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 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 getBannedFormesForTrainerPokemon() {
List 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> getMovesLearnt() {
Map> 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 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> 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 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> getEggMoves() {
Map> 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> 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> readEggMoves(byte[] data, int startingOffset) {
Map> eggMoves = new TreeMap<>();
int currentOffset = startingOffset;
int currentSpecies = 0;
List 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> 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 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 getStaticPokemon() {
List 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 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 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 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 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 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 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 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 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 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 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 moveIndexes) {
List 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 itemDescriptions = getStrings(romEntry.getInt("ItemDescriptionsTextOffset"));
List 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 textEntries = romEntry.tmTexts.get(tmNumber);
Set 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 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 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 getTMHMCompatibility() {
Map 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 compatData) {
for (Map.Entry 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 getMoveTutorMoves() {
if (!hasMoveTutors()) {
return new ArrayList<>();
}
int baseOffset = romEntry.getInt("MoveTutorMovesOffset");
int amount = romEntry.getInt("MoveTutorCount");
int bytesPer = romEntry.getInt("MoveTutorBytesCount");
List 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 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 replacements = new TreeMap<>();
replacements.put(moves[Moves.headbutt].name, replacementName);
replaceAllStringsInEntry(Gen4Constants.ilexForestStringsFile, replacements);
}
@Override
public Map getMoveTutorCompatibility() {
if (!hasMoveTutors()) {
return new TreeMap<>();
}
Map 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 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 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 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 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 newStrings) {
setStrings(index, newStrings, false);
}
private void setStrings(int index, List 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 getMainGameLegendaries() {
return Arrays.stream(romEntry.arrayEntries.get("MainGameLegendaries")).boxed().collect(Collectors.toList());
}
@Override
public List getSpecialMusicStatics() {
return Arrays.stream(romEntry.arrayEntries.get("SpecialMusicStatics")).boxed().collect(Collectors.toList());
}
@Override
public List getTotemPokemon() {
return new ArrayList<>();
}
@Override
public void setTotemPokemon(List totemPokemon) {
}
@Override
public boolean hasStarterAltFormes() {
return false;
}
@Override
public int starterCount() {
return 3;
}
@Override
public Map 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> movesets = this.getMovesLearnt();
Set 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 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 getShopItems() {
List shopNames = Gen4Constants.getShopNames(romEntry.romType);
List mainGameShops = Arrays.stream(romEntry.arrayEntries.get("MainGameShops")).boxed().collect(Collectors.toList());
List skipShops = Arrays.stream(romEntry.arrayEntries.get("SkipShops")).boxed().collect(Collectors.toList());
int shopCount = romEntry.getInt("ShopCount");
Map 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 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 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 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 getPickupItems() {
List 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 pickupItems) {
try {
if (pickupItemsTableOffset > 0 && rarePickupItemsTableOffset > 0) {
byte[] battleOverlay = readOverlay(romEntry.getInt("BattleOvlNumber"));
Iterator 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 getTrainerNames() {
List 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 trainerNames) {
List oldTNames = getStrings(romEntry.getInt("TrainerNamesTextOffset"));
List 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 getTCNameLengthsByTrainer() {
// not needed
return new ArrayList<>();
}
@Override
public List getTrainerClassNames() {
return getStrings(romEntry.getInt("TrainerClassesTextOffset"));
}
@Override
public void setTrainerClassNames(List 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 getDoublesTrainerClasses() {
int[] doublesClasses = romEntry.arrayEntries.get("DoublesTrainerClasses");
List doubles = new ArrayList<>();
for (int tClass : doublesClasses) {
doubles.add(tClass);
}
return doubles;
}
@Override
public String getDefaultExtension() {
return "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);
}
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 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 getUniqueNoSellItems() {
return new ArrayList<>();
}
@Override
public List getRegularShopItems() {
return Gen4Constants.regularShopItems;
}
@Override
public List 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> getAbilityVariations() {
return Gen4Constants.abilityVariations;
}
@Override
public List 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 getFieldItems() {
List 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 fieldItems) {
Iterator 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 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 getCurrentFieldTMs() {
List fieldItems = this.getFieldItems();
List 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 fieldTMs) {
List fieldItems = this.getFieldItems();
int fiLength = fieldItems.size();
Iterator 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 getRegularFieldItems() {
List fieldItems = this.getFieldItems();
List 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 items) {
List fieldItems = this.getFieldItems();
int fiLength = fieldItems.size();
Iterator 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 getIngameTrades() {
List 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 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 trades) {
int tradeOffset = 0;
List 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 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 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 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 strings = this.getStrings(entry);
for (int strNum = 0; strNum < strings.size(); strNum++) {
String oldString = strings.get(strNum);
boolean needsReplacement = false;
for (Map.Entry 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 pokemonIncluded = this.mainPokemonList;
Set keepEvos = new HashSet<>();
for (Pokemon pk : pokes) {
if (pk != null) {
keepEvos.clear();
for (Evolution evol : pk.evolutionsFrom) {
if (pokemonIncluded.contains(evol.from) && pokemonIncluded.contains(evol.to)) {
keepEvos.add(evol);
} else {
evol.to.evolutionsTo.remove(evol);
}
}
pk.evolutionsFrom.retainAll(keepEvos);
}
}
try {
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 getFieldMoves() {
if (romEntry.romType == Gen4Constants.Type_HGSS) {
return Gen4Constants.hgssFieldMoves;
} else {
return Gen4Constants.dpptFieldMoves;
}
}
@Override
public List 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();
}
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();
}
}
@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 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 readTypeEffectivenessTable(byte[] battleOverlay, int typeEffectivenessTableOffset) {
List 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 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);
}
@Override
public void applyCorrectStaticMusic(Map specialMusicStaticChanges) {
List 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 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) {
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 getAllConsumableHeldItems() {
return Gen4Constants.consumableHeldItems;
}
@Override
public List getAllHeldItems() {
return Gen4Constants.allHeldItems;
}
@Override
public List getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List moves, int[] pokeMoves) {
List 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 byType = Effectiveness.against(tp.pokemon.primaryType, tp.pokemon.secondaryType, 4, effectivenessUpdated);
for(Map.Entry 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 speciesItems = Gen4Constants.speciesBoostingItems.get(tp.pokemon.number);
if (speciesItems != null) {
for (int i = 0; i < frequencyBoostCount; i++) {
items.addAll(speciesItems);
}
}
}
return items;
}
}