package com.sneed.pkrandom.romhandlers; /*----------------------------------------------------------------------------*/ /*-- Gen1RomHandler.java - randomizer handler for R/B/Y. --*/ /*-- --*/ /*-- 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.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Scanner; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.sneed.pkrandom.*; import com.sneed.pkrandom.constants.*; import com.sneed.pkrandom.exceptions.RandomizationException; import com.sneed.pkrandom.exceptions.RandomizerIOException; import com.sneed.pkrandom.pokemon.*; import compressors.Gen1Decmp; public class Gen1RomHandler extends AbstractGBCRomHandler { public static class Factory extends RomHandler.Factory { @Override public Gen1RomHandler create(Random random, PrintStream logStream) { return new Gen1RomHandler(random, logStream); } public boolean isLoadable(String filename) { long fileLength = new File(filename).length(); if (fileLength > 8 * 1024 * 1024) { return false; } byte[] loaded = loadFilePartial(filename, 0x1000); // nope return loaded.length != 0 && detectRomInner(loaded, (int) fileLength); } } public Gen1RomHandler(Random random) { super(random, null); } public Gen1RomHandler(Random random, PrintStream logStream) { super(random, logStream); } // Important RBY Data Structures private int[] pokeNumToRBYTable; private int[] pokeRBYToNumTable; private int[] moveNumToRomTable; private int[] moveRomToNumTable; private int pokedexCount; private Type idToType(int value) { if (Gen1Constants.typeTable[value] != null) { return Gen1Constants.typeTable[value]; } if (romEntry.extraTypeLookup.containsKey(value)) { return romEntry.extraTypeLookup.get(value); } return null; } private byte typeToByte(Type type) { if (type == null) { return 0x00; // revert to normal } if (romEntry.extraTypeReverse.containsKey(type)) { return romEntry.extraTypeReverse.get(type).byteValue(); } return Gen1Constants.typeToByte(type); } private static class RomEntry { private String name; private String romName; private int version, nonJapanese; private String extraTableFile; private boolean isYellow; private long expectedCRC32 = -1; private int crcInHeader = -1; private Map tweakFiles = new HashMap<>(); private List tmTexts = new ArrayList<>(); private Map entries = new HashMap<>(); private Map arrayEntries = new HashMap<>(); private List staticPokemon = new ArrayList<>(); private int[] ghostMarowakOffsets = new int[0]; private Map extraTypeLookup = new HashMap<>(); private Map extraTypeReverse = new HashMap<>(); private int getValue(String key) { if (!entries.containsKey(key)) { entries.put(key, 0); } return entries.get(key); } } private static List roms; static { loadROMInfo(); } private static class TMTextEntry { private int number; private int offset; private String template; } private static void loadROMInfo() { roms = new ArrayList<>(); RomEntry current = null; try { Scanner sc = new Scanner(FileFunctions.openConfig("gen1_offsets.ini"), "UTF-8"); while (sc.hasNextLine()) { String q = sc.nextLine().trim(); if (q.contains("//")) { q = q.substring(0, q.indexOf("//")).trim(); } if (!q.isEmpty()) { if (q.startsWith("[") && q.endsWith("]")) { // New rom current = new RomEntry(); current.name = q.substring(1, q.length() - 1); roms.add(current); } else { String[] r = q.split("=", 2); if (r.length == 1) { System.err.println("invalid entry " + q); continue; } if (r[1].endsWith("\r\n")) { r[1] = r[1].substring(0, r[1].length() - 2); } r[1] = r[1].trim(); r[0] = r[0].trim(); // Static Pokemon? if (r[0].equals("StaticPokemon{}")) { current.staticPokemon.add(parseStaticPokemon(r[1])); } else if (r[0].equals("StaticPokemonGhostMarowak{}")) { StaticPokemon ghostMarowak = parseStaticPokemon(r[1]); current.staticPokemon.add(ghostMarowak); current.ghostMarowakOffsets = ghostMarowak.speciesOffsets; } else if (r[0].equals("TMText[]")) { if (r[1].startsWith("[") && r[1].endsWith("]")) { String[] parts = r[1].substring(1, r[1].length() - 1).split(",", 3); TMTextEntry tte = new TMTextEntry(); tte.number = parseRIInt(parts[0]); tte.offset = parseRIInt(parts[1]); tte.template = parts[2]; current.tmTexts.add(tte); } } else if (r[0].equals("Game")) { current.romName = r[1]; } else if (r[0].equals("Version")) { current.version = parseRIInt(r[1]); } else if (r[0].equals("NonJapanese")) { current.nonJapanese = parseRIInt(r[1]); } else if (r[0].equals("Type")) { current.isYellow = r[1].equalsIgnoreCase("Yellow"); } else if (r[0].equals("ExtraTableFile")) { current.extraTableFile = r[1]; } else if (r[0].equals("CRCInHeader")) { current.crcInHeader = parseRIInt(r[1]); } else if (r[0].equals("CRC32")) { current.expectedCRC32 = parseRILong("0x" + r[1]); } else if (r[0].endsWith("Tweak")) { current.tweakFiles.put(r[0], r[1]); } else if (r[0].equals("ExtraTypes")) { // remove the containers r[1] = r[1].substring(1, r[1].length() - 1); String[] parts = r[1].split(","); for (String part : parts) { String[] iParts = part.split("="); int typeId = Integer.parseInt(iParts[0], 16); String typeName = iParts[1].trim(); Type theType = Type.valueOf(typeName); current.extraTypeLookup.put(typeId, theType); current.extraTypeReverse.put(theType, typeId); } } else if (r[0].equals("CopyFrom")) { for (RomEntry otherEntry : roms) { if (r[1].equalsIgnoreCase(otherEntry.name)) { // copy from here boolean cSP = (current.getValue("CopyStaticPokemon") == 1); boolean cTT = (current.getValue("CopyTMText") == 1); current.arrayEntries.putAll(otherEntry.arrayEntries); current.entries.putAll(otherEntry.entries); if (cSP) { current.staticPokemon.addAll(otherEntry.staticPokemon); current.ghostMarowakOffsets = otherEntry.ghostMarowakOffsets; current.entries.put("StaticPokemonSupport", 1); } else { current.entries.put("StaticPokemonSupport", 0); } if (cTT) { current.tmTexts.addAll(otherEntry.tmTexts); } current.extraTableFile = otherEntry.extraTableFile; } } } else { if (r[1].startsWith("[") && r[1].endsWith("]")) { String[] offsets = r[1].substring(1, r[1].length() - 1).split(","); if (offsets.length == 1 && offsets[0].trim().isEmpty()) { current.arrayEntries.put(r[0], new int[0]); } else { int[] offs = new int[offsets.length]; int c = 0; for (String off : offsets) { offs[c++] = parseRIInt(off); } current.arrayEntries.put(r[0], offs); } } else { int offs = parseRIInt(r[1]); current.entries.put(r[0], offs); } } } } } sc.close(); } catch (FileNotFoundException e) { System.err.println("File not found!"); } } private static StaticPokemon parseStaticPokemon(String staticPokemonString) { StaticPokemon sp = new StaticPokemon(); String pattern = "[A-z]+=\\[(0x[0-9a-fA-F]+,?\\s?)+]"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(staticPokemonString); while (m.find()) { String[] segments = m.group().split("="); String[] romOffsets = segments[1].substring(1, segments[1].length() - 1).split(","); int[] offsets = new int [romOffsets.length]; for (int i = 0; i < offsets.length; i++) { offsets[i] = parseRIInt(romOffsets[i]); } switch (segments[0]) { case "Species": sp.speciesOffsets = offsets; break; case "Level": sp.levelOffsets = offsets; break; } } return sp; } private static int parseRIInt(String off) { int radix = 10; off = off.trim().toLowerCase(); if (off.startsWith("0x") || off.startsWith("&h")) { radix = 16; off = off.substring(2); } try { return Integer.parseInt(off, radix); } catch (NumberFormatException ex) { System.err.println("invalid base " + radix + "number " + off); return 0; } } private static long parseRILong(String off) { int radix = 10; off = off.trim().toLowerCase(); if (off.startsWith("0x") || off.startsWith("&h")) { radix = 16; off = off.substring(2); } try { return Long.parseLong(off, radix); } catch (NumberFormatException ex) { System.err.println("invalid base " + radix + "number " + off); return 0; } } // This ROM's data private Pokemon[] pokes; private List pokemonList; private RomEntry romEntry; private Move[] moves; private String[] itemNames; private String[] mapNames; private SubMap[] maps; private boolean xAccNerfed; private long actualCRC32; private boolean effectivenessUpdated; @Override public boolean detectRom(byte[] rom) { return detectRomInner(rom, rom.length); } public static boolean detectRomInner(byte[] rom, int romSize) { // size check return romSize >= GBConstants.minRomSize && romSize <= GBConstants.maxRomSize && checkRomEntry(rom) != null; } @Override public void loadedRom() { romEntry = checkRomEntry(this.rom); pokeNumToRBYTable = new int[256]; pokeRBYToNumTable = new int[256]; moveNumToRomTable = new int[256]; moveRomToNumTable = new int[256]; maps = new SubMap[256]; xAccNerfed = false; clearTextTables(); readTextTable("gameboy_jpn"); if (romEntry.extraTableFile != null && !romEntry.extraTableFile.equalsIgnoreCase("none")) { readTextTable(romEntry.extraTableFile); } loadPokedexOrder(); loadPokemonStats(); pokemonList = Arrays.asList(pokes); loadMoves(); loadItemNames(); preloadMaps(); loadMapNames(); actualCRC32 = FileFunctions.getCRC32(rom); } private void loadPokedexOrder() { int pkmnCount = romEntry.getValue("InternalPokemonCount"); int orderOffset = romEntry.getValue("PokedexOrder"); pokedexCount = 0; for (int i = 1; i <= pkmnCount; i++) { int pokedexNum = rom[orderOffset + i - 1] & 0xFF; pokeRBYToNumTable[i] = pokedexNum; if (pokedexNum != 0 && pokeNumToRBYTable[pokedexNum] == 0) { pokeNumToRBYTable[pokedexNum] = i; } pokedexCount = Math.max(pokedexCount, pokedexNum); } } private static RomEntry checkRomEntry(byte[] rom) { int version = rom[GBConstants.versionOffset] & 0xFF; int nonjap = rom[GBConstants.jpFlagOffset] & 0xFF; // Check for specific CRC first int crcInHeader = ((rom[GBConstants.crcOffset] & 0xFF) << 8) | (rom[GBConstants.crcOffset + 1] & 0xFF); for (RomEntry re : roms) { if (romSig(rom, re.romName) && re.version == version && re.nonJapanese == nonjap && re.crcInHeader == crcInHeader) { return re; } } // Now check for non-specific-CRC entries for (RomEntry re : roms) { if (romSig(rom, re.romName) && re.version == version && re.nonJapanese == nonjap && re.crcInHeader == -1) { return re; } } // Not found return null; } @Override public void savingRom() { savePokemonStats(); saveMoves(); } private String[] readMoveNames() { int moveCount = romEntry.getValue("MoveCount"); int offset = romEntry.getValue("MoveNamesOffset"); String[] moveNames = new String[moveCount + 1]; for (int i = 1; i <= moveCount; i++) { moveNames[i] = readVariableLengthString(offset, false); offset += lengthOfStringAt(offset, false) + 1; } return moveNames; } private void loadMoves() { String[] moveNames = readMoveNames(); int moveCount = romEntry.getValue("MoveCount"); int movesOffset = romEntry.getValue("MoveDataOffset"); // check real move count int trueMoveCount = 0; for (int i = 1; i <= moveCount; i++) { // temp hack for Brown if (rom[movesOffset + (i - 1) * 6] != 0 && !moveNames[i].equals("Nothing")) { trueMoveCount++; } } moves = new Move[trueMoveCount + 1]; int trueMoveIndex = 0; for (int i = 1; i <= moveCount; i++) { int anim = rom[movesOffset + (i - 1) * 6] & 0xFF; // another temp hack for brown if (anim > 0 && !moveNames[i].equals("Nothing")) { trueMoveIndex++; moveNumToRomTable[trueMoveIndex] = i; moveRomToNumTable[i] = trueMoveIndex; moves[trueMoveIndex] = new Move(); moves[trueMoveIndex].name = moveNames[i]; moves[trueMoveIndex].internalId = i; moves[trueMoveIndex].number = trueMoveIndex; moves[trueMoveIndex].effectIndex = rom[movesOffset + (i - 1) * 6 + 1] & 0xFF; moves[trueMoveIndex].hitratio = ((rom[movesOffset + (i - 1) * 6 + 4] & 0xFF)) / 255.0 * 100; moves[trueMoveIndex].power = rom[movesOffset + (i - 1) * 6 + 2] & 0xFF; moves[trueMoveIndex].pp = rom[movesOffset + (i - 1) * 6 + 5] & 0xFF; moves[trueMoveIndex].type = idToType(rom[movesOffset + (i - 1) * 6 + 3] & 0xFF); moves[trueMoveIndex].category = GBConstants.physicalTypes.contains(moves[trueMoveIndex].type) ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL; if (moves[trueMoveIndex].power == 0 && !GlobalConstants.noPowerNonStatusMoves.contains(trueMoveIndex)) { moves[trueMoveIndex].category = MoveCategory.STATUS; } if (moves[trueMoveIndex].name.equals("Swift")) { perfectAccuracy = (int)moves[trueMoveIndex].hitratio; } if (GlobalConstants.normalMultihitMoves.contains(i)) { moves[trueMoveIndex].hitCount = 3; } else if (GlobalConstants.doubleHitMoves.contains(i)) { moves[trueMoveIndex].hitCount = 2; } loadStatChangesFromEffect(moves[trueMoveIndex]); loadStatusFromEffect(moves[trueMoveIndex]); loadMiscMoveInfoFromEffect(moves[trueMoveIndex]); } } } private void loadStatChangesFromEffect(Move move) { switch (move.effectIndex) { case Gen1Constants.noDamageAtkPlusOneEffect: move.statChanges[0].type = StatChangeType.ATTACK; move.statChanges[0].stages = 1; break; case Gen1Constants.noDamageDefPlusOneEffect: move.statChanges[0].type = StatChangeType.DEFENSE; move.statChanges[0].stages = 1; break; case Gen1Constants.noDamageSpecialPlusOneEffect: move.statChanges[0].type = StatChangeType.SPECIAL; move.statChanges[0].stages = 1; break; case Gen1Constants.noDamageEvasionPlusOneEffect: move.statChanges[0].type = StatChangeType.EVASION; move.statChanges[0].stages = 1; break; case Gen1Constants.noDamageAtkMinusOneEffect: case Gen1Constants.damageAtkMinusOneEffect: move.statChanges[0].type = StatChangeType.ATTACK; move.statChanges[0].stages = -1; break; case Gen1Constants.noDamageDefMinusOneEffect: case Gen1Constants.damageDefMinusOneEffect: move.statChanges[0].type = StatChangeType.DEFENSE; move.statChanges[0].stages = -1; break; case Gen1Constants.noDamageSpeMinusOneEffect: case Gen1Constants.damageSpeMinusOneEffect: move.statChanges[0].type = StatChangeType.SPEED; move.statChanges[0].stages = -1; break; case Gen1Constants.noDamageAccuracyMinusOneEffect: move.statChanges[0].type = StatChangeType.ACCURACY; move.statChanges[0].stages = -1; break; case Gen1Constants.noDamageAtkPlusTwoEffect: move.statChanges[0].type = StatChangeType.ATTACK; move.statChanges[0].stages = 2; break; case Gen1Constants.noDamageDefPlusTwoEffect: move.statChanges[0].type = StatChangeType.DEFENSE; move.statChanges[0].stages = 2; break; case Gen1Constants.noDamageSpePlusTwoEffect: move.statChanges[0].type = StatChangeType.SPEED; move.statChanges[0].stages = 2; break; case Gen1Constants.noDamageSpecialPlusTwoEffect: move.statChanges[0].type = StatChangeType.SPECIAL; move.statChanges[0].stages = 2; break; case Gen1Constants.noDamageDefMinusTwoEffect: move.statChanges[0].type = StatChangeType.DEFENSE; move.statChanges[0].stages = -2; break; case Gen1Constants.damageSpecialMinusOneEffect: move.statChanges[0].type = StatChangeType.SPECIAL; move.statChanges[0].stages = -1; break; default: // Move does not have a stat-changing effect return; } switch (move.effectIndex) { case Gen1Constants.noDamageAtkPlusOneEffect: case Gen1Constants.noDamageDefPlusOneEffect: case Gen1Constants.noDamageSpecialPlusOneEffect: case Gen1Constants.noDamageEvasionPlusOneEffect: case Gen1Constants.noDamageAtkMinusOneEffect: case Gen1Constants.noDamageDefMinusOneEffect: case Gen1Constants.noDamageSpeMinusOneEffect: case Gen1Constants.noDamageAccuracyMinusOneEffect: case Gen1Constants.noDamageAtkPlusTwoEffect: case Gen1Constants.noDamageDefPlusTwoEffect: case Gen1Constants.noDamageSpePlusTwoEffect: case Gen1Constants.noDamageSpecialPlusTwoEffect: case Gen1Constants.noDamageDefMinusTwoEffect: if (move.statChanges[0].stages < 0) { move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET; } else { move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER; } break; case Gen1Constants.damageAtkMinusOneEffect: case Gen1Constants.damageDefMinusOneEffect: case Gen1Constants.damageSpeMinusOneEffect: case Gen1Constants.damageSpecialMinusOneEffect: move.statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET; break; } if (move.statChangeMoveType == StatChangeMoveType.DAMAGE_TARGET) { for (int i = 0; i < move.statChanges.length; i++) { if (move.statChanges[i].type != StatChangeType.NONE) { move.statChanges[i].percentChance = 85 / 256.0; } } } } private void loadStatusFromEffect(Move move) { switch (move.effectIndex) { case Gen1Constants.noDamageSleepEffect: case Gen1Constants.noDamageConfusionEffect: case Gen1Constants.noDamagePoisonEffect: case Gen1Constants.noDamageParalyzeEffect: move.statusMoveType = StatusMoveType.NO_DAMAGE; break; case Gen1Constants.damagePoison20PercentEffect: case Gen1Constants.damageBurn10PercentEffect: case Gen1Constants.damageFreeze10PercentEffect: case Gen1Constants.damageParalyze10PercentEffect: case Gen1Constants.damagePoison40PercentEffect: case Gen1Constants.damageBurn30PercentEffect: case Gen1Constants.damageFreeze30PercentEffect: case Gen1Constants.damageParalyze30PercentEffect: case Gen1Constants.damageConfusionEffect: case Gen1Constants.twineedleEffect: move.statusMoveType = StatusMoveType.DAMAGE; break; default: // Move does not have a status effect return; } switch (move.effectIndex) { case Gen1Constants.noDamageSleepEffect: move.statusType = StatusType.SLEEP; break; case Gen1Constants.damagePoison20PercentEffect: case Gen1Constants.damagePoison40PercentEffect: case Gen1Constants.noDamagePoisonEffect: case Gen1Constants.twineedleEffect: move.statusType = StatusType.POISON; if (move.number == Moves.toxic) { move.statusType = StatusType.TOXIC_POISON; } break; case Gen1Constants.damageBurn10PercentEffect: case Gen1Constants.damageBurn30PercentEffect: move.statusType = StatusType.BURN; break; case Gen1Constants.damageFreeze10PercentEffect: case Gen1Constants.damageFreeze30PercentEffect: move.statusType = StatusType.FREEZE; break; case Gen1Constants.damageParalyze10PercentEffect: case Gen1Constants.damageParalyze30PercentEffect: case Gen1Constants.noDamageParalyzeEffect: move.statusType = StatusType.PARALYZE; break; case Gen1Constants.noDamageConfusionEffect: case Gen1Constants.damageConfusionEffect: move.statusType = StatusType.CONFUSION; break; } if (move.statusMoveType == StatusMoveType.DAMAGE) { switch (move.effectIndex) { case Gen1Constants.damageBurn10PercentEffect: case Gen1Constants.damageFreeze10PercentEffect: case Gen1Constants.damageParalyze10PercentEffect: case Gen1Constants.damageConfusionEffect: move.statusPercentChance = 10.0; break; case Gen1Constants.damagePoison20PercentEffect: case Gen1Constants.twineedleEffect: move.statusPercentChance = 20.0; break; case Gen1Constants.damageBurn30PercentEffect: case Gen1Constants.damageFreeze30PercentEffect: case Gen1Constants.damageParalyze30PercentEffect: move.statusPercentChance = 30.0; break; case Gen1Constants.damagePoison40PercentEffect: move.statusPercentChance = 40.0; break; } } } private void loadMiscMoveInfoFromEffect(Move move) { switch (move.effectIndex) { case Gen1Constants.flinch10PercentEffect: move.flinchPercentChance = 10.0; break; case Gen1Constants.flinch30PercentEffect: move.flinchPercentChance = 30.0; break; case Gen1Constants.damageAbsorbEffect: case Gen1Constants.dreamEaterEffect: move.absorbPercent = 50; break; case Gen1Constants.damageRecoilEffect: move.recoilPercent = 25; break; case Gen1Constants.chargeEffect: case Gen1Constants.flyEffect: move.isChargeMove = true; break; case Gen1Constants.hyperBeamEffect: move.isRechargeMove = true; break; } if (Gen1Constants.increasedCritMoves.contains(move.number)) { move.criticalChance = CriticalChance.INCREASED; } } private void saveMoves() { int movesOffset = romEntry.getValue("MoveDataOffset"); for (Move m : moves) { if (m != null) { int i = m.internalId; rom[movesOffset + (i - 1) * 6 + 1] = (byte) m.effectIndex; rom[movesOffset + (i - 1) * 6 + 2] = (byte) m.power; rom[movesOffset + (i - 1) * 6 + 3] = typeToByte(m.type); int hitratio = (int) Math.round(m.hitratio * 2.55); if (hitratio < 0) { hitratio = 0; } if (hitratio > 255) { hitratio = 255; } rom[movesOffset + (i - 1) * 6 + 4] = (byte) hitratio; rom[movesOffset + (i - 1) * 6 + 5] = (byte) m.pp; } } } public List getMoves() { return Arrays.asList(moves); } private void loadPokemonStats() { pokes = new Gen1Pokemon[pokedexCount + 1]; // Fetch our names String[] pokeNames = readPokemonNames(); // Get base stats int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset"); for (int i = 1; i <= pokedexCount; i++) { pokes[i] = new Gen1Pokemon(); pokes[i].number = i; if (i != Species.mew || romEntry.isYellow) { loadBasicPokeStats(pokes[i], pokeStatsOffset + (i - 1) * Gen1Constants.baseStatsEntrySize); } // Name? pokes[i].name = pokeNames[pokeNumToRBYTable[i]]; } // Mew override for R/B if (!romEntry.isYellow) { loadBasicPokeStats(pokes[Species.mew], romEntry.getValue("MewStatsOffset")); } // Evolutions populateEvolutions(); } private void savePokemonStats() { // Write pokemon names int offs = romEntry.getValue("PokemonNamesOffset"); int nameLength = romEntry.getValue("PokemonNamesLength"); for (int i = 1; i <= pokedexCount; i++) { int rbynum = pokeNumToRBYTable[i]; int stringOffset = offs + (rbynum - 1) * nameLength; writeFixedLengthString(pokes[i].name, stringOffset, nameLength); } // Write pokemon stats int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset"); for (int i = 1; i <= pokedexCount; i++) { if (i == Species.mew) { continue; } saveBasicPokeStats(pokes[i], pokeStatsOffset + (i - 1) * Gen1Constants.baseStatsEntrySize); } // Write MEW int mewOffset = romEntry.isYellow ? pokeStatsOffset + (Species.mew - 1) * Gen1Constants.baseStatsEntrySize : romEntry.getValue("MewStatsOffset"); saveBasicPokeStats(pokes[Species.mew], mewOffset); // Write evolutions writeEvosAndMovesLearnt(true, null); } private void loadBasicPokeStats(Pokemon pkmn, int offset) { pkmn.hp = rom[offset + Gen1Constants.bsHPOffset] & 0xFF; pkmn.attack = rom[offset + Gen1Constants.bsAttackOffset] & 0xFF; pkmn.defense = rom[offset + Gen1Constants.bsDefenseOffset] & 0xFF; pkmn.speed = rom[offset + Gen1Constants.bsSpeedOffset] & 0xFF; pkmn.special = rom[offset + Gen1Constants.bsSpecialOffset] & 0xFF; // Type pkmn.primaryType = idToType(rom[offset + Gen1Constants.bsPrimaryTypeOffset] & 0xFF); pkmn.secondaryType = idToType(rom[offset + Gen1Constants.bsSecondaryTypeOffset] & 0xFF); // Only one type? if (pkmn.secondaryType == pkmn.primaryType) { pkmn.secondaryType = null; } pkmn.catchRate = rom[offset + Gen1Constants.bsCatchRateOffset] & 0xFF; pkmn.expYield = rom[offset + Gen1Constants.bsExpYieldOffset] & 0xFF; pkmn.growthCurve = ExpCurve.fromByte(rom[offset + Gen1Constants.bsGrowthCurveOffset]); pkmn.frontSpritePointer = readWord(offset + Gen1Constants.bsFrontSpriteOffset); pkmn.guaranteedHeldItem = -1; pkmn.commonHeldItem = -1; pkmn.rareHeldItem = -1; pkmn.darkGrassHeldItem = -1; } private void saveBasicPokeStats(Pokemon pkmn, int offset) { rom[offset + Gen1Constants.bsHPOffset] = (byte) pkmn.hp; rom[offset + Gen1Constants.bsAttackOffset] = (byte) pkmn.attack; rom[offset + Gen1Constants.bsDefenseOffset] = (byte) pkmn.defense; rom[offset + Gen1Constants.bsSpeedOffset] = (byte) pkmn.speed; rom[offset + Gen1Constants.bsSpecialOffset] = (byte) pkmn.special; rom[offset + Gen1Constants.bsPrimaryTypeOffset] = typeToByte(pkmn.primaryType); if (pkmn.secondaryType == null) { rom[offset + Gen1Constants.bsSecondaryTypeOffset] = rom[offset + Gen1Constants.bsPrimaryTypeOffset]; } else { rom[offset + Gen1Constants.bsSecondaryTypeOffset] = typeToByte(pkmn.secondaryType); } rom[offset + Gen1Constants.bsCatchRateOffset] = (byte) pkmn.catchRate; rom[offset + Gen1Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte(); rom[offset + Gen1Constants.bsExpYieldOffset] = (byte) pkmn.expYield; } private String[] readPokemonNames() { int offs = romEntry.getValue("PokemonNamesOffset"); int nameLength = romEntry.getValue("PokemonNamesLength"); int pkmnCount = romEntry.getValue("InternalPokemonCount"); String[] names = new String[pkmnCount + 1]; for (int i = 1; i <= pkmnCount; i++) { names[i] = readFixedLengthString(offs + (i - 1) * nameLength, nameLength); } return names; } @Override public List getStarters() { // Get the starters List starters = new ArrayList<>(); starters.add(pokes[pokeRBYToNumTable[rom[romEntry.arrayEntries.get("StarterOffsets1")[0]] & 0xFF]]); starters.add(pokes[pokeRBYToNumTable[rom[romEntry.arrayEntries.get("StarterOffsets2")[0]] & 0xFF]]); if (!romEntry.isYellow) { starters.add(pokes[pokeRBYToNumTable[rom[romEntry.arrayEntries.get("StarterOffsets3")[0]] & 0xFF]]); } return starters; } @Override public boolean setStarters(List newStarters) { // Amount? int starterAmount = 2; if (!romEntry.isYellow) { starterAmount = 3; } // Basic checks if (newStarters.size() != starterAmount) { return false; } // Patch starter bytes for (int i = 0; i < starterAmount; i++) { byte starter = (byte) pokeNumToRBYTable[newStarters.get(i).number]; int[] offsets = romEntry.arrayEntries.get("StarterOffsets" + (i + 1)); for (int offset : offsets) { rom[offset] = starter; } } // Special stuff for non-Yellow only if (!romEntry.isYellow) { // Starter text if (romEntry.getValue("CanChangeStarterText") > 0) { int[] starterTextOffsets = romEntry.arrayEntries.get("StarterTextOffsets"); for (int i = 0; i < 3 && i < starterTextOffsets.length; i++) { writeVariableLengthString(String.format("So! You want\\n%s?\\e", newStarters.get(i).name), starterTextOffsets[i], true); } } // Patch starter pokedex routine? // Can only do in 1M roms because of size concerns if (romEntry.getValue("PatchPokedex") > 0) { // Starter pokedex required RAM values // RAM offset => value // Allows for multiple starters in the same RAM byte Map onValues = new TreeMap<>(); for (int i = 0; i < 3; i++) { int pkDexNum = newStarters.get(i).number; int ramOffset = (pkDexNum - 1) / 8 + romEntry.getValue("PokedexRamOffset"); int bitShift = (pkDexNum - 1) % 8; int writeValue = 1 << bitShift; if (onValues.containsKey(ramOffset)) { onValues.put(ramOffset, onValues.get(ramOffset) | writeValue); } else { onValues.put(ramOffset, writeValue); } } // Starter pokedex offset/pointer calculations int pkDexOnOffset = romEntry.getValue("StarterPokedexOnOffset"); int pkDexOffOffset = romEntry.getValue("StarterPokedexOffOffset"); int sizeForOnRoutine = 5 * onValues.size() + 3; int writeOnRoutineTo = romEntry.getValue("StarterPokedexBranchOffset"); int writeOffRoutineTo = writeOnRoutineTo + sizeForOnRoutine; int offsetForOnRoutine = makeGBPointer(writeOnRoutineTo); int offsetForOffRoutine = makeGBPointer(writeOffRoutineTo); int retOnOffset = makeGBPointer(pkDexOnOffset + 5); int retOffOffset = makeGBPointer(pkDexOffOffset + 4); // Starter pokedex // Branch to our new routine(s) // Turn bytes on rom[pkDexOnOffset] = GBConstants.gbZ80Jump; writeWord(pkDexOnOffset + 1, offsetForOnRoutine); rom[pkDexOnOffset + 3] = GBConstants.gbZ80Nop; rom[pkDexOnOffset + 4] = GBConstants.gbZ80Nop; // Turn bytes off rom[pkDexOffOffset] = GBConstants.gbZ80Jump; writeWord(pkDexOffOffset + 1, offsetForOffRoutine); rom[pkDexOffOffset + 3] = GBConstants.gbZ80Nop; // Put together the two scripts rom[writeOffRoutineTo] = GBConstants.gbZ80XorA; int turnOnOffset = writeOnRoutineTo; int turnOffOffset = writeOffRoutineTo + 1; for (int ramOffset : onValues.keySet()) { int onValue = onValues.get(ramOffset); // Turn on code rom[turnOnOffset++] = GBConstants.gbZ80LdA; rom[turnOnOffset++] = (byte) onValue; // Turn on code for ram writing rom[turnOnOffset++] = GBConstants.gbZ80LdAToFar; rom[turnOnOffset++] = (byte) (ramOffset % 0x100); rom[turnOnOffset++] = (byte) (ramOffset / 0x100); // Turn off code for ram writing rom[turnOffOffset++] = GBConstants.gbZ80LdAToFar; rom[turnOffOffset++] = (byte) (ramOffset % 0x100); rom[turnOffOffset++] = (byte) (ramOffset / 0x100); } // Jump back rom[turnOnOffset++] = GBConstants.gbZ80Jump; writeWord(turnOnOffset, retOnOffset); rom[turnOffOffset++] = GBConstants.gbZ80Jump; writeWord(turnOffOffset, retOffOffset); } } // If we're changing the player's starter for Yellow, then the player can't get the // Bulbasaur gift unless they randomly stumble into a Pikachu somewhere else. This is // because you need a certain amount of Pikachu happiness to acquire this gift, and // happiness only accumulates if you have a Pikachu. Instead, just patch out this check. if (romEntry.entries.containsKey("PikachuHappinessCheckOffset") && newStarters.get(0).number != Species.pikachu) { int offset = romEntry.getValue("PikachuHappinessCheckOffset"); // The code looks like this: // ld a, [wPikachuHappiness] // cp 147 // jr c, .asm_1cfb3 <- this is where "offset" is // Write two nops to patch out the jump rom[offset] = GBConstants.gbZ80Nop; rom[offset + 1] = GBConstants.gbZ80Nop; } return true; } @Override public boolean hasStarterAltFormes() { return false; } @Override public int starterCount() { return isYellow() ? 2 : 3; } @Override public Map getUpdatedPokemonStats(int generation) { Map map = GlobalConstants.getStatChanges(generation); switch(generation) { case 6: map.put(12,new StatChange(Stat.SPECIAL.val,90)); map.put(36,new StatChange(Stat.SPECIAL.val,95)); map.put(45,new StatChange(Stat.SPECIAL.val,110)); break; default: break; } return map; } @Override public boolean supportsStarterHeldItems() { // No held items in Gen 1 return false; } @Override public List getStarterHeldItems() { // do nothing return new ArrayList<>(); } @Override public void setStarterHeldItems(List items) { // do nothing } @Override public List getEvolutionItems() { return null; } @Override public List getEncounters(boolean useTimeOfDay) { List encounters = new ArrayList<>(); Pokemon ghostMarowak = pokes[Species.marowak]; if (canChangeStaticPokemon()) { ghostMarowak = pokes[pokeRBYToNumTable[rom[romEntry.ghostMarowakOffsets[0]] & 0xFF]]; } // grass & water List usedOffsets = new ArrayList<>(); int tableOffset = romEntry.getValue("WildPokemonTableOffset"); int tableBank = bankOf(tableOffset); int mapID = -1; while (readWord(tableOffset) != Gen1Constants.encounterTableEnd) { mapID++; int offset = calculateOffset(tableBank, readWord(tableOffset)); int rootOffset = offset; if (!usedOffsets.contains(offset)) { usedOffsets.add(offset); // grass and water are exactly the same for (int a = 0; a < 2; a++) { int rate = rom[offset++] & 0xFF; if (rate > 0) { // there is data here EncounterSet thisSet = new EncounterSet(); thisSet.rate = rate; thisSet.offset = rootOffset; thisSet.displayName = (a == 1 ? "Surfing" : "Grass/Cave") + " on " + mapNames[mapID]; if (mapID >= Gen1Constants.towerMapsStartIndex && mapID <= Gen1Constants.towerMapsEndIndex) { thisSet.bannedPokemon.add(ghostMarowak); } for (int slot = 0; slot < Gen1Constants.encounterTableSize; slot++) { Encounter enc = new Encounter(); enc.level = rom[offset] & 0xFF; enc.pokemon = pokes[pokeRBYToNumTable[rom[offset + 1] & 0xFF]]; thisSet.encounters.add(enc); offset += 2; } encounters.add(thisSet); } } } else { for (EncounterSet es : encounters) { if (es.offset == offset) { es.displayName += ", " + mapNames[mapID]; } } } tableOffset += 2; } // old rod int oldRodOffset = romEntry.getValue("OldRodOffset"); EncounterSet oldRodSet = new EncounterSet(); oldRodSet.displayName = "Old Rod Fishing"; Encounter oldRodEnc = new Encounter(); oldRodEnc.level = rom[oldRodOffset + 2] & 0xFF; oldRodEnc.pokemon = pokes[pokeRBYToNumTable[rom[oldRodOffset + 1] & 0xFF]]; oldRodSet.encounters.add(oldRodEnc); oldRodSet.bannedPokemon.add(ghostMarowak); encounters.add(oldRodSet); // good rod int goodRodOffset = romEntry.getValue("GoodRodOffset"); EncounterSet goodRodSet = new EncounterSet(); goodRodSet.displayName = "Good Rod Fishing"; for (int grSlot = 0; grSlot < 2; grSlot++) { Encounter enc = new Encounter(); enc.level = rom[goodRodOffset + grSlot * 2] & 0xFF; enc.pokemon = pokes[pokeRBYToNumTable[rom[goodRodOffset + grSlot * 2 + 1] & 0xFF]]; goodRodSet.encounters.add(enc); } goodRodSet.bannedPokemon.add(ghostMarowak); encounters.add(goodRodSet); // super rod if (romEntry.isYellow) { int superRodOffset = romEntry.getValue("SuperRodTableOffset"); while ((rom[superRodOffset] & 0xFF) != 0xFF) { int map = rom[superRodOffset++] & 0xFF; EncounterSet thisSet = new EncounterSet(); thisSet.displayName = "Super Rod Fishing on " + mapNames[map]; for (int encN = 0; encN < Gen1Constants.yellowSuperRodTableSize; encN++) { Encounter enc = new Encounter(); enc.level = rom[superRodOffset + 1] & 0xFF; enc.pokemon = pokes[pokeRBYToNumTable[rom[superRodOffset] & 0xFF]]; thisSet.encounters.add(enc); superRodOffset += 2; } thisSet.bannedPokemon.add(ghostMarowak); encounters.add(thisSet); } } else { // red/blue int superRodOffset = romEntry.getValue("SuperRodTableOffset"); int superRodBank = bankOf(superRodOffset); List usedSROffsets = new ArrayList<>(); while ((rom[superRodOffset] & 0xFF) != 0xFF) { int map = rom[superRodOffset++] & 0xFF; int setOffset = calculateOffset(superRodBank, readWord(superRodOffset)); superRodOffset += 2; if (!usedSROffsets.contains(setOffset)) { usedSROffsets.add(setOffset); EncounterSet thisSet = new EncounterSet(); thisSet.displayName = "Super Rod Fishing on " + mapNames[map]; thisSet.offset = setOffset; int pokesInSet = rom[setOffset++] & 0xFF; for (int encN = 0; encN < pokesInSet; encN++) { Encounter enc = new Encounter(); enc.level = rom[setOffset] & 0xFF; enc.pokemon = pokes[pokeRBYToNumTable[rom[setOffset + 1] & 0xFF]]; thisSet.encounters.add(enc); setOffset += 2; } thisSet.bannedPokemon.add(ghostMarowak); encounters.add(thisSet); } else { for (EncounterSet es : encounters) { if (es.offset == setOffset) { es.displayName += ", " + mapNames[map]; } } } } } return encounters; } @Override public void setEncounters(boolean useTimeOfDay, List encounters) { Iterator encsetit = encounters.iterator(); // grass & water List usedOffsets = new ArrayList<>(); int tableOffset = romEntry.getValue("WildPokemonTableOffset"); int tableBank = bankOf(tableOffset); while (readWord(tableOffset) != Gen1Constants.encounterTableEnd) { int offset = calculateOffset(tableBank, readWord(tableOffset)); if (!usedOffsets.contains(offset)) { usedOffsets.add(offset); // grass and water are exactly the same for (int a = 0; a < 2; a++) { int rate = rom[offset++] & 0xFF; if (rate > 0) { // there is data here EncounterSet thisSet = encsetit.next(); for (int slot = 0; slot < Gen1Constants.encounterTableSize; slot++) { Encounter enc = thisSet.encounters.get(slot); rom[offset] = (byte) enc.level; rom[offset + 1] = (byte) pokeNumToRBYTable[enc.pokemon.number]; offset += 2; } } } } tableOffset += 2; } // old rod int oldRodOffset = romEntry.getValue("OldRodOffset"); EncounterSet oldRodSet = encsetit.next(); Encounter oldRodEnc = oldRodSet.encounters.get(0); rom[oldRodOffset + 2] = (byte) oldRodEnc.level; rom[oldRodOffset + 1] = (byte) pokeNumToRBYTable[oldRodEnc.pokemon.number]; // good rod int goodRodOffset = romEntry.getValue("GoodRodOffset"); EncounterSet goodRodSet = encsetit.next(); for (int grSlot = 0; grSlot < 2; grSlot++) { Encounter enc = goodRodSet.encounters.get(grSlot); rom[goodRodOffset + grSlot * 2] = (byte) enc.level; rom[goodRodOffset + grSlot * 2 + 1] = (byte) pokeNumToRBYTable[enc.pokemon.number]; } // super rod if (romEntry.isYellow) { int superRodOffset = romEntry.getValue("SuperRodTableOffset"); while ((rom[superRodOffset] & 0xFF) != 0xFF) { superRodOffset++; EncounterSet thisSet = encsetit.next(); for (int encN = 0; encN < Gen1Constants.yellowSuperRodTableSize; encN++) { Encounter enc = thisSet.encounters.get(encN); rom[superRodOffset + 1] = (byte) enc.level; rom[superRodOffset] = (byte) pokeNumToRBYTable[enc.pokemon.number]; superRodOffset += 2; } } } else { // red/blue int superRodOffset = romEntry.getValue("SuperRodTableOffset"); int superRodBank = bankOf(superRodOffset); List usedSROffsets = new ArrayList<>(); while ((rom[superRodOffset] & 0xFF) != 0xFF) { superRodOffset++; int setOffset = calculateOffset(superRodBank, readWord(superRodOffset)); superRodOffset += 2; if (!usedSROffsets.contains(setOffset)) { usedSROffsets.add(setOffset); int pokesInSet = rom[setOffset++] & 0xFF; EncounterSet thisSet = encsetit.next(); for (int encN = 0; encN < pokesInSet; encN++) { Encounter enc = thisSet.encounters.get(encN); rom[setOffset] = (byte) enc.level; rom[setOffset + 1] = (byte) pokeNumToRBYTable[enc.pokemon.number]; setOffset += 2; } } } } } @Override public boolean hasWildAltFormes() { return false; } @Override public List getPokemon() { return pokemonList; } @Override public List getPokemonInclFormes() { return pokemonList; } @Override public List getAltFormes() { return new ArrayList<>(); } @Override public List getMegaEvolutions() { return new ArrayList<>(); } @Override public Pokemon getAltFormeOfPokemon(Pokemon pk, int forme) { return pk; } @Override public List getIrregularFormes() { return new ArrayList<>(); } @Override public boolean hasFunctionalFormes() { return false; } public List getTrainers() { int traineroffset = romEntry.getValue("TrainerDataTableOffset"); int traineramount = Gen1Constants.trainerClassCount; int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts"); int[] pointers = new int[traineramount + 1]; for (int i = 1; i <= traineramount; i++) { int tPointer = readWord(traineroffset + (i - 1) * 2); pointers[i] = calculateOffset(bankOf(traineroffset), tPointer); } List tcnames = getTrainerClassesForText(); List allTrainers = new ArrayList<>(); int index = 0; for (int i = 1; i <= traineramount; i++) { int offs = pointers[i]; int limit = trainerclasslimits[i]; String tcname = tcnames.get(i - 1); for (int trnum = 0; trnum < limit; trnum++) { index++; Trainer tr = new Trainer(); tr.offset = offs; tr.index = index; tr.trainerclass = i; tr.fullDisplayName = tcname; int dataType = rom[offs] & 0xFF; if (dataType == 0xFF) { // "Special" trainer tr.poketype = 1; offs++; while (rom[offs] != 0x0) { TrainerPokemon tpk = new TrainerPokemon(); tpk.level = rom[offs] & 0xFF; tpk.pokemon = pokes[pokeRBYToNumTable[rom[offs + 1] & 0xFF]]; tr.pokemon.add(tpk); offs += 2; } } else { tr.poketype = 0; offs++; while (rom[offs] != 0x0) { TrainerPokemon tpk = new TrainerPokemon(); tpk.level = dataType; tpk.pokemon = pokes[pokeRBYToNumTable[rom[offs] & 0xFF]]; tr.pokemon.add(tpk); offs++; } } offs++; allTrainers.add(tr); } } Gen1Constants.tagTrainersUniversal(allTrainers); if (romEntry.isYellow) { Gen1Constants.tagTrainersYellow(allTrainers); } else { Gen1Constants.tagTrainersRB(allTrainers); } return allTrainers; } @Override public List getMainPlaythroughTrainers() { return new ArrayList<>(); // Not implemented } @Override public List getEliteFourTrainers(boolean isChallengeMode) { return new ArrayList<>(); } public void setTrainers(List trainerData, boolean doubleBattleMode) { int traineroffset = romEntry.getValue("TrainerDataTableOffset"); int traineramount = Gen1Constants.trainerClassCount; int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts"); int[] pointers = new int[traineramount + 1]; for (int i = 1; i <= traineramount; i++) { int tPointer = readWord(traineroffset + (i - 1) * 2); pointers[i] = calculateOffset(bankOf(traineroffset), tPointer); } Iterator allTrainers = trainerData.iterator(); for (int i = 1; i <= traineramount; i++) { int offs = pointers[i]; int limit = trainerclasslimits[i]; for (int trnum = 0; trnum < limit; trnum++) { Trainer tr = allTrainers.next(); if (tr.trainerclass != i) { System.err.println("Trainer mismatch: " + tr.name); } Iterator tPokes = tr.pokemon.iterator(); // Write their pokemon based on poketype if (tr.poketype == 0) { // Regular trainer int fixedLevel = tr.pokemon.get(0).level; rom[offs] = (byte) fixedLevel; offs++; while (tPokes.hasNext()) { TrainerPokemon tpk = tPokes.next(); rom[offs] = (byte) pokeNumToRBYTable[tpk.pokemon.number]; offs++; } } else { // Special trainer rom[offs] = (byte) 0xFF; offs++; while (tPokes.hasNext()) { TrainerPokemon tpk = tPokes.next(); rom[offs] = (byte) tpk.level; rom[offs + 1] = (byte) pokeNumToRBYTable[tpk.pokemon.number]; offs += 2; } } rom[offs] = 0; offs++; } } // Custom Moves AI Table // Zero it out entirely. rom[romEntry.getValue("ExtraTrainerMovesTableOffset")] = (byte) 0xFF; // Champion Rival overrides in Red/Blue if (!isYellow()) { // hacky relative offset (very likely to work but maybe not always) int champRivalJump = romEntry.getValue("GymLeaderMovesTableOffset") - Gen1Constants.champRivalOffsetFromGymLeaderMoves; // nop out this jump rom[champRivalJump] = GBConstants.gbZ80Nop; rom[champRivalJump + 1] = GBConstants.gbZ80Nop; } } @Override public boolean hasRivalFinalBattle() { return true; } @Override public boolean isYellow() { return romEntry.isYellow; } @Override public boolean typeInGame(Type type) { if (!type.isHackOnly && (type != Type.DARK && type != Type.STEEL && type != Type.FAIRY)) { return true; } return romEntry.extraTypeReverse.containsKey(type); } @Override public List getMovesBannedFromLevelup() { return Gen1Constants.bannedLevelupMoves; } private void updateTypeEffectiveness() { List typeEffectivenessTable = readTypeEffectivenessTable(); log("--Updating Type Effectiveness--"); for (TypeRelationship relationship : typeEffectivenessTable) { // Change Poison 2x against bug (should be neutral) to Ice 0.5x against Fire (is currently neutral) if (relationship.attacker == Type.POISON && relationship.defender == Type.BUG) { relationship.attacker = Type.ICE; relationship.defender = Type.FIRE; relationship.effectiveness = Effectiveness.HALF; log("Replaced: Poison super effective vs Bug => Ice not very effective vs Fire"); } // Change Bug 2x against Poison to Bug 0.5x against Poison else if (relationship.attacker == Type.BUG && relationship.defender == Type.POISON) { relationship.effectiveness = Effectiveness.HALF; log("Changed: Bug super effective vs Poison => Bug not very effective vs Poison"); } // Change Ghost 0x against Psychic to Ghost 2x against Psychic else if (relationship.attacker == Type.GHOST && relationship.defender == Type.PSYCHIC) { relationship.effectiveness = Effectiveness.DOUBLE; log("Changed: Psychic immune to Ghost => Ghost super effective vs Psychic"); } } logBlankLine(); writeTypeEffectivenessTable(typeEffectivenessTable); effectivenessUpdated = true; } private List readTypeEffectivenessTable() { List typeEffectivenessTable = new ArrayList<>(); int currentOffset = romEntry.getValue("TypeEffectivenessOffset"); int attackingType = rom[currentOffset]; while (attackingType != (byte) 0xFF) { int defendingType = rom[currentOffset + 1]; int effectivenessInternal = rom[currentOffset + 2]; Type attacking = Gen1Constants.typeTable[attackingType]; Type defending = Gen1Constants.typeTable[defendingType]; Effectiveness effectiveness = null; switch (effectivenessInternal) { case 20: effectiveness = Effectiveness.DOUBLE; break; case 10: effectiveness = Effectiveness.NEUTRAL; break; case 5: effectiveness = Effectiveness.HALF; break; case 0: effectiveness = Effectiveness.ZERO; break; } if (effectiveness != null) { TypeRelationship relationship = new TypeRelationship(attacking, defending, effectiveness); typeEffectivenessTable.add(relationship); } currentOffset += 3; attackingType = rom[currentOffset]; } return typeEffectivenessTable; } private void writeTypeEffectivenessTable(List typeEffectivenessTable) { int currentOffset = romEntry.getValue("TypeEffectivenessOffset"); for (TypeRelationship relationship : typeEffectivenessTable) { rom[currentOffset] = Gen1Constants.typeToByte(relationship.attacker); rom[currentOffset + 1] = Gen1Constants.typeToByte(relationship.defender); byte effectivenessInternal = 0; switch (relationship.effectiveness) { case DOUBLE: effectivenessInternal = 20; break; case NEUTRAL: effectivenessInternal = 10; break; case HALF: effectivenessInternal = 5; break; case ZERO: effectivenessInternal = 0; break; } rom[currentOffset + 2] = effectivenessInternal; currentOffset += 3; } } @Override public Map> getMovesLearnt() { Map> movesets = new TreeMap<>(); int pointersOffset = romEntry.getValue("PokemonMovesetsTableOffset"); int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset"); int pkmnCount = romEntry.getValue("InternalPokemonCount"); for (int i = 1; i <= pkmnCount; i++) { int pointer = readWord(pointersOffset + (i - 1) * 2); int realPointer = calculateOffset(bankOf(pointersOffset), pointer); if (pokeRBYToNumTable[i] != 0) { Pokemon pkmn = pokes[pokeRBYToNumTable[i]]; int statsOffset; if (pokeRBYToNumTable[i] == Species.mew && !romEntry.isYellow) { // Mewww statsOffset = romEntry.getValue("MewStatsOffset"); } else { statsOffset = (pokeRBYToNumTable[i] - 1) * 0x1C + pokeStatsOffset; } List ourMoves = new ArrayList<>(); for (int delta = Gen1Constants.bsLevel1MovesOffset; delta < Gen1Constants.bsLevel1MovesOffset + 4; delta++) { if (rom[statsOffset + delta] != 0x00) { MoveLearnt learnt = new MoveLearnt(); learnt.level = 1; learnt.move = moveRomToNumTable[rom[statsOffset + delta] & 0xFF]; ourMoves.add(learnt); } } // Skip over evolution data while (rom[realPointer] != 0) { if (rom[realPointer] == 1) { realPointer += 3; } else if (rom[realPointer] == 2) { realPointer += 4; } else if (rom[realPointer] == 3) { realPointer += 3; } } realPointer++; while (rom[realPointer] != 0) { MoveLearnt learnt = new MoveLearnt(); learnt.level = rom[realPointer] & 0xFF; learnt.move = moveRomToNumTable[rom[realPointer + 1] & 0xFF]; ourMoves.add(learnt); realPointer += 2; } movesets.put(pkmn.number, ourMoves); } } return movesets; } @Override public void setMovesLearnt(Map> movesets) { // new method for moves learnt writeEvosAndMovesLearnt(false, movesets); } @Override public Map> getEggMoves() { // Gen 1 does not have egg moves return new TreeMap<>(); } @Override public void setEggMoves(Map> eggMoves) { // Gen 1 does not have egg moves } private static class StaticPokemon { protected int[] speciesOffsets; protected int[] levelOffsets; public StaticPokemon() { this.speciesOffsets = new int[0]; this.levelOffsets = new int[0]; } public Pokemon getPokemon(Gen1RomHandler rh) { return rh.pokes[rh.pokeRBYToNumTable[rh.rom[speciesOffsets[0]] & 0xFF]]; } public void setPokemon(Gen1RomHandler rh, Pokemon pkmn) { for (int offset : speciesOffsets) { rh.rom[offset] = (byte) rh.pokeNumToRBYTable[pkmn.number]; } } public int getLevel(byte[] rom, int i) { if (levelOffsets.length <= i) { return 1; } return rom[levelOffsets[i]]; } public void setLevel(byte[] rom, int level, int i) { rom[levelOffsets[i]] = (byte) level; } } @Override public List getStaticPokemon() { List statics = new ArrayList<>(); if (romEntry.getValue("StaticPokemonSupport") > 0) { for (StaticPokemon sp : romEntry.staticPokemon) { StaticEncounter se = new StaticEncounter(); se.pkmn = sp.getPokemon(this); se.level = sp.getLevel(rom, 0); statics.add(se); } } return statics; } @Override public boolean setStaticPokemon(List staticPokemon) { if (romEntry.getValue("StaticPokemonSupport") == 0) { return false; } for (int i = 0; i < romEntry.staticPokemon.size(); i++) { StaticEncounter se = staticPokemon.get(i); StaticPokemon sp = romEntry.staticPokemon.get(i); sp.setPokemon(this, se.pkmn); sp.setLevel(rom, se.level, 0); } return true; } @Override public boolean canChangeStaticPokemon() { return (romEntry.getValue("StaticPokemonSupport") > 0); } @Override public boolean hasStaticAltFormes() { return false; } @Override public boolean hasMainGameLegendaries() { return false; } @Override public List getMainGameLegendaries() { return new ArrayList<>(); } @Override public List getSpecialMusicStatics() { return new ArrayList<>(); } @Override public void applyCorrectStaticMusic(Map specialMusicStaticChanges) { } @Override public boolean hasStaticMusicFix() { return false; } @Override public List getTotemPokemon() { return new ArrayList<>(); } @Override public void setTotemPokemon(List totemPokemon) { } @Override public List getTMMoves() { List tms = new ArrayList<>(); int offset = romEntry.getValue("TMMovesOffset"); for (int i = 1; i <= Gen1Constants.tmCount; i++) { tms.add(moveRomToNumTable[rom[offset + (i - 1)] & 0xFF]); } return tms; } @Override public List getHMMoves() { List hms = new ArrayList<>(); int offset = romEntry.getValue("TMMovesOffset"); for (int i = 1; i <= Gen1Constants.hmCount; i++) { hms.add(moveRomToNumTable[rom[offset + Gen1Constants.tmCount + (i - 1)] & 0xFF]); } return hms; } @Override public void setTMMoves(List moveIndexes) { int offset = romEntry.getValue("TMMovesOffset"); for (int i = 1; i <= Gen1Constants.tmCount; i++) { rom[offset + (i - 1)] = (byte) moveNumToRomTable[moveIndexes.get(i - 1)]; } // Gym Leader TM Moves (RB only) if (!romEntry.isYellow) { int[] tms = Gen1Constants.gymLeaderTMs; int glMovesOffset = romEntry.getValue("GymLeaderMovesTableOffset"); for (int i = 0; i < tms.length; i++) { // Set the special move used by gym (i+1) to // the move we just wrote to TM tms[i] rom[glMovesOffset + i * 2] = (byte) moveNumToRomTable[moveIndexes.get(tms[i] - 1)]; } } // TM Text String[] moveNames = readMoveNames(); for (TMTextEntry tte : romEntry.tmTexts) { String moveName = moveNames[moveNumToRomTable[moveIndexes.get(tte.number - 1)]]; String text = tte.template.replace("%m", moveName); writeVariableLengthString(text, tte.offset, true); } } @Override public int getTMCount() { return Gen1Constants.tmCount; } @Override public int getHMCount() { return Gen1Constants.hmCount; } @Override public Map getTMHMCompatibility() { Map compat = new TreeMap<>(); int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset"); for (int i = 1; i <= pokedexCount; i++) { int baseStatsOffset = (romEntry.isYellow || i != Species.mew) ? (pokeStatsOffset + (i - 1) * Gen1Constants.baseStatsEntrySize) : romEntry.getValue("MewStatsOffset"); Pokemon pkmn = pokes[i]; boolean[] flags = new boolean[Gen1Constants.tmCount + Gen1Constants.hmCount + 1]; for (int j = 0; j < 7; j++) { readByteIntoFlags(flags, j * 8 + 1, baseStatsOffset + Gen1Constants.bsTMHMCompatOffset + j); } compat.put(pkmn, flags); } return compat; } @Override public void setTMHMCompatibility(Map compatData) { int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset"); for (Map.Entry compatEntry : compatData.entrySet()) { Pokemon pkmn = compatEntry.getKey(); boolean[] flags = compatEntry.getValue(); int baseStatsOffset = (romEntry.isYellow || pkmn.number != Species.mew) ? (pokeStatsOffset + (pkmn.number - 1) * Gen1Constants.baseStatsEntrySize) : romEntry.getValue("MewStatsOffset"); for (int j = 0; j < 7; j++) { rom[baseStatsOffset + Gen1Constants.bsTMHMCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1); } } } @Override public boolean hasMoveTutors() { return false; } @Override public List getMoveTutorMoves() { return new ArrayList<>(); } @Override public void setMoveTutorMoves(List moves) { // Do nothing } @Override public Map getMoveTutorCompatibility() { return new TreeMap<>(); } @Override public void setMoveTutorCompatibility(Map compatData) { // Do nothing } @Override public String getROMName() { return "Pokemon " + romEntry.name; } @Override public String getROMCode() { return romEntry.romName + " (" + romEntry.version + "/" + romEntry.nonJapanese + ")"; } @Override public String getSupportLevel() { return (romEntry.getValue("StaticPokemonSupport") > 0) ? "Complete" : "No Static Pokemon"; } private static int find(byte[] haystack, String hexString) { if (hexString.length() % 2 != 0) { return -3; // error } byte[] searchFor = new byte[hexString.length() / 2]; for (int i = 0; i < searchFor.length; i++) { searchFor[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16); } List found = RomFunctions.search(haystack, searchFor); if (found.size() == 0) { return -1; // not found } else if (found.size() > 1) { return -2; // not unique } else { return found.get(0); } } private void populateEvolutions() { for (Pokemon pkmn : pokes) { if (pkmn != null) { pkmn.evolutionsFrom.clear(); pkmn.evolutionsTo.clear(); } } int pointersOffset = romEntry.getValue("PokemonMovesetsTableOffset"); int pkmnCount = romEntry.getValue("InternalPokemonCount"); for (int i = 1; i <= pkmnCount; i++) { int pointer = readWord(pointersOffset + (i - 1) * 2); int realPointer = calculateOffset(bankOf(pointersOffset), pointer); if (pokeRBYToNumTable[i] != 0) { int thisPoke = pokeRBYToNumTable[i]; Pokemon pkmn = pokes[thisPoke]; while (rom[realPointer] != 0) { int method = rom[realPointer]; EvolutionType type = EvolutionType.fromIndex(1, method); int otherPoke = pokeRBYToNumTable[rom[realPointer + 2 + (type == EvolutionType.STONE ? 1 : 0)] & 0xFF]; int extraInfo = rom[realPointer + 1] & 0xFF; Evolution evo = new Evolution(pkmn, pokes[otherPoke], true, type, extraInfo); if (!pkmn.evolutionsFrom.contains(evo)) { pkmn.evolutionsFrom.add(evo); if (pokes[otherPoke] != null) { pokes[otherPoke].evolutionsTo.add(evo); } } realPointer += (type == EvolutionType.STONE ? 4 : 3); } // split evos don't carry stats if (pkmn.evolutionsFrom.size() > 1) { for (Evolution e : pkmn.evolutionsFrom) { e.carryStats = false; } } } } } @Override public void removeImpossibleEvolutions(Settings settings) { // Gen 1: only regular trade evos // change them all to evolve at level 37 for (Pokemon pkmn : pokes) { if (pkmn != null) { for (Evolution evo : pkmn.evolutionsFrom) { if (evo.type == EvolutionType.TRADE) { // change evo.type = EvolutionType.LEVEL; evo.extraInfo = 37; addEvoUpdateLevel(impossibleEvolutionUpdates,evo); } } } } } @Override public void makeEvolutionsEasier(Settings settings) { // No such thing } @Override public void removeTimeBasedEvolutions() { // No such thing } @Override public boolean hasShopRandomization() { return false; } @Override public Map getShopItems() { return null; // Not implemented } @Override public void setShopItems(Map shopItems) { // Not implemented } @Override public void setShopPrices() { // Not implemented } private List getTrainerClassesForText() { int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets"); List tcNames = new ArrayList<>(); int offset = offsets[offsets.length - 1]; for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) { String name = readVariableLengthString(offset, false); offset += lengthOfStringAt(offset, false) + 1; tcNames.add(name); } return tcNames; } @Override public boolean canChangeTrainerText() { return romEntry.getValue("CanChangeTrainerText") > 0; } @Override public List getDoublesTrainerClasses() { return Collections.emptyList(); } @Override public List getTrainerNames() { int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets"); List trainerNames = new ArrayList<>(); int offset = offsets[offsets.length - 1]; for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) { String name = readVariableLengthString(offset, false); offset += lengthOfStringAt(offset, false) + 1; if (Gen1Constants.singularTrainers.contains(j)) { trainerNames.add(name); } } return trainerNames; } @Override public void setTrainerNames(List trainerNames) { if (romEntry.getValue("CanChangeTrainerText") > 0) { int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets"); Iterator trainerNamesI = trainerNames.iterator(); int offset = offsets[offsets.length - 1]; for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) { int oldLength = lengthOfStringAt(offset, false) + 1; if (Gen1Constants.singularTrainers.contains(j)) { String newName = trainerNamesI.next(); writeFixedLengthString(newName, offset, oldLength); } offset += oldLength; } } } @Override public TrainerNameMode trainerNameMode() { return TrainerNameMode.SAME_LENGTH; } @Override public List getTCNameLengthsByTrainer() { // not needed return new ArrayList<>(); } @Override public List getTrainerClassNames() { int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets"); List trainerClassNames = new ArrayList<>(); if (offsets.length == 2) { for (int i = 0; i < offsets.length; i++) { int offset = offsets[i]; for (int j = 0; j < Gen1Constants.tclassesCounts[i]; j++) { String name = readVariableLengthString(offset, false); offset += lengthOfStringAt(offset, false) + 1; if (i == 0 || !Gen1Constants.singularTrainers.contains(j)) { trainerClassNames.add(name); } } } } else { int offset = offsets[0]; for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) { String name = readVariableLengthString(offset, false); offset += lengthOfStringAt(offset, false) + 1; if (!Gen1Constants.singularTrainers.contains(j)) { trainerClassNames.add(name); } } } return trainerClassNames; } @Override public void setTrainerClassNames(List trainerClassNames) { if (romEntry.getValue("CanChangeTrainerText") > 0) { int[] offsets = romEntry.arrayEntries.get("TrainerClassNamesOffsets"); Iterator tcNamesIter = trainerClassNames.iterator(); if (offsets.length == 2) { for (int i = 0; i < offsets.length; i++) { int offset = offsets[i]; for (int j = 0; j < Gen1Constants.tclassesCounts[i]; j++) { int oldLength = lengthOfStringAt(offset, false) + 1; if (i == 0 || !Gen1Constants.singularTrainers.contains(j)) { String newName = tcNamesIter.next(); writeFixedLengthString(newName, offset, oldLength); } offset += oldLength; } } } else { int offset = offsets[0]; for (int j = 0; j < Gen1Constants.tclassesCounts[1]; j++) { int oldLength = lengthOfStringAt(offset, false) + 1; if (!Gen1Constants.singularTrainers.contains(j)) { String newName = tcNamesIter.next(); writeFixedLengthString(newName, offset, oldLength); } offset += oldLength; } } } } @Override public boolean fixedTrainerClassNamesLength() { return true; } @Override public String getDefaultExtension() { return "gbc"; } @Override public int abilitiesPerPokemon() { return 0; } @Override public int highestAbilityIndex() { return 0; } @Override public Map> getAbilityVariations() { return new HashMap<>(); } @Override public boolean hasMegaEvolutions() { return false; } @Override public int internalStringLength(String string) { return translateString(string).length; } @Override public int miscTweaksAvailable() { int available = MiscTweak.LOWER_CASE_POKEMON_NAMES.getValue(); available |= MiscTweak.UPDATE_TYPE_EFFECTIVENESS.getValue(); if (romEntry.tweakFiles.get("BWXPTweak") != null) { available |= MiscTweak.BW_EXP_PATCH.getValue(); } if (romEntry.tweakFiles.get("XAccNerfTweak") != null) { available |= MiscTweak.NERF_X_ACCURACY.getValue(); } if (romEntry.tweakFiles.get("CritRateTweak") != null) { available |= MiscTweak.FIX_CRIT_RATE.getValue(); } if (romEntry.getValue("TextDelayFunctionOffset") != 0) { available |= MiscTweak.FASTEST_TEXT.getValue(); } if (romEntry.getValue("PCPotionOffset") != 0) { available |= MiscTweak.RANDOMIZE_PC_POTION.getValue(); } if (romEntry.getValue("PikachuEvoJumpOffset") != 0) { available |= MiscTweak.ALLOW_PIKACHU_EVOLUTION.getValue(); } if (romEntry.getValue("CatchingTutorialMonOffset") != 0) { available |= MiscTweak.RANDOMIZE_CATCHING_TUTORIAL.getValue(); } return available; } @Override public void applyMiscTweak(MiscTweak tweak) { if (tweak == MiscTweak.BW_EXP_PATCH) { applyBWEXPPatch(); } else if (tweak == MiscTweak.NERF_X_ACCURACY) { applyXAccNerfPatch(); } else if (tweak == MiscTweak.FIX_CRIT_RATE) { applyCritRatePatch(); } else if (tweak == MiscTweak.FASTEST_TEXT) { applyFastestTextPatch(); } else if (tweak == MiscTweak.RANDOMIZE_PC_POTION) { randomizePCPotion(); } else if (tweak == MiscTweak.ALLOW_PIKACHU_EVOLUTION) { applyPikachuEvoPatch(); } else if (tweak == MiscTweak.LOWER_CASE_POKEMON_NAMES) { applyCamelCaseNames(); } else if (tweak == MiscTweak.UPDATE_TYPE_EFFECTIVENESS) { updateTypeEffectiveness(); } else if (tweak == MiscTweak.RANDOMIZE_CATCHING_TUTORIAL) { randomizeCatchingTutorial(); } } @Override public boolean isEffectivenessUpdated() { return effectivenessUpdated; } private void applyBWEXPPatch() { genericIPSPatch("BWXPTweak"); } private void applyXAccNerfPatch() { xAccNerfed = genericIPSPatch("XAccNerfTweak"); } private void applyCritRatePatch() { genericIPSPatch("CritRateTweak"); } private void applyFastestTextPatch() { if (romEntry.getValue("TextDelayFunctionOffset") != 0) { rom[romEntry.getValue("TextDelayFunctionOffset")] = GBConstants.gbZ80Ret; } } private void randomizePCPotion() { if (romEntry.getValue("PCPotionOffset") != 0) { rom[romEntry.getValue("PCPotionOffset")] = (byte) this.getNonBadItems().randomNonTM(this.random); } } private void applyPikachuEvoPatch() { if (romEntry.getValue("PikachuEvoJumpOffset") != 0) { rom[romEntry.getValue("PikachuEvoJumpOffset")] = GBConstants.gbZ80JumpRelative; } } private void randomizeCatchingTutorial() { if (romEntry.getValue("CatchingTutorialMonOffset") != 0) { rom[romEntry.getValue("CatchingTutorialMonOffset")] = (byte) pokeNumToRBYTable[this.randomPokemon().number]; } } @Override public void enableGuaranteedPokemonCatching() { int offset = find(rom, Gen1Constants.guaranteedCatchPrefix); if (offset > 0) { offset += Gen1Constants.guaranteedCatchPrefix.length() / 2; // because it was a prefix // The game ensures that the Master Ball always catches a Pokemon by running the following code: // ; Get the item ID. // ld hl, wcf91 // ld a, [hl] // // ; The Master Ball always succeeds. // cp MASTER_BALL // jp z, .captured // By making the jump here unconditional, we can ensure that catching always succeeds no // matter the ball type. We check that the original condition is present just for safety. if (rom[offset] == (byte)0xCA) { rom[offset] = (byte)0xC3; } } } private boolean genericIPSPatch(String ctName) { String patchName = romEntry.tweakFiles.get(ctName); if (patchName == null) { return false; } try { FileFunctions.applyPatch(rom, patchName); return true; } catch (IOException e) { throw new RandomizerIOException(e); } } @Override public List getGameBreakingMoves() { // Sonicboom & drage & OHKO moves // 160 add spore // also remove OHKO if xacc nerfed if (xAccNerfed) { return Gen1Constants.bannedMovesWithXAccBanned; } else { return Gen1Constants.bannedMovesWithoutXAccBanned; } } @Override public List getFieldMoves() { // cut, fly, surf, strength, flash, // dig, teleport (NOT softboiled) return Gen1Constants.fieldMoves; } @Override public List getEarlyRequiredHMMoves() { // just cut return Gen1Constants.earlyRequiredHMs; } @Override public void randomizeIntroPokemon() { // First off, intro Pokemon // 160 add yellow intro random int introPokemon = pokeNumToRBYTable[this.randomPokemon().number]; rom[romEntry.getValue("IntroPokemonOffset")] = (byte) introPokemon; rom[romEntry.getValue("IntroCryOffset")] = (byte) introPokemon; } @Override public ItemList getAllowedItems() { return Gen1Constants.allowedItems; } @Override public ItemList getNonBadItems() { // Gen 1 has no bad items Kappa return Gen1Constants.allowedItems; } @Override public List getUniqueNoSellItems() { return new ArrayList<>(); } @Override public List getRegularShopItems() { return null; // Not implemented } @Override public List getOPShopItems() { return null; // Not implemented } private void loadItemNames() { itemNames = new String[256]; itemNames[0] = "glitch"; // trying to emulate pretty much what the game does here // normal items int origOffset = romEntry.getValue("ItemNamesOffset"); int itemNameOffset = origOffset; for (int index = 1; index <= 0x100; index++) { if (itemNameOffset / GBConstants.bankSize > origOffset / GBConstants.bankSize) { // the game would continue making its merry way into VRAM here, // but we don't have VRAM to simulate. // just give up. break; } int startOfText = itemNameOffset; while ((rom[itemNameOffset] & 0xFF) != GBConstants.stringTerminator) { itemNameOffset++; } itemNameOffset++; itemNames[index % 256] = readFixedLengthString(startOfText, 20); } // hms override for (int index = Gen1Constants.hmsStartIndex; index < Gen1Constants.tmsStartIndex; index++) { itemNames[index] = String.format("HM%02d", index - Gen1Constants.hmsStartIndex + 1); } // tms override for (int index = Gen1Constants.tmsStartIndex; index < 0x100; index++) { itemNames[index] = String.format("TM%02d", index - Gen1Constants.tmsStartIndex + 1); } } @Override public String[] getItemNames() { return itemNames; } private static class SubMap { private int id; private int addr; private int bank; private MapHeader header; private Connection[] cons; private int n_cons; private int obj_addr; private List itemOffsets; } private static class MapHeader { private int tileset_id; // u8 private int map_h, map_w; // u8 private int map_ptr, text_ptr, script_ptr; // u16 private int connect_byte; // u8 // 10 bytes } private static class Connection { private int index; // u8 private int connected_map; // u16 private int current_map; // u16 private int bigness; // u8 private int map_width; // u8 private int y_align; // u8 private int x_align; // u8 private int window; // u16 // 11 bytes } private void preloadMaps() { int mapBanks = romEntry.getValue("MapBanks"); int mapAddresses = romEntry.getValue("MapAddresses"); preloadMap(mapBanks, mapAddresses, 0); } private void preloadMap(int mapBanks, int mapAddresses, int mapID) { if (maps[mapID] != null || mapID == 0xED || mapID == 0xFF) { return; } SubMap map = new SubMap(); maps[mapID] = map; map.id = mapID; map.addr = calculateOffset(rom[mapBanks + mapID] & 0xFF, readWord(mapAddresses + mapID * 2)); map.bank = bankOf(map.addr); map.header = new MapHeader(); map.header.tileset_id = rom[map.addr] & 0xFF; map.header.map_h = rom[map.addr + 1] & 0xFF; map.header.map_w = rom[map.addr + 2] & 0xFF; map.header.map_ptr = calculateOffset(map.bank, readWord(map.addr + 3)); map.header.text_ptr = calculateOffset(map.bank, readWord(map.addr + 5)); map.header.script_ptr = calculateOffset(map.bank, readWord(map.addr + 7)); map.header.connect_byte = rom[map.addr + 9] & 0xFF; int cb = map.header.connect_byte; map.n_cons = ((cb & 8) >> 3) + ((cb & 4) >> 2) + ((cb & 2) >> 1) + (cb & 1); int cons_offset = map.addr + 10; map.cons = new Connection[map.n_cons]; for (int i = 0; i < map.n_cons; i++) { int tcon_offs = cons_offset + i * 11; Connection con = new Connection(); con.index = rom[tcon_offs] & 0xFF; con.connected_map = readWord(tcon_offs + 1); con.current_map = readWord(tcon_offs + 3); con.bigness = rom[tcon_offs + 5] & 0xFF; con.map_width = rom[tcon_offs + 6] & 0xFF; con.y_align = rom[tcon_offs + 7] & 0xFF; con.x_align = rom[tcon_offs + 8] & 0xFF; con.window = readWord(tcon_offs + 9); map.cons[i] = con; preloadMap(mapBanks, mapAddresses, con.index); } map.obj_addr = calculateOffset(map.bank, readWord(cons_offset + map.n_cons * 11)); // Read objects // +0 is the border tile (ignore) // +1 is warp count int n_warps = rom[map.obj_addr + 1] & 0xFF; int offs = map.obj_addr + 2; for (int i = 0; i < n_warps; i++) { // track this warp int to_map = rom[offs + 3] & 0xFF; preloadMap(mapBanks, mapAddresses, to_map); offs += 4; } // Now we're pointing to sign count int n_signs = rom[offs++] & 0xFF; offs += n_signs * 3; // Finally, entities, which contain the items map.itemOffsets = new ArrayList<>(); int n_entities = rom[offs++] & 0xFF; for (int i = 0; i < n_entities; i++) { // Read text ID int tid = rom[offs + 5] & 0xFF; if ((tid & (1 << 6)) > 0) { // trainer offs += 8; } else if ((tid & (1 << 7)) > 0 && (rom[offs + 6] != 0x00)) { // item map.itemOffsets.add(offs + 6); offs += 7; } else { // generic offs += 6; } } } private void loadMapNames() { mapNames = new String[256]; int mapNameTableOffset = romEntry.getValue("MapNameTableOffset"); int mapNameBank = bankOf(mapNameTableOffset); // external names List usedExternal = new ArrayList<>(); for (int i = 0; i < 0x25; i++) { int externalOffset = calculateOffset(mapNameBank, readWord(mapNameTableOffset + 1)); usedExternal.add(externalOffset); mapNames[i] = readVariableLengthString(externalOffset, false); mapNameTableOffset += 3; } // internal names int lastMaxMap = 0x25; Map previousMapCounts = new HashMap<>(); while ((rom[mapNameTableOffset] & 0xFF) != 0xFF) { int maxMap = rom[mapNameTableOffset] & 0xFF; int nameOffset = calculateOffset(mapNameBank, readWord(mapNameTableOffset + 2)); String actualName = readVariableLengthString(nameOffset, false).trim(); if (usedExternal.contains(nameOffset)) { for (int i = lastMaxMap; i < maxMap; i++) { if (maps[i] != null) { mapNames[i] = actualName + " (Building)"; } } } else { int mapCount = 0; if (previousMapCounts.containsKey(nameOffset)) { mapCount = previousMapCounts.get(nameOffset); } for (int i = lastMaxMap; i < maxMap; i++) { if (maps[i] != null) { mapCount++; mapNames[i] = actualName + " (" + mapCount + ")"; } } previousMapCounts.put(nameOffset, mapCount); } lastMaxMap = maxMap; mapNameTableOffset += 4; } } private List getItemOffsets() { List itemOffs = new ArrayList<>(); for (SubMap map : maps) { if (map != null) { itemOffs.addAll(map.itemOffsets); } } int hiRoutine = romEntry.getValue("HiddenItemRoutine"); int spclTable = romEntry.getValue("SpecialMapPointerTable"); int spclBank = bankOf(spclTable); if (!isYellow()) { int lOffs = romEntry.getValue("SpecialMapList"); int idx = 0; while ((rom[lOffs] & 0xFF) != 0xFF) { int spclOffset = calculateOffset(spclBank, readWord(spclTable + idx)); while ((rom[spclOffset] & 0xFF) != 0xFF) { if (calculateOffset(rom[spclOffset + 3] & 0xFF, readWord(spclOffset + 4)) == hiRoutine) { itemOffs.add(spclOffset + 2); } spclOffset += 6; } lOffs++; idx += 2; } } else { int lOffs = spclTable; while ((rom[lOffs] & 0xFF) != 0xFF) { int spclOffset = calculateOffset(spclBank, readWord(lOffs + 1)); while ((rom[spclOffset] & 0xFF) != 0xFF) { if (calculateOffset(rom[spclOffset + 3] & 0xFF, readWord(spclOffset + 4)) == hiRoutine) { itemOffs.add(spclOffset + 2); } spclOffset += 6; } lOffs += 3; } } return itemOffs; } @Override public List getRequiredFieldTMs() { return Gen1Constants.requiredFieldTMs; } @Override public List getCurrentFieldTMs() { List itemOffsets = getItemOffsets(); List fieldTMs = new ArrayList<>(); for (int offset : itemOffsets) { int itemHere = rom[offset] & 0xFF; if (Gen1Constants.allowedItems.isTM(itemHere)) { fieldTMs.add(itemHere - Gen1Constants.tmsStartIndex + 1); // TM // offset } } return fieldTMs; } @Override public void setFieldTMs(List fieldTMs) { List itemOffsets = getItemOffsets(); Iterator iterTMs = fieldTMs.iterator(); for (int offset : itemOffsets) { int itemHere = rom[offset] & 0xFF; if (Gen1Constants.allowedItems.isTM(itemHere)) { // Replace this with a TM from the list rom[offset] = (byte) (iterTMs.next() + Gen1Constants.tmsStartIndex - 1); } } } @Override public List getRegularFieldItems() { List itemOffsets = getItemOffsets(); List fieldItems = new ArrayList<>(); for (int offset : itemOffsets) { int itemHere = rom[offset] & 0xFF; if (Gen1Constants.allowedItems.isAllowed(itemHere) && !(Gen1Constants.allowedItems.isTM(itemHere))) { fieldItems.add(itemHere); } } return fieldItems; } @Override public void setRegularFieldItems(List items) { List itemOffsets = getItemOffsets(); Iterator iterItems = items.iterator(); for (int offset : itemOffsets) { int itemHere = rom[offset] & 0xFF; if (Gen1Constants.allowedItems.isAllowed(itemHere) && !(Gen1Constants.allowedItems.isTM(itemHere))) { // Replace it rom[offset] = (byte) (iterItems.next().intValue()); } } } @Override public List getIngameTrades() { List trades = new ArrayList<>(); // info int tableOffset = romEntry.getValue("TradeTableOffset"); int tableSize = romEntry.getValue("TradeTableSize"); int nicknameLength = romEntry.getValue("TradeNameLength"); int[] unused = romEntry.arrayEntries.get("TradesUnused"); int unusedOffset = 0; int entryLength = nicknameLength + 3; for (int entry = 0; entry < tableSize; entry++) { if (unusedOffset < unused.length && unused[unusedOffset] == entry) { unusedOffset++; continue; } IngameTrade trade = new IngameTrade(); int entryOffset = tableOffset + entry * entryLength; trade.requestedPokemon = pokes[pokeRBYToNumTable[rom[entryOffset] & 0xFF]]; trade.givenPokemon = pokes[pokeRBYToNumTable[rom[entryOffset + 1] & 0xFF]]; trade.nickname = readString(entryOffset + 3, nicknameLength, false); trades.add(trade); } return trades; } @Override public void setIngameTrades(List trades) { // info int tableOffset = romEntry.getValue("TradeTableOffset"); int tableSize = romEntry.getValue("TradeTableSize"); int nicknameLength = romEntry.getValue("TradeNameLength"); int[] unused = romEntry.arrayEntries.get("TradesUnused"); int unusedOffset = 0; int entryLength = nicknameLength + 3; int tradeOffset = 0; for (int entry = 0; entry < tableSize; entry++) { if (unusedOffset < unused.length && unused[unusedOffset] == entry) { unusedOffset++; continue; } IngameTrade trade = trades.get(tradeOffset++); int entryOffset = tableOffset + entry * entryLength; rom[entryOffset] = (byte) pokeNumToRBYTable[trade.requestedPokemon.number]; rom[entryOffset + 1] = (byte) pokeNumToRBYTable[trade.givenPokemon.number]; if (romEntry.getValue("CanChangeTrainerText") > 0) { writeFixedLengthString(trade.nickname, entryOffset + 3, nicknameLength); } } } @Override public boolean hasDVs() { return true; } @Override public int generationOfPokemon() { return 1; } @Override public void removeEvosForPokemonPool() { // gen1 doesn't have this functionality anyway } @Override public boolean supportsFourStartingMoves() { return true; } private void writeEvosAndMovesLearnt(boolean writeEvos, Map> movesets) { // we assume a few things here: // 1) evos & moves learnt are stored directly after their pointer table // 2) PokemonMovesetsExtraSpaceOffset is in the same bank, and // points to the start of the free space at the end of the bank // (if set to 0, disabled from being used) // 3) PokemonMovesetsDataSize is from the start of actual data to // the start of engine/battle/e_2.asm in pokered (aka code we can't // overwrite) // it appears that in yellow, this code is moved // so we can write the evos/movesets in one continuous block // until the end of the bank. // so for yellow, extraspace is disabled. // specify null to either argument to copy old values int pokeStatsOffset = romEntry.getValue("PokemonStatsOffset"); int movesEvosStart = romEntry.getValue("PokemonMovesetsTableOffset"); int movesEvosBank = bankOf(movesEvosStart); int pkmnCount = romEntry.getValue("InternalPokemonCount"); byte[] pointerTable = new byte[pkmnCount * 2]; int mainDataBlockSize = romEntry.getValue("PokemonMovesetsDataSize"); int mainDataBlockOffset = movesEvosStart + pointerTable.length; byte[] mainDataBlock = new byte[mainDataBlockSize]; int offsetInMainData = 0; int extraSpaceOffset = romEntry.getValue("PokemonMovesetsExtraSpaceOffset"); int extraSpaceBank = bankOf(extraSpaceOffset); boolean extraSpaceEnabled = false; byte[] extraDataBlock = null; int offsetInExtraData = 0; int extraSpaceSize = 0; if (movesEvosBank == extraSpaceBank && extraSpaceOffset != 0) { extraSpaceEnabled = true; int startOfNextBank = ((extraSpaceOffset / GBConstants.bankSize) + 1) * GBConstants.bankSize; extraSpaceSize = startOfNextBank - extraSpaceOffset; extraDataBlock = new byte[extraSpaceSize]; } int nullEntryPointer = -1; for (int i = 1; i <= pkmnCount; i++) { byte[] writeData = null; int oldDataOffset = calculateOffset(movesEvosBank, readWord(movesEvosStart + (i - 1) * 2)); boolean setNullEntryPointerHere = false; if (pokeRBYToNumTable[i] == 0) { // null entry if (nullEntryPointer == -1) { // make the null entry writeData = new byte[] { 0, 0 }; setNullEntryPointerHere = true; } else { writeWord(pointerTable, (i - 1) * 2, nullEntryPointer); } } else { int pokeNum = pokeRBYToNumTable[i]; Pokemon pkmn = pokes[pokeNum]; ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); // Evolutions if (!writeEvos) { // copy old int evoOffset = oldDataOffset; while (rom[evoOffset] != 0x00) { int method = rom[evoOffset] & 0xFF; int limiter = (method == 2) ? 4 : 3; for (int b = 0; b < limiter; b++) { dataStream.write(rom[evoOffset++] & 0xFF); } } } else { for (Evolution evo : pkmn.evolutionsFrom) { // write evos for this poke dataStream.write(evo.type.toIndex(1)); if (evo.type == EvolutionType.LEVEL) { dataStream.write(evo.extraInfo); // min lvl } else if (evo.type == EvolutionType.STONE) { dataStream.write(evo.extraInfo); // stone item dataStream.write(1); // minimum level } else if (evo.type == EvolutionType.TRADE) { dataStream.write(1); // minimum level } int pokeIndexTo = pokeNumToRBYTable[evo.to.number]; dataStream.write(pokeIndexTo); // species } } // write terminator for evos dataStream.write(0); // Movesets if (movesets == null) { // copy old int movesOffset = oldDataOffset; // move past evos while (rom[movesOffset] != 0x00) { int method = rom[movesOffset] & 0xFF; movesOffset += (method == 2) ? 4 : 3; } movesOffset++; // copy moves while (rom[movesOffset] != 0x00) { dataStream.write(rom[movesOffset++] & 0xFF); dataStream.write(rom[movesOffset++] & 0xFF); } } else { List ourMoves = movesets.get(pkmn.number); int statsOffset; if (pokeNum == Species.mew && !romEntry.isYellow) { // Mewww statsOffset = romEntry.getValue("MewStatsOffset"); } else { statsOffset = (pokeNum - 1) * Gen1Constants.baseStatsEntrySize + pokeStatsOffset; } int movenum = 0; while (movenum < 4 && ourMoves.size() > movenum && ourMoves.get(movenum).level == 1) { rom[statsOffset + Gen1Constants.bsLevel1MovesOffset + movenum] = (byte) moveNumToRomTable[ourMoves .get(movenum).move]; movenum++; } // Write out the rest of zeroes for (int mn = movenum; mn < 4; mn++) { rom[statsOffset + Gen1Constants.bsLevel1MovesOffset + mn] = 0; } // Add the non level 1 moves to the data stream while (movenum < ourMoves.size()) { dataStream.write(ourMoves.get(movenum).level); dataStream.write(moveNumToRomTable[ourMoves.get(movenum).move]); movenum++; } } // terminator dataStream.write(0); // done, set writeData writeData = dataStream.toByteArray(); try { dataStream.close(); } catch (IOException e) { } } // write data and set pointer? if (writeData != null) { int lengthToFit = writeData.length; int pointerToWrite; // compression of leading & trailing 0s: // every entry ends in a 0 (end of move list). // if a block already has data in it, and the data // we want to write starts with a 0 (no evolutions) // we can compress it into the end of the last entry // this saves a decent amount of space overall. if ((offsetInMainData + lengthToFit <= mainDataBlockSize) || (writeData[0] == 0 && offsetInMainData > 0 && offsetInMainData + lengthToFit == mainDataBlockSize + 1)) { // place in main storage if (writeData[0] == 0 && offsetInMainData > 0) { int writtenDataOffset = mainDataBlockOffset + offsetInMainData - 1; pointerToWrite = makeGBPointer(writtenDataOffset); System.arraycopy(writeData, 1, mainDataBlock, offsetInMainData, lengthToFit - 1); offsetInMainData += lengthToFit - 1; } else { int writtenDataOffset = mainDataBlockOffset + offsetInMainData; pointerToWrite = makeGBPointer(writtenDataOffset); System.arraycopy(writeData, 0, mainDataBlock, offsetInMainData, lengthToFit); offsetInMainData += lengthToFit; } } else if (extraSpaceEnabled && ((offsetInExtraData + lengthToFit <= extraSpaceSize) || (writeData[0] == 0 && offsetInExtraData > 0 && offsetInExtraData + lengthToFit == extraSpaceSize + 1))) { // place in extra space if (writeData[0] == 0 && offsetInExtraData > 0) { int writtenDataOffset = extraSpaceOffset + offsetInExtraData - 1; pointerToWrite = makeGBPointer(writtenDataOffset); System.arraycopy(writeData, 1, extraDataBlock, offsetInExtraData, lengthToFit - 1); offsetInExtraData += lengthToFit - 1; } else { int writtenDataOffset = extraSpaceOffset + offsetInExtraData; pointerToWrite = makeGBPointer(writtenDataOffset); System.arraycopy(writeData, 0, extraDataBlock, offsetInExtraData, lengthToFit); offsetInExtraData += lengthToFit; } } else { // this should never happen, but if not, uh oh throw new RandomizationException("Unable to save moves/evolutions, out of space"); } if (pointerToWrite >= 0) { writeWord(pointerTable, (i - 1) * 2, pointerToWrite); if (setNullEntryPointerHere) { nullEntryPointer = pointerToWrite; } } } } // Done, write final results to ROM System.arraycopy(pointerTable, 0, rom, movesEvosStart, pointerTable.length); System.arraycopy(mainDataBlock, 0, rom, mainDataBlockOffset, mainDataBlock.length); if (extraSpaceEnabled) { System.arraycopy(extraDataBlock, 0, rom, extraSpaceOffset, extraDataBlock.length); } } @Override public boolean isRomValid() { return romEntry.expectedCRC32 == actualCRC32; } @Override public BufferedImage getMascotImage() { Pokemon mascot = randomPokemon(); int idx = pokeNumToRBYTable[mascot.number]; int fsBank; // define (by index number) the bank that a pokemon's image is in // using pokered code if (mascot.number == Species.mew && !romEntry.isYellow) { fsBank = 1; } else if (idx < 0x1F) { fsBank = 0x9; } else if (idx < 0x4A) { fsBank = 0xA; } else if (idx < 0x74 || idx == 0x74 && mascot.frontSpritePointer > 0x7000) { fsBank = 0xB; } else if (idx < 0x99 || idx == 0x99 && mascot.frontSpritePointer > 0x7000) { fsBank = 0xC; } else { fsBank = 0xD; } int fsOffset = calculateOffset(fsBank, mascot.frontSpritePointer); Gen1Decmp mscSprite = new Gen1Decmp(rom, fsOffset); mscSprite.decompress(); mscSprite.transpose(); int w = mscSprite.getWidth(); int h = mscSprite.getHeight(); // Palette? int[] palette; if (romEntry.getValue("MonPaletteIndicesOffset") > 0 && romEntry.getValue("SGBPalettesOffset") > 0) { int palIndex = rom[romEntry.getValue("MonPaletteIndicesOffset") + mascot.number] & 0xFF; int palOffset = romEntry.getValue("SGBPalettesOffset") + palIndex * 8; if (romEntry.isYellow && romEntry.nonJapanese == 1) { // Non-japanese Yellow can use GBC palettes instead. // Stored directly after regular SGB palettes. palOffset += 320; } palette = new int[4]; for (int i = 0; i < 4; i++) { palette[i] = GFXFunctions.conv16BitColorToARGB(readWord(palOffset + i * 2)); } } else { palette = new int[] { 0xFFFFFFFF, 0xFFAAAAAA, 0xFF666666, 0xFF000000 }; } byte[] data = mscSprite.getFlattenedData(); BufferedImage bim = GFXFunctions.drawTiledImage(data, palette, w, h, 8); GFXFunctions.pseudoTransparency(bim, palette[0]); return bim; } }