package com.sneed.pkrandom.romhandlers; /*----------------------------------------------------------------------------*/ /*-- Gen2RomHandler.java - randomizer handler for G/S/C. --*/ /*-- --*/ /*-- 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.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.sneed.pkrandom.*; import com.sneed.pkrandom.constants.*; import com.sneed.pkrandom.exceptions.RandomizerIOException; import com.sneed.pkrandom.pokemon.*; import compressors.Gen2Decmp; public class Gen2RomHandler extends AbstractGBCRomHandler { public static class Factory extends RomHandler.Factory { @Override public Gen2RomHandler create(Random random, PrintStream logStream) { return new Gen2RomHandler(random, logStream); } public boolean isLoadable(String filename) { long fileLength = new File(filename).length(); if (fileLength > 8 * 1024 * 1024) { return false; } byte[] loaded = loadFilePartial(filename, 0x1000); // nope return loaded.length != 0 && detectRomInner(loaded, (int) fileLength); } } public Gen2RomHandler(Random random) { super(random, null); } public Gen2RomHandler(Random random, PrintStream logStream) { super(random, logStream); } private static class RomEntry { private String name; private String romCode; private int version, nonJapanese; private String extraTableFile; private boolean isCrystal; private long expectedCRC32 = -1; private int crcInHeader = -1; private Map codeTweaks = new HashMap<>(); private List tmTexts = new ArrayList<>(); private Map entries = new HashMap<>(); private Map arrayEntries = new HashMap<>(); private Map strings = new HashMap<>(); private List staticPokemon = new ArrayList<>(); private int getValue(String key) { if (!entries.containsKey(key)) { entries.put(key, 0); } return entries.get(key); } private String getString(String key) { if (!strings.containsKey(key)) { strings.put(key, ""); } return strings.get(key); } } private static class TMTextEntry { private int number; private int offset; private String template; } private static List roms; static { loadROMInfo(); } private static void loadROMInfo() { roms = new ArrayList<>(); RomEntry current = null; try { Scanner sc = new Scanner(FileFunctions.openConfig("gen2_offsets.ini"), "UTF-8"); while (sc.hasNextLine()) { String q = sc.nextLine().trim(); if (q.contains("//")) { q = q.substring(0, q.indexOf("//")).trim(); } if (!q.isEmpty()) { if (q.startsWith("[") && q.endsWith("]")) { // New rom current = new RomEntry(); current.name = q.substring(1, q.length() - 1); roms.add(current); } else { String[] r = q.split("=", 2); if (r.length == 1) { System.err.println("invalid entry " + q); continue; } if (r[1].endsWith("\r\n")) { r[1] = r[1].substring(0, r[1].length() - 2); } r[1] = r[1].trim(); r[0] = r[0].trim(); if (r[0].equals("StaticPokemon{}")) { current.staticPokemon.add(parseStaticPokemon(r[1], false)); } else if (r[0].equals("StaticPokemonGameCorner{}")) { current.staticPokemon.add(parseStaticPokemon(r[1], true)); } else if (r[0].equals("TMText[]")) { if (r[1].startsWith("[") && r[1].endsWith("]")) { String[] parts = r[1].substring(1, r[1].length() - 1).split(",", 3); TMTextEntry tte = new TMTextEntry(); tte.number = parseRIInt(parts[0]); tte.offset = parseRIInt(parts[1]); tte.template = parts[2]; current.tmTexts.add(tte); } } else if (r[0].equals("Game")) { current.romCode = r[1]; } else if (r[0].equals("Version")) { current.version = parseRIInt(r[1]); } else if (r[0].equals("NonJapanese")) { current.nonJapanese = parseRIInt(r[1]); } else if (r[0].equals("Type")) { current.isCrystal = r[1].equalsIgnoreCase("Crystal"); } else if (r[0].equals("ExtraTableFile")) { current.extraTableFile = r[1]; } else if (r[0].equals("CRCInHeader")) { current.crcInHeader = parseRIInt(r[1]); } else if (r[0].equals("CRC32")) { current.expectedCRC32 = parseRILong("0x" + r[1]); } else if (r[0].endsWith("Tweak")) { current.codeTweaks.put(r[0], r[1]); } else if (r[0].equals("CopyFrom")) { for (RomEntry otherEntry : roms) { if (r[1].equalsIgnoreCase(otherEntry.name)) { // copy from here boolean cSP = (current.getValue("CopyStaticPokemon") == 1); boolean cTT = (current.getValue("CopyTMText") == 1); current.arrayEntries.putAll(otherEntry.arrayEntries); current.entries.putAll(otherEntry.entries); current.strings.putAll(otherEntry.strings); if (cSP) { current.staticPokemon.addAll(otherEntry.staticPokemon); current.entries.put("StaticPokemonSupport", 1); } else { current.entries.put("StaticPokemonSupport", 0); current.entries.remove("StaticPokemonOddEggOffset"); current.entries.remove("StaticPokemonOddEggDataSize"); } if (cTT) { current.tmTexts.addAll(otherEntry.tmTexts); } current.extraTableFile = otherEntry.extraTableFile; } } } else if (r[0].endsWith("Locator") || r[0].endsWith("Prefix")) { current.strings.put(r[0], r[1]); } else { if (r[1].startsWith("[") && r[1].endsWith("]")) { String[] offsets = r[1].substring(1, r[1].length() - 1).split(","); if (offsets.length == 1 && offsets[0].trim().isEmpty()) { current.arrayEntries.put(r[0], new int[0]); } else { int[] offs = new int[offsets.length]; int c = 0; for (String off : offsets) { offs[c++] = parseRIInt(off); } current.arrayEntries.put(r[0], offs); } } else { int offs = parseRIInt(r[1]); current.entries.put(r[0], offs); } } } } } sc.close(); } catch (FileNotFoundException e) { System.err.println("File not found!"); } } private static StaticPokemon parseStaticPokemon(String staticPokemonString, boolean isGameCorner) { StaticPokemon sp; if (isGameCorner) { sp = new StaticPokemonGameCorner(); } else { sp = new StaticPokemon(); } String pattern = "[A-z]+=\\[(0x[0-9a-fA-F]+,?\\s?)+]"; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(staticPokemonString); while (m.find()) { String[] segments = m.group().split("="); String[] romOffsets = segments[1].substring(1, segments[1].length() - 1).split(","); int[] offsets = new int [romOffsets.length]; for (int i = 0; i < offsets.length; i++) { offsets[i] = parseRIInt(romOffsets[i]); } switch (segments[0]) { case "Species": sp.speciesOffsets = offsets; break; case "Level": sp.levelOffsets = offsets; break; } } return sp; } private static int parseRIInt(String off) { int radix = 10; off = off.trim().toLowerCase(); if (off.startsWith("0x") || off.startsWith("&h")) { radix = 16; off = off.substring(2); } try { return Integer.parseInt(off, radix); } catch (NumberFormatException ex) { System.err.println("invalid base " + radix + "number " + off); return 0; } } private static long parseRILong(String off) { int radix = 10; off = off.trim().toLowerCase(); if (off.startsWith("0x") || off.startsWith("&h")) { radix = 16; off = off.substring(2); } try { return Long.parseLong(off, radix); } catch (NumberFormatException ex) { System.err.println("invalid base " + radix + "number " + off); return 0; } } // This ROM's data private Pokemon[] pokes; private List pokemonList; private RomEntry romEntry; private Move[] moves; private boolean havePatchedFleeing; private String[] itemNames; private List itemOffs; private String[][] mapNames; private String[] landmarkNames; private boolean isVietCrystal; private ItemList allowedItems, nonBadItems; private long actualCRC32; private boolean effectivenessUpdated; @Override public boolean detectRom(byte[] rom) { return detectRomInner(rom, rom.length); } private static boolean detectRomInner(byte[] rom, int romSize) { // size check return romSize >= GBConstants.minRomSize && romSize <= GBConstants.maxRomSize && checkRomEntry(rom) != null; } @Override public void loadedRom() { romEntry = checkRomEntry(this.rom); clearTextTables(); readTextTable("gameboy_jpn"); if (romEntry.extraTableFile != null && !romEntry.extraTableFile.equalsIgnoreCase("none")) { readTextTable(romEntry.extraTableFile); } // VietCrystal override if (romEntry.name.equals("Crystal (J)") && rom[Gen2Constants.vietCrystalCheckOffset] == Gen2Constants.vietCrystalCheckValue) { readTextTable("vietcrystal"); isVietCrystal = true; } else { isVietCrystal = false; } havePatchedFleeing = false; loadPokemonStats(); pokemonList = Arrays.asList(pokes); loadMoves(); loadLandmarkNames(); preprocessMaps(); loadItemNames(); allowedItems = Gen2Constants.allowedItems.copy(); nonBadItems = Gen2Constants.nonBadItems.copy(); actualCRC32 = FileFunctions.getCRC32(rom); // VietCrystal: exclude Burn Heal, Calcium, and Elixir // crashes your game if used, glitches out your inventory if carried if (isVietCrystal) { allowedItems.banSingles(Gen2Items.burnHeal, Gen2Items.calcium, Gen2Items.elixer); } } private static RomEntry checkRomEntry(byte[] rom) { int version = rom[GBConstants.versionOffset] & 0xFF; int nonjap = rom[GBConstants.jpFlagOffset] & 0xFF; // Check for specific CRC first int crcInHeader = ((rom[GBConstants.crcOffset] & 0xFF) << 8) | (rom[GBConstants.crcOffset + 1] & 0xFF); for (RomEntry re : roms) { if (romCode(rom, re.romCode) && re.version == version && re.nonJapanese == nonjap && re.crcInHeader == crcInHeader) { return re; } } // Now check for non-specific-CRC entries for (RomEntry re : roms) { if (romCode(rom, re.romCode) && re.version == version && re.nonJapanese == nonjap && re.crcInHeader == -1) { return re; } } // Not found return null; } @Override public void savingRom() { savePokemonStats(); saveMoves(); } private void loadPokemonStats() { pokes = new Pokemon[Gen2Constants.pokemonCount + 1]; // Fetch our names String[] pokeNames = readPokemonNames(); int offs = romEntry.getValue("PokemonStatsOffset"); // Get base stats for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { pokes[i] = new Pokemon(); pokes[i].number = i; loadBasicPokeStats(pokes[i], offs + (i - 1) * Gen2Constants.baseStatsEntrySize); // Name? pokes[i].name = pokeNames[i]; } // Get evolutions populateEvolutions(); } private void savePokemonStats() { // Write pokemon names int offs = romEntry.getValue("PokemonNamesOffset"); int len = romEntry.getValue("PokemonNamesLength"); for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { int stringOffset = offs + (i - 1) * len; writeFixedLengthString(pokes[i].name, stringOffset, len); } // Write pokemon stats int offs2 = romEntry.getValue("PokemonStatsOffset"); for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { saveBasicPokeStats(pokes[i], offs2 + (i - 1) * Gen2Constants.baseStatsEntrySize); } // Write evolutions writeEvosAndMovesLearnt(true, null); } private String[] readMoveNames() { int offset = romEntry.getValue("MoveNamesOffset"); String[] moveNames = new String[Gen2Constants.moveCount + 1]; for (int i = 1; i <= Gen2Constants.moveCount; i++) { moveNames[i] = readVariableLengthString(offset, false); offset += lengthOfStringAt(offset, false) + 1; } return moveNames; } private void loadMoves() { moves = new Move[Gen2Constants.moveCount + 1]; String[] moveNames = readMoveNames(); int offs = romEntry.getValue("MoveDataOffset"); for (int i = 1; i <= Gen2Constants.moveCount; i++) { moves[i] = new Move(); moves[i].name = moveNames[i]; moves[i].number = i; moves[i].internalId = i; moves[i].effectIndex = rom[offs + (i - 1) * 7 + 1] & 0xFF; moves[i].hitratio = ((rom[offs + (i - 1) * 7 + 4] & 0xFF)) / 255.0 * 100; moves[i].power = rom[offs + (i - 1) * 7 + 2] & 0xFF; moves[i].pp = rom[offs + (i - 1) * 7 + 5] & 0xFF; moves[i].type = Gen2Constants.typeTable[rom[offs + (i - 1) * 7 + 3]]; moves[i].category = GBConstants.physicalTypes.contains(moves[i].type) ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL; if (moves[i].power == 0 && !GlobalConstants.noPowerNonStatusMoves.contains(i)) { moves[i].category = MoveCategory.STATUS; } if (i == Moves.swift) { perfectAccuracy = (int)moves[i].hitratio; } if (GlobalConstants.normalMultihitMoves.contains(i)) { moves[i].hitCount = 3; } else if (GlobalConstants.doubleHitMoves.contains(i)) { moves[i].hitCount = 2; } else if (i == Moves.tripleKick) { moves[i].hitCount = 2.71; // this assumes the first hit lands } // Values taken from effect_priorities.asm from the Gen 2 disassemblies. if (moves[i].effectIndex == Gen2Constants.priorityHitEffectIndex) { moves[i].priority = 2; } else if (moves[i].effectIndex == Gen2Constants.protectEffectIndex || moves[i].effectIndex == Gen2Constants.endureEffectIndex) { moves[i].priority = 3; } else if (moves[i].effectIndex == Gen2Constants.forceSwitchEffectIndex || moves[i].effectIndex == Gen2Constants.counterEffectIndex || moves[i].effectIndex == Gen2Constants.mirrorCoatEffectIndex) { moves[i].priority = 0; } else { moves[i].priority = 1; } double secondaryEffectChance = ((rom[offs + (i - 1) * 7 + 6] & 0xFF)) / 255.0 * 100; loadStatChangesFromEffect(moves[i], secondaryEffectChance); loadStatusFromEffect(moves[i], secondaryEffectChance); loadMiscMoveInfoFromEffect(moves[i], secondaryEffectChance); } } private void loadStatChangesFromEffect(Move move, double secondaryEffectChance) { switch (move.effectIndex) { case Gen2Constants.noDamageAtkPlusOneEffect: case Gen2Constants.damageUserAtkPlusOneEffect: move.statChanges[0].type = StatChangeType.ATTACK; move.statChanges[0].stages = 1; break; case Gen2Constants.noDamageDefPlusOneEffect: case Gen2Constants.damageUserDefPlusOneEffect: case Gen2Constants.defenseCurlEffect: move.statChanges[0].type = StatChangeType.DEFENSE; move.statChanges[0].stages = 1; break; case Gen2Constants.noDamageSpAtkPlusOneEffect: move.statChanges[0].type = StatChangeType.SPECIAL_ATTACK; move.statChanges[0].stages = 1; break; case Gen2Constants.noDamageEvasionPlusOneEffect: move.statChanges[0].type = StatChangeType.EVASION; move.statChanges[0].stages = 1; break; case Gen2Constants.noDamageAtkMinusOneEffect: case Gen2Constants.damageAtkMinusOneEffect: move.statChanges[0].type = StatChangeType.ATTACK; move.statChanges[0].stages = -1; break; case Gen2Constants.noDamageDefMinusOneEffect: case Gen2Constants.damageDefMinusOneEffect: move.statChanges[0].type = StatChangeType.DEFENSE; move.statChanges[0].stages = -1; break; case Gen2Constants.noDamageSpeMinusOneEffect: case Gen2Constants.damageSpeMinusOneEffect: move.statChanges[0].type = StatChangeType.SPEED; move.statChanges[0].stages = -1; break; case Gen2Constants.noDamageAccuracyMinusOneEffect: case Gen2Constants.damageAccuracyMinusOneEffect: move.statChanges[0].type = StatChangeType.ACCURACY; move.statChanges[0].stages = -1; break; case Gen2Constants.noDamageEvasionMinusOneEffect: move.statChanges[0].type = StatChangeType.EVASION; move.statChanges[0].stages = -1; break; case Gen2Constants.noDamageAtkPlusTwoEffect: case Gen2Constants.swaggerEffect: move.statChanges[0].type = StatChangeType.ATTACK; move.statChanges[0].stages = 2; break; case Gen2Constants.noDamageDefPlusTwoEffect: move.statChanges[0].type = StatChangeType.DEFENSE; move.statChanges[0].stages = 2; break; case Gen2Constants.noDamageSpePlusTwoEffect: move.statChanges[0].type = StatChangeType.SPEED; move.statChanges[0].stages = 2; break; case Gen2Constants.noDamageSpDefPlusTwoEffect: move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; move.statChanges[0].stages = 2; break; case Gen2Constants.noDamageAtkMinusTwoEffect: move.statChanges[0].type = StatChangeType.ATTACK; move.statChanges[0].stages = -2; break; case Gen2Constants.noDamageDefMinusTwoEffect: move.statChanges[0].type = StatChangeType.DEFENSE; move.statChanges[0].stages = -2; break; case Gen2Constants.noDamageSpeMinusTwoEffect: move.statChanges[0].type = StatChangeType.SPEED; move.statChanges[0].stages = -2; break; case Gen2Constants.noDamageSpDefMinusTwoEffect: move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; move.statChanges[0].stages = -2; break; case Gen2Constants.damageSpDefMinusOneEffect: move.statChanges[0].type = StatChangeType.SPECIAL_DEFENSE; move.statChanges[0].stages = -1; break; case Gen2Constants.damageUserAllPlusOneEffect: move.statChanges[0].type = StatChangeType.ALL; move.statChanges[0].stages = 1; break; default: // Move does not have a stat-changing effect return; } switch (move.effectIndex) { case Gen2Constants.noDamageAtkPlusOneEffect: case Gen2Constants.noDamageDefPlusOneEffect: case Gen2Constants.noDamageSpAtkPlusOneEffect: case Gen2Constants.noDamageEvasionPlusOneEffect: case Gen2Constants.noDamageAtkMinusOneEffect: case Gen2Constants.noDamageDefMinusOneEffect: case Gen2Constants.noDamageSpeMinusOneEffect: case Gen2Constants.noDamageAccuracyMinusOneEffect: case Gen2Constants.noDamageEvasionMinusOneEffect: case Gen2Constants.noDamageAtkPlusTwoEffect: case Gen2Constants.noDamageDefPlusTwoEffect: case Gen2Constants.noDamageSpePlusTwoEffect: case Gen2Constants.noDamageSpDefPlusTwoEffect: case Gen2Constants.noDamageAtkMinusTwoEffect: case Gen2Constants.noDamageDefMinusTwoEffect: case Gen2Constants.noDamageSpeMinusTwoEffect: case Gen2Constants.noDamageSpDefMinusTwoEffect: case Gen2Constants.swaggerEffect: case Gen2Constants.defenseCurlEffect: if (move.statChanges[0].stages < 0 || move.effectIndex == Gen2Constants.swaggerEffect) { move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_TARGET; } else { move.statChangeMoveType = StatChangeMoveType.NO_DAMAGE_USER; } break; case Gen2Constants.damageAtkMinusOneEffect: case Gen2Constants.damageDefMinusOneEffect: case Gen2Constants.damageSpeMinusOneEffect: case Gen2Constants.damageSpDefMinusOneEffect: case Gen2Constants.damageAccuracyMinusOneEffect: move.statChangeMoveType = StatChangeMoveType.DAMAGE_TARGET; break; case Gen2Constants.damageUserDefPlusOneEffect: case Gen2Constants.damageUserAtkPlusOneEffect: case Gen2Constants.damageUserAllPlusOneEffect: move.statChangeMoveType = StatChangeMoveType.DAMAGE_USER; break; } if (move.statChangeMoveType == StatChangeMoveType.DAMAGE_TARGET || move.statChangeMoveType == StatChangeMoveType.DAMAGE_USER) { for (int i = 0; i < move.statChanges.length; i++) { if (move.statChanges[i].type != StatChangeType.NONE) { move.statChanges[i].percentChance = secondaryEffectChance; if (move.statChanges[i].percentChance == 0.0) { move.statChanges[i].percentChance = 100.0; } } } } } private void loadStatusFromEffect(Move move, double secondaryEffectChance) { switch (move.effectIndex) { case Gen2Constants.noDamageSleepEffect: case Gen2Constants.toxicEffect: case Gen2Constants.noDamageConfusionEffect: case Gen2Constants.noDamagePoisonEffect: case Gen2Constants.noDamageParalyzeEffect: case Gen2Constants.swaggerEffect: move.statusMoveType = StatusMoveType.NO_DAMAGE; break; case Gen2Constants.damagePoisonEffect: case Gen2Constants.damageBurnEffect: case Gen2Constants.damageFreezeEffect: case Gen2Constants.damageParalyzeEffect: case Gen2Constants.damageConfusionEffect: case Gen2Constants.twineedleEffect: case Gen2Constants.damageBurnAndThawUserEffect: case Gen2Constants.thunderEffect: move.statusMoveType = StatusMoveType.DAMAGE; break; default: // Move does not have a status effect return; } switch (move.effectIndex) { case Gen2Constants.noDamageSleepEffect: move.statusType = StatusType.SLEEP; break; case Gen2Constants.damagePoisonEffect: case Gen2Constants.noDamagePoisonEffect: case Gen2Constants.twineedleEffect: move.statusType = StatusType.POISON; break; case Gen2Constants.damageBurnEffect: case Gen2Constants.damageBurnAndThawUserEffect: move.statusType = StatusType.BURN; break; case Gen2Constants.damageFreezeEffect: move.statusType = StatusType.FREEZE; break; case Gen2Constants.damageParalyzeEffect: case Gen2Constants.noDamageParalyzeEffect: case Gen2Constants.thunderEffect: move.statusType = StatusType.PARALYZE; break; case Gen2Constants.toxicEffect: move.statusType = StatusType.TOXIC_POISON; break; case Gen2Constants.noDamageConfusionEffect: case Gen2Constants.damageConfusionEffect: case Gen2Constants.swaggerEffect: move.statusType = StatusType.CONFUSION; break; } if (move.statusMoveType == StatusMoveType.DAMAGE) { move.statusPercentChance = secondaryEffectChance; if (move.statusPercentChance == 0.0) { move.statusPercentChance = 100.0; } } } private void loadMiscMoveInfoFromEffect(Move move, double secondaryEffectChance) { switch (move.effectIndex) { case Gen2Constants.flinchEffect: case Gen2Constants.snoreEffect: case Gen2Constants.twisterEffect: case Gen2Constants.stompEffect: move.flinchPercentChance = secondaryEffectChance; break; case Gen2Constants.damageAbsorbEffect: case Gen2Constants.dreamEaterEffect: move.absorbPercent = 50; break; case Gen2Constants.damageRecoilEffect: move.recoilPercent = 25; break; case Gen2Constants.flailAndReversalEffect: case Gen2Constants.futureSightEffect: move.criticalChance = CriticalChance.NONE; break; case Gen2Constants.bindingEffect: case Gen2Constants.trappingEffect: move.isTrapMove = true; break; case Gen2Constants.razorWindEffect: case Gen2Constants.skyAttackEffect: case Gen2Constants.skullBashEffect: case Gen2Constants.solarbeamEffect: case Gen2Constants.semiInvulnerableEffect: move.isChargeMove = true; break; case Gen2Constants.hyperBeamEffect: move.isRechargeMove = true; break; } if (Gen2Constants.increasedCritMoves.contains(move.number)) { move.criticalChance = CriticalChance.INCREASED; } } private void saveMoves() { int offs = romEntry.getValue("MoveDataOffset"); for (int i = 1; i <= 251; i++) { rom[offs + (i - 1) * 7 + 1] = (byte) moves[i].effectIndex; rom[offs + (i - 1) * 7 + 2] = (byte) moves[i].power; rom[offs + (i - 1) * 7 + 3] = Gen2Constants.typeToByte(moves[i].type); int hitratio = (int) Math.round(moves[i].hitratio * 2.55); if (hitratio < 0) { hitratio = 0; } if (hitratio > 255) { hitratio = 255; } rom[offs + (i - 1) * 7 + 4] = (byte) hitratio; rom[offs + (i - 1) * 7 + 5] = (byte) moves[i].pp; } } public List getMoves() { return Arrays.asList(moves); } private void loadBasicPokeStats(Pokemon pkmn, int offset) { pkmn.hp = rom[offset + Gen2Constants.bsHPOffset] & 0xFF; pkmn.attack = rom[offset + Gen2Constants.bsAttackOffset] & 0xFF; pkmn.defense = rom[offset + Gen2Constants.bsDefenseOffset] & 0xFF; pkmn.speed = rom[offset + Gen2Constants.bsSpeedOffset] & 0xFF; pkmn.spatk = rom[offset + Gen2Constants.bsSpAtkOffset] & 0xFF; pkmn.spdef = rom[offset + Gen2Constants.bsSpDefOffset] & 0xFF; // Type pkmn.primaryType = Gen2Constants.typeTable[rom[offset + Gen2Constants.bsPrimaryTypeOffset] & 0xFF]; pkmn.secondaryType = Gen2Constants.typeTable[rom[offset + Gen2Constants.bsSecondaryTypeOffset] & 0xFF]; // Only one type? if (pkmn.secondaryType == pkmn.primaryType) { pkmn.secondaryType = null; } pkmn.catchRate = rom[offset + Gen2Constants.bsCatchRateOffset] & 0xFF; pkmn.guaranteedHeldItem = -1; pkmn.commonHeldItem = rom[offset + Gen2Constants.bsCommonHeldItemOffset] & 0xFF; pkmn.rareHeldItem = rom[offset + Gen2Constants.bsRareHeldItemOffset] & 0xFF; pkmn.darkGrassHeldItem = -1; pkmn.growthCurve = ExpCurve.fromByte(rom[offset + Gen2Constants.bsGrowthCurveOffset]); pkmn.picDimensions = rom[offset + Gen2Constants.bsPicDimensionsOffset] & 0xFF; } private void saveBasicPokeStats(Pokemon pkmn, int offset) { rom[offset + Gen2Constants.bsHPOffset] = (byte) pkmn.hp; rom[offset + Gen2Constants.bsAttackOffset] = (byte) pkmn.attack; rom[offset + Gen2Constants.bsDefenseOffset] = (byte) pkmn.defense; rom[offset + Gen2Constants.bsSpeedOffset] = (byte) pkmn.speed; rom[offset + Gen2Constants.bsSpAtkOffset] = (byte) pkmn.spatk; rom[offset + Gen2Constants.bsSpDefOffset] = (byte) pkmn.spdef; rom[offset + Gen2Constants.bsPrimaryTypeOffset] = Gen2Constants.typeToByte(pkmn.primaryType); if (pkmn.secondaryType == null) { rom[offset + Gen2Constants.bsSecondaryTypeOffset] = rom[offset + Gen2Constants.bsPrimaryTypeOffset]; } else { rom[offset + Gen2Constants.bsSecondaryTypeOffset] = Gen2Constants.typeToByte(pkmn.secondaryType); } rom[offset + Gen2Constants.bsCatchRateOffset] = (byte) pkmn.catchRate; rom[offset + Gen2Constants.bsCommonHeldItemOffset] = (byte) pkmn.commonHeldItem; rom[offset + Gen2Constants.bsRareHeldItemOffset] = (byte) pkmn.rareHeldItem; rom[offset + Gen2Constants.bsGrowthCurveOffset] = pkmn.growthCurve.toByte(); } private String[] readPokemonNames() { int offs = romEntry.getValue("PokemonNamesOffset"); int len = romEntry.getValue("PokemonNamesLength"); String[] names = new String[Gen2Constants.pokemonCount + 1]; for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { names[i] = readFixedLengthString(offs + (i - 1) * len, len); } return names; } @Override public List getStarters() { // Get the starters List starters = new ArrayList<>(); starters.add(pokes[rom[romEntry.arrayEntries.get("StarterOffsets1")[0]] & 0xFF]); starters.add(pokes[rom[romEntry.arrayEntries.get("StarterOffsets2")[0]] & 0xFF]); starters.add(pokes[rom[romEntry.arrayEntries.get("StarterOffsets3")[0]] & 0xFF]); return starters; } @Override public boolean setStarters(List newStarters) { if (newStarters.size() != 3) { return false; } // Actually write for (int i = 0; i < 3; i++) { byte starter = (byte) newStarters.get(i).number; int[] offsets = romEntry.arrayEntries.get("StarterOffsets" + (i + 1)); for (int offset : offsets) { rom[offset] = starter; } } // Attempt to replace text if (romEntry.getValue("CanChangeStarterText") > 0) { int[] starterTextOffsets = romEntry.arrayEntries.get("StarterTextOffsets"); for (int i = 0; i < 3 && i < starterTextOffsets.length; i++) { writeVariableLengthString(String.format("%s?\\e", newStarters.get(i).name), starterTextOffsets[i], true); } } return true; } @Override public boolean hasStarterAltFormes() { return false; } @Override public int starterCount() { return 3; } @Override public Map getUpdatedPokemonStats(int generation) { return GlobalConstants.getStatChanges(generation); } @Override public boolean supportsStarterHeldItems() { return true; } @Override public List getStarterHeldItems() { List sHeldItems = new ArrayList<>(); int[] shiOffsets = romEntry.arrayEntries.get("StarterHeldItems"); for (int offset : shiOffsets) { sHeldItems.add(rom[offset] & 0xFF); } return sHeldItems; } @Override public void setStarterHeldItems(List items) { int[] shiOffsets = romEntry.arrayEntries.get("StarterHeldItems"); if (items.size() != shiOffsets.length) { return; } Iterator sHeldItems = items.iterator(); for (int offset : shiOffsets) { rom[offset] = sHeldItems.next().byteValue(); } } @Override public List getEncounters(boolean useTimeOfDay) { int offset = romEntry.getValue("WildPokemonOffset"); List areas = new ArrayList<>(); offset = readLandEncounters(offset, areas, useTimeOfDay); // Johto offset = readSeaEncounters(offset, areas); // Johto offset = readLandEncounters(offset, areas, useTimeOfDay); // Kanto offset = readSeaEncounters(offset, areas); // Kanto offset = readLandEncounters(offset, areas, useTimeOfDay); // Specials offset = readSeaEncounters(offset, areas); // Specials // Fishing Data offset = romEntry.getValue("FishingWildsOffset"); int rootOffset = offset; for (int k = 0; k < Gen2Constants.fishingGroupCount; k++) { EncounterSet es = new EncounterSet(); es.displayName = "Fishing Group " + (k + 1); for (int i = 0; i < Gen2Constants.pokesPerFishingGroup; i++) { offset++; int pokeNum = rom[offset++] & 0xFF; int level = rom[offset++] & 0xFF; if (pokeNum == 0) { if (!useTimeOfDay) { // read the encounter they put here for DAY int specialOffset = rootOffset + Gen2Constants.fishingGroupEntryLength * Gen2Constants.pokesPerFishingGroup * Gen2Constants.fishingGroupCount + level * 4 + 2; Encounter enc = new Encounter(); enc.pokemon = pokes[rom[specialOffset] & 0xFF]; enc.level = rom[specialOffset + 1] & 0xFF; es.encounters.add(enc); } // else will be handled by code below } else { Encounter enc = new Encounter(); enc.pokemon = pokes[pokeNum]; enc.level = level; es.encounters.add(enc); } } areas.add(es); } if (useTimeOfDay) { for (int k = 0; k < Gen2Constants.timeSpecificFishingGroupCount; k++) { EncounterSet es = new EncounterSet(); es.displayName = "Time-Specific Fishing " + (k + 1); for (int i = 0; i < Gen2Constants.pokesPerTSFishingGroup; i++) { int pokeNum = rom[offset++] & 0xFF; int level = rom[offset++] & 0xFF; Encounter enc = new Encounter(); enc.pokemon = pokes[pokeNum]; enc.level = level; es.encounters.add(enc); } areas.add(es); } } // Headbutt Data offset = romEntry.getValue("HeadbuttWildsOffset"); int limit = romEntry.getValue("HeadbuttTableSize"); for (int i = 0; i < limit; i++) { EncounterSet es = new EncounterSet(); es.displayName = "Headbutt Trees Set " + (i + 1); while ((rom[offset] & 0xFF) != 0xFF) { offset++; int pokeNum = rom[offset++] & 0xFF; int level = rom[offset++] & 0xFF; Encounter enc = new Encounter(); enc.pokemon = pokes[pokeNum]; enc.level = level; es.encounters.add(enc); } offset++; areas.add(es); } // Bug Catching Contest Data offset = romEntry.getValue("BCCWildsOffset"); EncounterSet bccES = new EncounterSet(); bccES.displayName = "Bug Catching Contest"; while ((rom[offset] & 0xFF) != 0xFF) { offset++; Encounter enc = new Encounter(); enc.pokemon = pokes[rom[offset++] & 0xFF]; enc.level = rom[offset++] & 0xFF; enc.maxLevel = rom[offset++] & 0xFF; bccES.encounters.add(enc); } // Unown is banned for Bug Catching Contest (5/8/2016) bccES.bannedPokemon.add(pokes[Species.unown]); areas.add(bccES); return areas; } private int readLandEncounters(int offset, List areas, boolean useTimeOfDay) { String[] todNames = new String[] { "Morning", "Day", "Night" }; while ((rom[offset] & 0xFF) != 0xFF) { int mapBank = rom[offset] & 0xFF; int mapNumber = rom[offset + 1] & 0xFF; String mapName = mapNames[mapBank][mapNumber]; if (useTimeOfDay) { for (int i = 0; i < 3; i++) { EncounterSet encset = new EncounterSet(); encset.rate = rom[offset + 2 + i] & 0xFF; encset.displayName = mapName + " Grass/Cave (" + todNames[i] + ")"; for (int j = 0; j < Gen2Constants.landEncounterSlots; j++) { Encounter enc = new Encounter(); enc.level = rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2)] & 0xFF; enc.maxLevel = 0; enc.pokemon = pokes[rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2) + 1] & 0xFF]; encset.encounters.add(enc); } areas.add(encset); } } else { // Use Day only EncounterSet encset = new EncounterSet(); encset.rate = rom[offset + 3] & 0xFF; encset.displayName = mapName + " Grass/Cave"; for (int j = 0; j < Gen2Constants.landEncounterSlots; j++) { Encounter enc = new Encounter(); enc.level = rom[offset + 5 + Gen2Constants.landEncounterSlots * 2 + (j * 2)] & 0xFF; enc.maxLevel = 0; enc.pokemon = pokes[rom[offset + 5 + Gen2Constants.landEncounterSlots * 2 + (j * 2) + 1] & 0xFF]; encset.encounters.add(enc); } areas.add(encset); } offset += 5 + 6 * Gen2Constants.landEncounterSlots; } return offset + 1; } private int readSeaEncounters(int offset, List areas) { while ((rom[offset] & 0xFF) != 0xFF) { int mapBank = rom[offset] & 0xFF; int mapNumber = rom[offset + 1] & 0xFF; String mapName = mapNames[mapBank][mapNumber]; EncounterSet encset = new EncounterSet(); encset.rate = rom[offset + 2] & 0xFF; encset.displayName = mapName + " Surfing"; for (int j = 0; j < Gen2Constants.seaEncounterSlots; j++) { Encounter enc = new Encounter(); enc.level = rom[offset + 3 + (j * 2)] & 0xFF; enc.maxLevel = 0; enc.pokemon = pokes[rom[offset + 3 + (j * 2) + 1] & 0xFF]; encset.encounters.add(enc); } areas.add(encset); offset += 3 + Gen2Constants.seaEncounterSlots * 2; } return offset + 1; } @Override public void setEncounters(boolean useTimeOfDay, List encounters) { if (!havePatchedFleeing) { patchFleeing(); } int offset = romEntry.getValue("WildPokemonOffset"); Iterator areas = encounters.iterator(); offset = writeLandEncounters(offset, areas, useTimeOfDay); // Johto offset = writeSeaEncounters(offset, areas); // Johto offset = writeLandEncounters(offset, areas, useTimeOfDay); // Kanto offset = writeSeaEncounters(offset, areas); // Kanto offset = writeLandEncounters(offset, areas, useTimeOfDay); // Specials offset = writeSeaEncounters(offset, areas); // Specials // Fishing Data offset = romEntry.getValue("FishingWildsOffset"); for (int k = 0; k < Gen2Constants.fishingGroupCount; k++) { EncounterSet es = areas.next(); Iterator encs = es.encounters.iterator(); for (int i = 0; i < Gen2Constants.pokesPerFishingGroup; i++) { offset++; if (rom[offset] == 0) { if (!useTimeOfDay) { // overwrite with a static encounter Encounter enc = encs.next(); rom[offset++] = (byte) enc.pokemon.number; rom[offset++] = (byte) enc.level; } else { // else handle below offset += 2; } } else { Encounter enc = encs.next(); rom[offset++] = (byte) enc.pokemon.number; rom[offset++] = (byte) enc.level; } } } if (useTimeOfDay) { for (int k = 0; k < Gen2Constants.timeSpecificFishingGroupCount; k++) { EncounterSet es = areas.next(); Iterator encs = es.encounters.iterator(); for (int i = 0; i < Gen2Constants.pokesPerTSFishingGroup; i++) { Encounter enc = encs.next(); rom[offset++] = (byte) enc.pokemon.number; rom[offset++] = (byte) enc.level; } } } // Headbutt Data offset = romEntry.getValue("HeadbuttWildsOffset"); int limit = romEntry.getValue("HeadbuttTableSize"); for (int i = 0; i < limit; i++) { EncounterSet es = areas.next(); Iterator encs = es.encounters.iterator(); while ((rom[offset] & 0xFF) != 0xFF) { Encounter enc = encs.next(); offset++; rom[offset++] = (byte) enc.pokemon.number; rom[offset++] = (byte) enc.level; } offset++; } // Bug Catching Contest Data offset = romEntry.getValue("BCCWildsOffset"); EncounterSet bccES = areas.next(); Iterator bccEncs = bccES.encounters.iterator(); while ((rom[offset] & 0xFF) != 0xFF) { offset++; Encounter enc = bccEncs.next(); rom[offset++] = (byte) enc.pokemon.number; rom[offset++] = (byte) enc.level; rom[offset++] = (byte) enc.maxLevel; } } private int writeLandEncounters(int offset, Iterator areas, boolean useTimeOfDay) { while ((rom[offset] & 0xFF) != 0xFF) { if (useTimeOfDay) { for (int i = 0; i < 3; i++) { EncounterSet encset = areas.next(); Iterator encountersHere = encset.encounters.iterator(); for (int j = 0; j < Gen2Constants.landEncounterSlots; j++) { Encounter enc = encountersHere.next(); rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2)] = (byte) enc.level; rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2) + 1] = (byte) enc.pokemon.number; } } } else { // Write the set to all 3 equally EncounterSet encset = areas.next(); for (int i = 0; i < 3; i++) { Iterator encountersHere = encset.encounters.iterator(); for (int j = 0; j < Gen2Constants.landEncounterSlots; j++) { Encounter enc = encountersHere.next(); rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2)] = (byte) enc.level; rom[offset + 5 + (i * Gen2Constants.landEncounterSlots * 2) + (j * 2) + 1] = (byte) enc.pokemon.number; } } } offset += 5 + 6 * Gen2Constants.landEncounterSlots; } return offset + 1; } private int writeSeaEncounters(int offset, Iterator areas) { while ((rom[offset] & 0xFF) != 0xFF) { EncounterSet encset = areas.next(); Iterator encountersHere = encset.encounters.iterator(); for (int j = 0; j < Gen2Constants.seaEncounterSlots; j++) { Encounter enc = encountersHere.next(); rom[offset + 3 + (j * 2)] = (byte) enc.level; rom[offset + 3 + (j * 2) + 1] = (byte) enc.pokemon.number; } offset += 3 + Gen2Constants.seaEncounterSlots * 2; } return offset + 1; } @Override public List getTrainers() { int traineroffset = romEntry.getValue("TrainerDataTableOffset"); int traineramount = romEntry.getValue("TrainerClassAmount"); int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts"); int[] pointers = new int[traineramount]; for (int i = 0; i < traineramount; i++) { int pointer = readWord(traineroffset + i * 2); pointers[i] = calculateOffset(bankOf(traineroffset), pointer); } List tcnames = this.getTrainerClassNames(); List allTrainers = new ArrayList<>(); int index = 0; for (int i = 0; i < traineramount; i++) { int offs = pointers[i]; int limit = trainerclasslimits[i]; for (int trnum = 0; trnum < limit; trnum++) { index++; Trainer tr = new Trainer(); tr.offset = offs; tr.index = index; tr.trainerclass = i; String name = readVariableLengthString(offs, false); tr.name = name; tr.fullDisplayName = tcnames.get(i) + " " + name; offs += lengthOfStringAt(offs, false) + 1; int dataType = rom[offs] & 0xFF; tr.poketype = dataType; offs++; while ((rom[offs] & 0xFF) != 0xFF) { TrainerPokemon tp = new TrainerPokemon(); tp.level = rom[offs] & 0xFF; tp.pokemon = pokes[rom[offs + 1] & 0xFF]; offs += 2; if ((dataType & 2) == 2) { tp.heldItem = rom[offs] & 0xFF; offs++; } if ((dataType & 1) == 1) { for (int move = 0; move < 4; move++) { tp.moves[move] = rom[offs + move] & 0xFF; } offs += 4; } tr.pokemon.add(tp); } allTrainers.add(tr); offs++; } } Gen2Constants.universalTrainerTags(allTrainers); if (romEntry.isCrystal) { Gen2Constants.crystalTags(allTrainers); } else { Gen2Constants.goldSilverTags(allTrainers); } return allTrainers; } @Override public List getMainPlaythroughTrainers() { return new ArrayList<>(); // Not implemented } @Override public List getEliteFourTrainers(boolean isChallengeMode) { return new ArrayList<>(); } @Override public void setTrainers(List trainerData, boolean doubleBattleMode) { int traineroffset = romEntry.getValue("TrainerDataTableOffset"); int traineramount = romEntry.getValue("TrainerClassAmount"); int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts"); int[] pointers = new int[traineramount]; for (int i = 0; i < traineramount; i++) { int pointer = readWord(traineroffset + i * 2); pointers[i] = calculateOffset(bankOf(traineroffset), pointer); } // Get current movesets in case we need to reset them for certain // trainer mons. Map> movesets = this.getMovesLearnt(); Iterator allTrainers = trainerData.iterator(); for (int i = 0; i < traineramount; i++) { int offs = pointers[i]; int limit = trainerclasslimits[i]; for (int trnum = 0; trnum < limit; trnum++) { Trainer tr = allTrainers.next(); if (tr.trainerclass != i) { System.err.println("Trainer mismatch: " + tr.name); } // Write their name int trnamelen = internalStringLength(tr.name); writeFixedLengthString(tr.name, offs, trnamelen + 1); offs += trnamelen + 1; // Write out new trainer data rom[offs++] = (byte) tr.poketype; Iterator tPokes = tr.pokemon.iterator(); for (int tpnum = 0; tpnum < tr.pokemon.size(); tpnum++) { TrainerPokemon tp = tPokes.next(); rom[offs] = (byte) tp.level; rom[offs + 1] = (byte) tp.pokemon.number; offs += 2; if (tr.pokemonHaveItems()) { rom[offs] = (byte) tp.heldItem; offs++; } if (tr.pokemonHaveCustomMoves()) { if (tp.resetMoves) { int[] pokeMoves = RomFunctions.getMovesAtLevel(tp.pokemon.number, movesets, tp.level); for (int m = 0; m < 4; m++) { rom[offs + m] = (byte) pokeMoves[m]; } } else { rom[offs] = (byte) tp.moves[0]; rom[offs + 1] = (byte) tp.moves[1]; rom[offs + 2] = (byte) tp.moves[2]; rom[offs + 3] = (byte) tp.moves[3]; } offs += 4; } } rom[offs] = (byte) 0xFF; offs++; } } } @Override public List 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; } @Override public Map> getMovesLearnt() { Map> movesets = new TreeMap<>(); int pointersOffset = romEntry.getValue("PokemonMovesetsTableOffset"); for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { int pointer = readWord(pointersOffset + (i - 1) * 2); int realPointer = calculateOffset(bankOf(pointersOffset), pointer); Pokemon pkmn = pokes[i]; // Skip over evolution data while (rom[realPointer] != 0) { if (rom[realPointer] == 5) { realPointer += 4; } else { realPointer += 3; } } List ourMoves = new ArrayList<>(); realPointer++; while (rom[realPointer] != 0) { MoveLearnt learnt = new MoveLearnt(); learnt.level = rom[realPointer] & 0xFF; learnt.move = rom[realPointer + 1] & 0xFF; ourMoves.add(learnt); realPointer += 2; } movesets.put(pkmn.number, ourMoves); } return movesets; } @Override public void setMovesLearnt(Map> movesets) { writeEvosAndMovesLearnt(false, movesets); } @Override public List getMovesBannedFromLevelup() { return Gen2Constants.bannedLevelupMoves; } @Override public Map> getEggMoves() { Map> eggMoves = new TreeMap<>(); int pointersOffset = romEntry.getValue("EggMovesTableOffset"); int baseOffset = (pointersOffset / 0x1000) * 0x1000; for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { int eggMovePointer = FileFunctions.read2ByteInt(rom, pointersOffset + ((i - 1) * 2)); int eggMoveOffset = baseOffset + (eggMovePointer % 0x1000); List moves = new ArrayList<>(); int val = rom[eggMoveOffset] & 0xFF; while (val != 0xFF) { moves.add(val); eggMoveOffset++; val = rom[eggMoveOffset] & 0xFF; } if (moves.size() > 0) { eggMoves.put(i, moves); } } return eggMoves; } @Override public void setEggMoves(Map> eggMoves) { int pointersOffset = romEntry.getValue("EggMovesTableOffset"); int baseOffset = (pointersOffset / 0x1000) * 0x1000; for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { int eggMovePointer = FileFunctions.read2ByteInt(rom, pointersOffset + ((i - 1) * 2)); int eggMoveOffset = baseOffset + (eggMovePointer % 0x1000); if (eggMoves.containsKey(i)) { List moves = eggMoves.get(i); for (int move: moves) { rom[eggMoveOffset] = (byte) move; eggMoveOffset++; } } } } private static class StaticPokemon { protected int[] speciesOffsets; protected int[] levelOffsets; public StaticPokemon() { this.speciesOffsets = new int[0]; this.levelOffsets = new int[0]; } public Pokemon getPokemon(Gen2RomHandler rh) { return rh.pokes[rh.rom[speciesOffsets[0]] & 0xFF]; } public void setPokemon(Gen2RomHandler rh, Pokemon pkmn) { for (int offset : speciesOffsets) { rh.rom[offset] = (byte) pkmn.number; } } public int getLevel(byte[] rom, int i) { if (levelOffsets.length <= i) { return 1; } return rom[levelOffsets[i]]; } public void setLevel(byte[] rom, int level, int i) { if (levelOffsets.length > i) { // Might not have a level entry e.g., it's an egg rom[levelOffsets[i]] = (byte) level; } } } private static class StaticPokemonGameCorner extends StaticPokemon { @Override public void setPokemon(Gen2RomHandler rh, Pokemon pkmn) { // Last offset is a pointer to the name int offsetSize = speciesOffsets.length; for (int i = 0; i < offsetSize - 1; i++) { rh.rom[speciesOffsets[i]] = (byte) pkmn.number; } rh.writePaddedPokemonName(pkmn.name, rh.romEntry.getValue("GameCornerPokemonNameLength"), speciesOffsets[offsetSize - 1]); } } @Override public List getStaticPokemon() { List statics = new ArrayList<>(); int[] staticEggOffsets = new int[0]; if (romEntry.arrayEntries.containsKey("StaticEggPokemonOffsets")) { staticEggOffsets = romEntry.arrayEntries.get("StaticEggPokemonOffsets"); } if (romEntry.getValue("StaticPokemonSupport") > 0) { for (int i = 0; i < romEntry.staticPokemon.size(); i++) { int currentOffset = i; StaticPokemon sp = romEntry.staticPokemon.get(i); StaticEncounter se = new StaticEncounter(); se.pkmn = sp.getPokemon(this); se.level = sp.getLevel(rom, 0); se.isEgg = Arrays.stream(staticEggOffsets).anyMatch(x-> x == currentOffset); statics.add(se); } } if (romEntry.getValue("StaticPokemonOddEggOffset") > 0) { int oeOffset = romEntry.getValue("StaticPokemonOddEggOffset"); int oeSize = romEntry.getValue("StaticPokemonOddEggDataSize"); for (int i = 0; i < Gen2Constants.oddEggPokemonCount; i++) { StaticEncounter se = new StaticEncounter(); se.pkmn = pokes[rom[oeOffset + i * oeSize] & 0xFF]; se.isEgg = true; statics.add(se); } } return statics; } @Override public boolean setStaticPokemon(List staticPokemon) { if (romEntry.getValue("StaticPokemonSupport") == 0) { return false; } if (!havePatchedFleeing) { patchFleeing(); } int desiredSize = romEntry.staticPokemon.size(); if (romEntry.getValue("StaticPokemonOddEggOffset") > 0) { desiredSize += Gen2Constants.oddEggPokemonCount; } if (staticPokemon.size() != desiredSize) { return false; } Iterator statics = staticPokemon.iterator(); for (int i = 0; i < romEntry.staticPokemon.size(); i++) { StaticEncounter currentStatic = statics.next(); StaticPokemon sp = romEntry.staticPokemon.get(i); sp.setPokemon(this, currentStatic.pkmn); sp.setLevel(rom, currentStatic.level, 0); } if (romEntry.getValue("StaticPokemonOddEggOffset") > 0) { int oeOffset = romEntry.getValue("StaticPokemonOddEggOffset"); int oeSize = romEntry.getValue("StaticPokemonOddEggDataSize"); for (int i = 0; i < Gen2Constants.oddEggPokemonCount; i++) { int oddEggPokemonNumber = statics.next().pkmn.number; rom[oeOffset + i * oeSize] = (byte) oddEggPokemonNumber; setMovesForOddEggPokemon(oddEggPokemonNumber, oeOffset + i * oeSize); } } return true; } // This method depends on movesets being randomized before static Pokemon. This is currently true, // but may not *always* be true, so take care. private void setMovesForOddEggPokemon(int oddEggPokemonNumber, int oddEggPokemonOffset) { // Determine the level 5 moveset, minus Dizzy Punch Map> movesets = this.getMovesLearnt(); List moves = this.getMoves(); List moveset = movesets.get(oddEggPokemonNumber); Queue level5Moveset = new LinkedList<>(); int currentMoveIndex = 0; while (moveset.size() > currentMoveIndex && moveset.get(currentMoveIndex).level <= 5) { if (level5Moveset.size() == 4) { level5Moveset.remove(); } level5Moveset.add(moveset.get(currentMoveIndex).move); currentMoveIndex++; } // Now add Dizzy Punch and write the moveset and PP if (level5Moveset.size() == 4) { level5Moveset.remove(); } level5Moveset.add(Moves.dizzyPunch); for (int i = 0; i < 4; i++) { int move = 0; int pp = 0; if (level5Moveset.size() > 0) { move = level5Moveset.remove(); pp = moves.get(move).pp; // This assumes the ordering of moves matches the internal order } rom[oddEggPokemonOffset + 2 + i] = (byte) move; rom[oddEggPokemonOffset + 23 + i] = (byte) pp; } } @Override public boolean canChangeStaticPokemon() { return (romEntry.getValue("StaticPokemonSupport") > 0); } @Override public boolean hasStaticAltFormes() { return false; } @Override public List bannedForStaticPokemon() { return Collections.singletonList(pokes[Species.unown]); // Unown banned } @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) { } private void writePaddedPokemonName(String name, int length, int offset) { String paddedName = String.format("%-" + length + "s", name); byte[] rawData = translateString(paddedName); System.arraycopy(rawData, 0, rom, offset, length); } @Override public List getTMMoves() { List tms = new ArrayList<>(); int offset = romEntry.getValue("TMMovesOffset"); for (int i = 1; i <= Gen2Constants.tmCount; i++) { tms.add(rom[offset + (i - 1)] & 0xFF); } return tms; } @Override public List getHMMoves() { List hms = new ArrayList<>(); int offset = romEntry.getValue("TMMovesOffset"); for (int i = 1; i <= Gen2Constants.hmCount; i++) { hms.add(rom[offset + Gen2Constants.tmCount + (i - 1)] & 0xFF); } return hms; } @Override public void setTMMoves(List moveIndexes) { int offset = romEntry.getValue("TMMovesOffset"); for (int i = 1; i <= Gen2Constants.tmCount; i++) { rom[offset + (i - 1)] = moveIndexes.get(i - 1).byteValue(); } // TM Text String[] moveNames = readMoveNames(); for (TMTextEntry tte : romEntry.tmTexts) { String moveName = moveNames[moveIndexes.get(tte.number - 1)]; String text = tte.template.replace("%m", moveName); writeVariableLengthString(text, tte.offset, true); } } @Override public int getTMCount() { return Gen2Constants.tmCount; } @Override public int getHMCount() { return Gen2Constants.hmCount; } @Override public Map getTMHMCompatibility() { Map compat = new TreeMap<>(); for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { int baseStatsOffset = romEntry.getValue("PokemonStatsOffset") + (i - 1) * Gen2Constants.baseStatsEntrySize; Pokemon pkmn = pokes[i]; boolean[] flags = new boolean[Gen2Constants.tmCount + Gen2Constants.hmCount + 1]; for (int j = 0; j < 8; j++) { readByteIntoFlags(flags, j * 8 + 1, baseStatsOffset + Gen2Constants.bsTMHMCompatOffset + j); } compat.put(pkmn, flags); } return compat; } @Override public void setTMHMCompatibility(Map compatData) { for (Map.Entry compatEntry : compatData.entrySet()) { Pokemon pkmn = compatEntry.getKey(); boolean[] flags = compatEntry.getValue(); int baseStatsOffset = romEntry.getValue("PokemonStatsOffset") + (pkmn.number - 1) * Gen2Constants.baseStatsEntrySize; for (int j = 0; j < 8; j++) { if (!romEntry.isCrystal || j != 7) { rom[baseStatsOffset + Gen2Constants.bsTMHMCompatOffset + j] = getByteFromFlags(flags, j * 8 + 1); } else { // Move tutor data // bits 1,2,3 of byte 7 int changedByte = getByteFromFlags(flags, j * 8 + 1) & 0xFF; int currentByte = rom[baseStatsOffset + Gen2Constants.bsTMHMCompatOffset + j]; changedByte |= ((currentByte >> 1) & 0x01) << 1; changedByte |= ((currentByte >> 2) & 0x01) << 2; changedByte |= ((currentByte >> 3) & 0x01) << 3; rom[baseStatsOffset + 0x18 + j] = (byte) changedByte; } } } } @Override public boolean hasMoveTutors() { return romEntry.isCrystal; } @Override public List getMoveTutorMoves() { if (romEntry.isCrystal) { List mtMoves = new ArrayList<>(); for (int offset : romEntry.arrayEntries.get("MoveTutorMoves")) { mtMoves.add(rom[offset] & 0xFF); } return mtMoves; } return new ArrayList<>(); } @Override public void setMoveTutorMoves(List moves) { if (!romEntry.isCrystal) { return; } if (moves.size() != 3) { return; } Iterator mvList = moves.iterator(); for (int offset : romEntry.arrayEntries.get("MoveTutorMoves")) { rom[offset] = mvList.next().byteValue(); } // Construct a new menu if (romEntry.getValue("MoveTutorMenuOffset") > 0 && romEntry.getValue("MoveTutorMenuNewSpace") > 0) { String[] moveNames = readMoveNames(); String[] names = new String[] { moveNames[moves.get(0)], moveNames[moves.get(1)], moveNames[moves.get(2)], Gen2Constants.mtMenuCancelString }; int menuOffset = romEntry.getValue("MoveTutorMenuNewSpace"); rom[menuOffset++] = Gen2Constants.mtMenuInitByte; rom[menuOffset++] = 0x4; for (int i = 0; i < 4; i++) { byte[] trans = translateString(names[i]); System.arraycopy(trans, 0, rom, menuOffset, trans.length); menuOffset += trans.length; rom[menuOffset++] = GBConstants.stringTerminator; } int pointerOffset = romEntry.getValue("MoveTutorMenuOffset"); writeWord(pointerOffset, makeGBPointer(romEntry.getValue("MoveTutorMenuNewSpace"))); } } @Override public Map getMoveTutorCompatibility() { if (!romEntry.isCrystal) { return new TreeMap<>(); } Map compat = new TreeMap<>(); for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { int baseStatsOffset = romEntry.getValue("PokemonStatsOffset") + (i - 1) * Gen2Constants.baseStatsEntrySize; Pokemon pkmn = pokes[i]; boolean[] flags = new boolean[4]; int mtByte = rom[baseStatsOffset + Gen2Constants.bsMTCompatOffset] & 0xFF; for (int j = 1; j <= 3; j++) { flags[j] = ((mtByte >> j) & 0x01) > 0; } compat.put(pkmn, flags); } return compat; } @Override public void setMoveTutorCompatibility(Map compatData) { if (!romEntry.isCrystal) { return; } for (Map.Entry compatEntry : compatData.entrySet()) { Pokemon pkmn = compatEntry.getKey(); boolean[] flags = compatEntry.getValue(); int baseStatsOffset = romEntry.getValue("PokemonStatsOffset") + (pkmn.number - 1) * Gen2Constants.baseStatsEntrySize; int origMtByte = rom[baseStatsOffset + Gen2Constants.bsMTCompatOffset] & 0xFF; int mtByte = origMtByte & 0x01; for (int j = 1; j <= 3; j++) { mtByte |= flags[j] ? (1 << j) : 0; } rom[baseStatsOffset + Gen2Constants.bsMTCompatOffset] = (byte) mtByte; } } @Override public String getROMName() { if (isVietCrystal) { return Gen2Constants.vietCrystalROMName; } return "Pokemon " + romEntry.name; } @Override public String getROMCode() { return romEntry.romCode; } @Override public String getSupportLevel() { return "Complete"; } private static int find(byte[] haystack, String hexString) { if (hexString.length() % 2 != 0) { return -3; // error } byte[] searchFor = new byte[hexString.length() / 2]; for (int i = 0; i < searchFor.length; i++) { searchFor[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16); } List found = RomFunctions.search(haystack, searchFor); if (found.size() == 0) { return -1; // not found } else if (found.size() > 1) { return -2; // not unique } else { return found.get(0); } } @Override public boolean hasTimeBasedEncounters() { return true; // All GSC do } @Override public boolean hasWildAltFormes() { return false; } private void populateEvolutions() { for (Pokemon pkmn : pokes) { if (pkmn != null) { pkmn.evolutionsFrom.clear(); pkmn.evolutionsTo.clear(); } } int pointersOffset = romEntry.getValue("PokemonMovesetsTableOffset"); for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { int pointer = readWord(pointersOffset + (i - 1) * 2); int realPointer = calculateOffset(bankOf(pointersOffset), pointer); Pokemon pkmn = pokes[i]; while (rom[realPointer] != 0) { int method = rom[realPointer] & 0xFF; int otherPoke = rom[realPointer + 2 + (method == 5 ? 1 : 0)] & 0xFF; EvolutionType type = EvolutionType.fromIndex(2, method); int extraInfo = 0; if (type == EvolutionType.TRADE) { int itemNeeded = rom[realPointer + 1] & 0xFF; if (itemNeeded != 0xFF) { type = EvolutionType.TRADE_ITEM; extraInfo = itemNeeded; } } else if (type == EvolutionType.LEVEL_ATTACK_HIGHER) { int tyrogueCond = rom[realPointer + 2] & 0xFF; if (tyrogueCond == 2) { type = EvolutionType.LEVEL_DEFENSE_HIGHER; } else if (tyrogueCond == 3) { type = EvolutionType.LEVEL_ATK_DEF_SAME; } extraInfo = rom[realPointer + 1] & 0xFF; } else if (type == EvolutionType.HAPPINESS) { int happCond = rom[realPointer + 1] & 0xFF; if (happCond == 2) { type = EvolutionType.HAPPINESS_DAY; } else if (happCond == 3) { type = EvolutionType.HAPPINESS_NIGHT; } } else { extraInfo = rom[realPointer + 1] & 0xFF; } Evolution evo = new Evolution(pokes[i], pokes[otherPoke], true, type, extraInfo); if (!pkmn.evolutionsFrom.contains(evo)) { pkmn.evolutionsFrom.add(evo); pokes[otherPoke].evolutionsTo.add(evo); } realPointer += (method == 5 ? 4 : 3); } // split evos don't carry stats if (pkmn.evolutionsFrom.size() > 1) { for (Evolution e : pkmn.evolutionsFrom) { e.carryStats = false; } } } } @Override public void removeImpossibleEvolutions(Settings settings) { // no move evos, so no need to check for those for (Pokemon pkmn : pokes) { if (pkmn != null) { for (Evolution evol : pkmn.evolutionsFrom) { if (evol.type == EvolutionType.TRADE || evol.type == EvolutionType.TRADE_ITEM) { // change if (evol.from.number == Species.slowpoke) { // Slowpoke: Make water stone => Slowking evol.type = EvolutionType.STONE; evol.extraInfo = Gen2Items.waterStone; addEvoUpdateStone(impossibleEvolutionUpdates, evol, itemNames[24]); } else if (evol.from.number == Species.seadra) { // Seadra: level 40 evol.type = EvolutionType.LEVEL; evol.extraInfo = 40; // level addEvoUpdateLevel(impossibleEvolutionUpdates, evol); } else if (evol.from.number == Species.poliwhirl || evol.type == EvolutionType.TRADE) { // Poliwhirl or any of the original 4 trade evos // Level 37 evol.type = EvolutionType.LEVEL; evol.extraInfo = 37; // level addEvoUpdateLevel(impossibleEvolutionUpdates, evol); } else { // A new trade evo of a single stage Pokemon // level 30 evol.type = EvolutionType.LEVEL; evol.extraInfo = 30; // level addEvoUpdateLevel(impossibleEvolutionUpdates, evol); } } } } } } @Override public void makeEvolutionsEasier(Settings settings) { // Reduce the amount of happiness required to evolve. int offset = find(rom, Gen2Constants.friendshipValueForEvoLocator); if (offset > 0) { // The thing we're looking at is actually one byte before what we // want to change; this makes it work in both G/S and Crystal. offset++; // Amount of required happiness for all happiness evolutions. if (rom[offset] == (byte)220) { rom[offset] = (byte)160; } } } @Override public void removeTimeBasedEvolutions() { for (Pokemon pkmn : pokes) { if (pkmn != null) { for (Evolution evol : pkmn.evolutionsFrom) { // In Gen 2, only Eevee has a time-based evolution. if (evol.type == EvolutionType.HAPPINESS_DAY) { // Eevee: Make sun stone => Espeon evol.type = EvolutionType.STONE; evol.extraInfo = Gen2Items.sunStone; addEvoUpdateStone(timeBasedEvolutionUpdates, evol, itemNames[169]); } else if (evol.type == EvolutionType.HAPPINESS_NIGHT) { // Eevee: Make moon stone => Umbreon evol.type = EvolutionType.STONE; evol.extraInfo = Gen2Items.moonStone; addEvoUpdateStone(timeBasedEvolutionUpdates, evol, itemNames[8]); } } } } } @Override public boolean hasShopRandomization() { return false; } @Override public Map getShopItems() { return null; // Not implemented } @Override public void setShopItems(Map shopItems) { // Not implemented } @Override public void setShopPrices() { // Not implemented } @Override public boolean canChangeTrainerText() { return romEntry.getValue("CanChangeTrainerText") > 0; } @Override public List getTrainerNames() { int traineroffset = romEntry.getValue("TrainerDataTableOffset"); int traineramount = romEntry.getValue("TrainerClassAmount"); int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts"); int[] pointers = new int[traineramount]; for (int i = 0; i < traineramount; i++) { int pointer = readWord(traineroffset + i * 2); pointers[i] = calculateOffset(bankOf(traineroffset), pointer); } List allTrainers = new ArrayList<>(); for (int i = 0; i < traineramount; i++) { int offs = pointers[i]; int limit = trainerclasslimits[i]; for (int trnum = 0; trnum < limit; trnum++) { String name = readVariableLengthString(offs, false); allTrainers.add(name); offs += lengthOfStringAt(offs, false) + 1; int dataType = rom[offs] & 0xFF; offs++; while ((rom[offs] & 0xFF) != 0xFF) { offs += 2; if (dataType == 2 || dataType == 3) { offs++; } if (dataType % 2 == 1) { offs += 4; } } offs++; } } return allTrainers; } @Override public void setTrainerNames(List trainerNames) { if (romEntry.getValue("CanChangeTrainerText") != 0) { int traineroffset = romEntry.getValue("TrainerDataTableOffset"); int traineramount = romEntry.getValue("TrainerClassAmount"); int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts"); int[] pointers = new int[traineramount]; for (int i = 0; i < traineramount; i++) { int pointer = readWord(traineroffset + i * 2); pointers[i] = calculateOffset(bankOf(traineroffset), pointer); } // Build up new trainer data using old as a guideline. int[] offsetsInNew = new int[traineramount]; int oInNewCurrent = 0; Iterator allTrainers = trainerNames.iterator(); ByteArrayOutputStream newData = new ByteArrayOutputStream(); try { for (int i = 0; i < traineramount; i++) { int offs = pointers[i]; int limit = trainerclasslimits[i]; offsetsInNew[i] = oInNewCurrent; for (int trnum = 0; trnum < limit; trnum++) { String newName = allTrainers.next(); // The game uses 0xFF as a signifier for the end of the trainer data. // It ALSO uses 0xFF to encode the character "9". If a trainer name has // "9" in it, this causes strange side effects where certain trainers // effectively get skipped when parsing trainer data. Silently strip out // "9"s from trainer names to prevent this from happening. newName = newName.replace("9", "").trim(); byte[] newNameStr = translateString(newName); newData.write(newNameStr); newData.write(GBConstants.stringTerminator); oInNewCurrent += newNameStr.length + 1; offs += lengthOfStringAt(offs, false) + 1; int dataType = rom[offs] & 0xFF; offs++; newData.write(dataType); oInNewCurrent++; while ((rom[offs] & 0xFF) != 0xFF) { newData.write(rom, offs, 2); oInNewCurrent += 2; offs += 2; if (dataType == 2 || dataType == 3) { newData.write(rom, offs, 1); oInNewCurrent++; offs++; } if (dataType % 2 == 1) { newData.write(rom, offs, 4); oInNewCurrent += 4; offs += 4; } } newData.write(0xFF); oInNewCurrent++; offs++; } } // Copy new data into ROM byte[] newTrainerData = newData.toByteArray(); int tdBase = pointers[0]; System.arraycopy(newTrainerData, 0, rom, pointers[0], newTrainerData.length); // Finally, update the pointers for (int i = 1; i < traineramount; i++) { int newOffset = tdBase + offsetsInNew[i]; writeWord(traineroffset + i * 2, makeGBPointer(newOffset)); } } catch (IOException ex) { // This should never happen, but abort if it does. } } } @Override public TrainerNameMode trainerNameMode() { return TrainerNameMode.MAX_LENGTH_WITH_CLASS; } @Override public int maxTrainerNameLength() { // line size minus one for space return Gen2Constants.maxTrainerNameLength; } @Override public int maxSumOfTrainerNameLengths() { return romEntry.getValue("MaxSumOfTrainerNameLengths"); } @Override public List getTCNameLengthsByTrainer() { int traineramount = romEntry.getValue("TrainerClassAmount"); int[] trainerclasslimits = romEntry.arrayEntries.get("TrainerDataClassCounts"); List tcNames = this.getTrainerClassNames(); List tcLengthsByT = new ArrayList<>(); for (int i = 0; i < traineramount; i++) { int len = internalStringLength(tcNames.get(i)); for (int k = 0; k < trainerclasslimits[i]; k++) { tcLengthsByT.add(len); } } return tcLengthsByT; } @Override public List getTrainerClassNames() { int amount = romEntry.getValue("TrainerClassAmount"); int offset = romEntry.getValue("TrainerClassNamesOffset"); List trainerClassNames = new ArrayList<>(); for (int j = 0; j < amount; j++) { String name = readVariableLengthString(offset, false); offset += lengthOfStringAt(offset, false) + 1; trainerClassNames.add(name); } return trainerClassNames; } @Override public List getEvolutionItems() { return null; } @Override public void setTrainerClassNames(List trainerClassNames) { if (romEntry.getValue("CanChangeTrainerText") != 0) { int amount = romEntry.getValue("TrainerClassAmount"); int offset = romEntry.getValue("TrainerClassNamesOffset"); Iterator trainerClassNamesI = trainerClassNames.iterator(); for (int j = 0; j < amount; j++) { int len = lengthOfStringAt(offset, false) + 1; String newName = trainerClassNamesI.next(); writeFixedLengthString(newName, offset, len); offset += len; } } } @Override public boolean fixedTrainerClassNamesLength() { return true; } @Override public List 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 "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.codeTweaks.get("BWXPTweak") != null) { available |= MiscTweak.BW_EXP_PATCH.getValue(); } if (romEntry.getValue("TextDelayFunctionOffset") != 0) { available |= MiscTweak.FASTEST_TEXT.getValue(); } if (romEntry.arrayEntries.containsKey("CatchingTutorialOffsets")) { available |= MiscTweak.RANDOMIZE_CATCHING_TUTORIAL.getValue(); } available |= MiscTweak.BAN_LUCKY_EGG.getValue(); return available; } @Override public void applyMiscTweak(MiscTweak tweak) { if (tweak == MiscTweak.BW_EXP_PATCH) { applyBWEXPPatch(); } else if (tweak == MiscTweak.FASTEST_TEXT) { applyFastestTextPatch(); } else if (tweak == MiscTweak.LOWER_CASE_POKEMON_NAMES) { applyCamelCaseNames(); } else if (tweak == MiscTweak.RANDOMIZE_CATCHING_TUTORIAL) { randomizeCatchingTutorial(); } else if (tweak == MiscTweak.BAN_LUCKY_EGG) { allowedItems.banSingles(Gen2Items.luckyEgg); nonBadItems.banSingles(Gen2Items.luckyEgg); } else if (tweak == MiscTweak.UPDATE_TYPE_EFFECTIVENESS) { updateTypeEffectiveness(); } } @Override public boolean isEffectivenessUpdated() { return effectivenessUpdated; } private void randomizeCatchingTutorial() { if (romEntry.arrayEntries.containsKey("CatchingTutorialOffsets")) { // Pick a pokemon int pokemon = this.random.nextInt(Gen2Constants.pokemonCount) + 1; while (pokemon == Species.unown) { // Unown is banned pokemon = this.random.nextInt(Gen2Constants.pokemonCount) + 1; } int[] offsets = romEntry.arrayEntries.get("CatchingTutorialOffsets"); for (int offset : offsets) { rom[offset] = (byte) pokemon; } } } private void applyBWEXPPatch() { String patchName = romEntry.codeTweaks.get("BWXPTweak"); if (patchName == null) { return; } try { FileFunctions.applyPatch(rom, patchName); } catch (IOException e) { throw new RandomizerIOException(e); } } private void applyFastestTextPatch() { if (romEntry.getValue("TextDelayFunctionOffset") != 0) { rom[romEntry.getValue("TextDelayFunctionOffset")] = GBConstants.gbZ80Ret; } } private void updateTypeEffectiveness() { List typeEffectivenessTable = readTypeEffectivenessTable(); log("--Updating Type Effectiveness--"); for (TypeRelationship relationship : typeEffectivenessTable) { // Change Ghost 0.5x against Steel to Ghost 1x to Steel if (relationship.attacker == Type.GHOST && relationship.defender == Type.STEEL) { relationship.effectiveness = Effectiveness.NEUTRAL; log("Replaced: Ghost not very effective vs Steel => Ghost neutral vs Steel"); } // Change Dark 0.5x against Steel to Dark 1x to Steel else if (relationship.attacker == Type.DARK && relationship.defender == Type.STEEL) { relationship.effectiveness = Effectiveness.NEUTRAL; log("Replaced: Dark not very effective vs Steel => Dark neutral vs Steel"); } } logBlankLine(); writeTypeEffectivenessTable(typeEffectivenessTable); effectivenessUpdated = true; } private List readTypeEffectivenessTable() { List typeEffectivenessTable = new ArrayList<>(); int currentOffset = romEntry.getValue("TypeEffectivenessOffset"); int attackingType = rom[currentOffset]; // 0xFE marks the end of the table *not* affected by Foresight, while 0xFF marks // the actual end of the table. Since we don't care about Ghost immunities at all, // just stop once we reach the Foresight section. while (attackingType != (byte) 0xFE) { int defendingType = rom[currentOffset + 1]; int effectivenessInternal = rom[currentOffset + 2]; Type attacking = Gen2Constants.typeTable[attackingType]; Type defending = Gen2Constants.typeTable[defendingType]; Effectiveness effectiveness = null; switch (effectivenessInternal) { case 20: effectiveness = Effectiveness.DOUBLE; break; case 10: effectiveness = Effectiveness.NEUTRAL; break; case 5: effectiveness = Effectiveness.HALF; break; case 0: effectiveness = Effectiveness.ZERO; break; } if (effectiveness != null) { TypeRelationship relationship = new TypeRelationship(attacking, defending, effectiveness); typeEffectivenessTable.add(relationship); } currentOffset += 3; attackingType = rom[currentOffset]; } return typeEffectivenessTable; } private void writeTypeEffectivenessTable(List typeEffectivenessTable) { int currentOffset = romEntry.getValue("TypeEffectivenessOffset"); for (TypeRelationship relationship : typeEffectivenessTable) { rom[currentOffset] = Gen2Constants.typeToByte(relationship.attacker); rom[currentOffset + 1] = Gen2Constants.typeToByte(relationship.defender); byte effectivenessInternal = 0; switch (relationship.effectiveness) { case DOUBLE: effectivenessInternal = 20; break; case NEUTRAL: effectivenessInternal = 10; break; case HALF: effectivenessInternal = 5; break; case ZERO: effectivenessInternal = 0; break; } rom[currentOffset + 2] = effectivenessInternal; currentOffset += 3; } } @Override public void enableGuaranteedPokemonCatching() { String prefix = romEntry.getString("GuaranteedCatchPrefix"); int offset = find(rom, prefix); if (offset > 0) { offset += prefix.length() / 2; // because it was a prefix // The game guarantees that the catching tutorial always succeeds in catching by running // the following code: // ld a, [wBattleType] // cp BATTLETYPE_TUTORIAL // jp z, .catch_without_fail // By making the jump here unconditional, we can ensure that catching always succeeds no // matter the battle type. We check that the original condition is present just for safety. if (rom[offset] == (byte)0xCA) { rom[offset] = (byte)0xC3; } } } @Override public void randomizeIntroPokemon() { // Intro sprite // Pick a pokemon int pokemon = this.random.nextInt(Gen2Constants.pokemonCount) + 1; while (pokemon == Species.unown) { // Unown is banned pokemon = this.random.nextInt(Gen2Constants.pokemonCount) + 1; } rom[romEntry.getValue("IntroSpriteOffset")] = (byte) pokemon; rom[romEntry.getValue("IntroCryOffset")] = (byte) pokemon; } @Override public ItemList getAllowedItems() { return allowedItems; } @Override public ItemList getNonBadItems() { return nonBadItems; } @Override public List 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); } } @Override public String[] getItemNames() { return itemNames; } private void patchFleeing() { havePatchedFleeing = true; int offset = romEntry.getValue("FleeingDataOffset"); rom[offset] = (byte) 0xFF; rom[offset + Gen2Constants.fleeingSetTwoOffset] = (byte) 0xFF; rom[offset + Gen2Constants.fleeingSetThreeOffset] = (byte) 0xFF; } private void loadLandmarkNames() { int lmOffset = romEntry.getValue("LandmarkTableOffset"); int lmBank = bankOf(lmOffset); int lmCount = romEntry.getValue("LandmarkCount"); landmarkNames = new String[lmCount]; for (int i = 0; i < lmCount; i++) { int lmNameOffset = calculateOffset(lmBank, readWord(lmOffset + i * 4 + 2)); landmarkNames[i] = readVariableLengthString(lmNameOffset, false).replace("\\x1F", " "); } } private void preprocessMaps() { itemOffs = new ArrayList<>(); int mhOffset = romEntry.getValue("MapHeaders"); int mapGroupCount = Gen2Constants.mapGroupCount; int mapsInLastGroup = Gen2Constants.mapsInLastGroup; int mhBank = bankOf(mhOffset); mapNames = new String[mapGroupCount + 1][100]; int[] groupOffsets = new int[mapGroupCount]; for (int i = 0; i < mapGroupCount; i++) { groupOffsets[i] = calculateOffset(mhBank, readWord(mhOffset + i * 2)); } // Read maps for (int mg = 0; mg < mapGroupCount; mg++) { int offset = groupOffsets[mg]; int maxOffset = (mg == mapGroupCount - 1) ? (mhBank + 1) * GBConstants.bankSize : groupOffsets[mg + 1]; int map = 0; int maxMap = (mg == mapGroupCount - 1) ? mapsInLastGroup : Integer.MAX_VALUE; while (offset < maxOffset && map < maxMap) { processMapAt(offset, mg + 1, map + 1); offset += 9; map++; } } } private void processMapAt(int offset, int mapBank, int mapNumber) { // second map header int smhBank = rom[offset] & 0xFF; int smhPointer = readWord(offset + 3); int smhOffset = calculateOffset(smhBank, smhPointer); // map name int mapLandmark = rom[offset + 5] & 0xFF; mapNames[mapBank][mapNumber] = landmarkNames[mapLandmark]; // event header // event header is in same bank as script header int ehBank = rom[smhOffset + 6] & 0xFF; int ehPointer = readWord(smhOffset + 9); int ehOffset = calculateOffset(ehBank, ehPointer); // skip over filler ehOffset += 2; // warps int warpCount = rom[ehOffset++] & 0xFF; // warps are skipped ehOffset += warpCount * 5; // xy triggers int triggerCount = rom[ehOffset++] & 0xFF; // xy triggers are skipped ehOffset += triggerCount * 8; // signposts int signpostCount = rom[ehOffset++] & 0xFF; // we do care about these for (int sp = 0; sp < signpostCount; sp++) { // type=7 are hidden items int spType = rom[ehOffset + sp * 5 + 2] & 0xFF; if (spType == 7) { // get event pointer int spPointer = readWord(ehOffset + sp * 5 + 3); int spOffset = calculateOffset(ehBank, spPointer); // item is at spOffset+2 (first two bytes are the flag id) itemOffs.add(spOffset + 2); } } // now skip past them ehOffset += signpostCount * 5; // visible objects/people int peopleCount = rom[ehOffset++] & 0xFF; // we also care about these for (int p = 0; p < peopleCount; p++) { // color_function & 1 = 1 if itemball int pColorFunction = rom[ehOffset + p * 13 + 7]; if ((pColorFunction & 1) == 1) { // get event pointer int pPointer = readWord(ehOffset + p * 13 + 9); int pOffset = calculateOffset(ehBank, pPointer); // item is at the pOffset for non-hidden items itemOffs.add(pOffset); } } } @Override public List getRequiredFieldTMs() { return Gen2Constants.requiredFieldTMs; } @Override public List getCurrentFieldTMs() { List fieldTMs = new ArrayList<>(); for (int offset : itemOffs) { int itemHere = rom[offset] & 0xFF; if (Gen2Constants.allowedItems.isTM(itemHere)) { int thisTM; if (itemHere >= Gen2Constants.tmBlockOneIndex && itemHere < Gen2Constants.tmBlockOneIndex + Gen2Constants.tmBlockOneSize) { thisTM = itemHere - Gen2Constants.tmBlockOneIndex + 1; } else if (itemHere >= Gen2Constants.tmBlockTwoIndex && itemHere < Gen2Constants.tmBlockTwoIndex + Gen2Constants.tmBlockTwoSize) { thisTM = itemHere - Gen2Constants.tmBlockTwoIndex + 1 + Gen2Constants.tmBlockOneSize; // TM // block // 2 // offset } else { thisTM = itemHere - Gen2Constants.tmBlockThreeIndex + 1 + Gen2Constants.tmBlockOneSize + Gen2Constants.tmBlockTwoSize; // TM block 3 offset } // hack for the bug catching contest repeat TM28 if (!fieldTMs.contains(thisTM)) { fieldTMs.add(thisTM); } } } return fieldTMs; } @Override public void setFieldTMs(List fieldTMs) { Iterator iterTMs = fieldTMs.iterator(); int[] givenTMs = new int[256]; for (int offset : itemOffs) { int itemHere = rom[offset] & 0xFF; if (Gen2Constants.allowedItems.isTM(itemHere)) { // Cache replaced TMs to duplicate bug catching contest TM if (givenTMs[itemHere] != 0) { rom[offset] = (byte) givenTMs[itemHere]; } else { // Replace this with a TM from the list int tm = iterTMs.next(); if (tm >= 1 && tm <= Gen2Constants.tmBlockOneSize) { tm += Gen2Constants.tmBlockOneIndex - 1; } else if (tm >= Gen2Constants.tmBlockOneSize + 1 && tm <= Gen2Constants.tmBlockOneSize + Gen2Constants.tmBlockTwoSize) { tm += Gen2Constants.tmBlockTwoIndex - 1 - Gen2Constants.tmBlockOneSize; } else { tm += Gen2Constants.tmBlockThreeIndex - 1 - Gen2Constants.tmBlockOneSize - Gen2Constants.tmBlockTwoSize; } givenTMs[itemHere] = tm; rom[offset] = (byte) tm; } } } } @Override public List getRegularFieldItems() { List fieldItems = new ArrayList<>(); for (int offset : itemOffs) { int itemHere = rom[offset] & 0xFF; if (Gen2Constants.allowedItems.isAllowed(itemHere) && !(Gen2Constants.allowedItems.isTM(itemHere))) { fieldItems.add(itemHere); } } return fieldItems; } @Override public void setRegularFieldItems(List items) { Iterator iterItems = items.iterator(); for (int offset : itemOffs) { int itemHere = rom[offset] & 0xFF; if (Gen2Constants.allowedItems.isAllowed(itemHere) && !(Gen2Constants.allowedItems.isTM(itemHere))) { // Replace it rom[offset] = (byte) (iterItems.next().intValue()); } } } @Override public List getIngameTrades() { List trades = new ArrayList<>(); // info int tableOffset = romEntry.getValue("TradeTableOffset"); int tableSize = romEntry.getValue("TradeTableSize"); int nicknameLength = romEntry.getValue("TradeNameLength"); int otLength = romEntry.getValue("TradeOTLength"); int[] unused = romEntry.arrayEntries.get("TradesUnused"); int unusedOffset = 0; int entryLength = nicknameLength + otLength + 9; if (entryLength % 2 != 0) { entryLength++; } for (int entry = 0; entry < tableSize; entry++) { if (unusedOffset < unused.length && unused[unusedOffset] == entry) { unusedOffset++; continue; } IngameTrade trade = new IngameTrade(); int entryOffset = tableOffset + entry * entryLength; trade.requestedPokemon = pokes[rom[entryOffset + 1] & 0xFF]; trade.givenPokemon = pokes[rom[entryOffset + 2] & 0xFF]; trade.nickname = readString(entryOffset + 3, nicknameLength, false); int atkdef = rom[entryOffset + 3 + nicknameLength] & 0xFF; int spdspc = rom[entryOffset + 4 + nicknameLength] & 0xFF; trade.ivs = new int[] { (atkdef >> 4) & 0xF, atkdef & 0xF, (spdspc >> 4) & 0xF, spdspc & 0xF }; trade.item = rom[entryOffset + 5 + nicknameLength] & 0xFF; trade.otId = readWord(entryOffset + 6 + nicknameLength); trade.otName = readString(entryOffset + 8 + nicknameLength, otLength, false); trades.add(trade); } return trades; } @Override public void setIngameTrades(List trades) { // info int tableOffset = romEntry.getValue("TradeTableOffset"); int tableSize = romEntry.getValue("TradeTableSize"); int nicknameLength = romEntry.getValue("TradeNameLength"); int otLength = romEntry.getValue("TradeOTLength"); int[] unused = romEntry.arrayEntries.get("TradesUnused"); int unusedOffset = 0; int entryLength = nicknameLength + otLength + 9; if (entryLength % 2 != 0) { entryLength++; } int tradeOffset = 0; for (int entry = 0; entry < tableSize; entry++) { if (unusedOffset < unused.length && unused[unusedOffset] == entry) { unusedOffset++; continue; } IngameTrade trade = trades.get(tradeOffset++); int entryOffset = tableOffset + entry * entryLength; rom[entryOffset + 1] = (byte) trade.requestedPokemon.number; rom[entryOffset + 2] = (byte) trade.givenPokemon.number; if (romEntry.getValue("CanChangeTrainerText") > 0) { writeFixedLengthString(trade.nickname, entryOffset + 3, nicknameLength); } rom[entryOffset + 3 + nicknameLength] = (byte) (trade.ivs[0] << 4 | trade.ivs[1]); rom[entryOffset + 4 + nicknameLength] = (byte) (trade.ivs[2] << 4 | trade.ivs[3]); rom[entryOffset + 5 + nicknameLength] = (byte) trade.item; writeWord(entryOffset + 6 + nicknameLength, trade.otId); if (romEntry.getValue("CanChangeTrainerText") > 0) { writeFixedLengthString(trade.otName, entryOffset + 8 + nicknameLength, otLength); } // remove gender req rom[entryOffset + 8 + nicknameLength + otLength] = 0; } } @Override public boolean hasDVs() { return true; } @Override public int generationOfPokemon() { return 2; } @Override public void removeEvosForPokemonPool() { List 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); } } } private void writeEvosAndMovesLearnt(boolean writeEvos, Map> movesets) { // this assumes that the evo/attack pointers & data // are at the end of the bank // which, in every clean G/S/C rom supported, they are // specify null to either argument to copy old values int movesEvosStart = romEntry.getValue("PokemonMovesetsTableOffset"); int movesEvosBank = bankOf(movesEvosStart); byte[] pointerTable = new byte[Gen2Constants.pokemonCount * 2]; int startOfNextBank; if (isVietCrystal) { startOfNextBank = 0x43E00; // fix for pokedex crash } else { startOfNextBank = ((movesEvosStart / GBConstants.bankSize) + 1) * GBConstants.bankSize; } int dataBlockSize = startOfNextBank - (movesEvosStart + pointerTable.length); int dataBlockOffset = movesEvosStart + pointerTable.length; byte[] dataBlock = new byte[dataBlockSize]; int offsetInData = 0; for (int i = 1; i <= Gen2Constants.pokemonCount; i++) { // determine pointer int oldDataOffset = calculateOffset(movesEvosBank, readWord(movesEvosStart + (i - 1) * 2)); int offsetStart = dataBlockOffset + offsetInData; boolean evoWritten = false; if (!writeEvos) { // copy old int evoOffset = oldDataOffset; while (rom[evoOffset] != 0x00) { int method = rom[evoOffset] & 0xFF; int limiter = (method == 5) ? 4 : 3; for (int b = 0; b < limiter; b++) { dataBlock[offsetInData++] = rom[evoOffset++]; } evoWritten = true; } } else { for (Evolution evo : pokes[i].evolutionsFrom) { // write evos dataBlock[offsetInData++] = (byte) evo.type.toIndex(2); if (evo.type == EvolutionType.LEVEL || evo.type == EvolutionType.STONE || evo.type == EvolutionType.TRADE_ITEM) { // simple types dataBlock[offsetInData++] = (byte) evo.extraInfo; } else if (evo.type == EvolutionType.TRADE) { // non-item trade dataBlock[offsetInData++] = (byte) 0xFF; } else if (evo.type == EvolutionType.HAPPINESS) { // cond 01 dataBlock[offsetInData++] = 0x01; } else if (evo.type == EvolutionType.HAPPINESS_DAY) { // cond 02 dataBlock[offsetInData++] = 0x02; } else if (evo.type == EvolutionType.HAPPINESS_NIGHT) { // cond 03 dataBlock[offsetInData++] = 0x03; } else if (evo.type == EvolutionType.LEVEL_ATTACK_HIGHER) { dataBlock[offsetInData++] = (byte) evo.extraInfo; dataBlock[offsetInData++] = 0x01; } else if (evo.type == EvolutionType.LEVEL_DEFENSE_HIGHER) { dataBlock[offsetInData++] = (byte) evo.extraInfo; dataBlock[offsetInData++] = 0x02; } else if (evo.type == EvolutionType.LEVEL_ATK_DEF_SAME) { dataBlock[offsetInData++] = (byte) evo.extraInfo; dataBlock[offsetInData++] = 0x03; } dataBlock[offsetInData++] = (byte) evo.to.number; evoWritten = true; } } // can we reuse a terminator? if (!evoWritten && offsetStart != dataBlockOffset) { // reuse last pokemon's move terminator for our evos offsetStart -= 1; } else { // write a terminator dataBlock[offsetInData++] = 0x00; } // write table entry now that we're sure of its location int pointerNow = makeGBPointer(offsetStart); writeWord(pointerTable, (i - 1) * 2, pointerNow); // moveset if (movesets == null) { // copy old int movesOffset = oldDataOffset; // move past evos while (rom[movesOffset] != 0x00) { int method = rom[movesOffset] & 0xFF; movesOffset += (method == 5) ? 4 : 3; } movesOffset++; // copy moves while (rom[movesOffset] != 0x00) { dataBlock[offsetInData++] = rom[movesOffset++]; dataBlock[offsetInData++] = rom[movesOffset++]; } } else { List moves = movesets.get(pokes[i].number); for (MoveLearnt ml : moves) { dataBlock[offsetInData++] = (byte) ml.level; dataBlock[offsetInData++] = (byte) ml.move; } } // terminator dataBlock[offsetInData++] = 0x00; } // write new data System.arraycopy(pointerTable, 0, rom, movesEvosStart, pointerTable.length); System.arraycopy(dataBlock, 0, rom, dataBlockOffset, dataBlock.length); } @Override public boolean supportsFourStartingMoves() { return (romEntry.getValue("SupportsFourStartingMoves") > 0); } @Override public List getGameBreakingMoves() { // add OHKO moves for gen2 because x acc is still broken return Gen2Constants.brokenMoves; } @Override public List getIllegalMoves() { // 3 moves that crash the game when used by self or opponent if (isVietCrystal) { return Gen2Constants.illegalVietCrystalMoves; } return new ArrayList<>(); } @Override public List getFieldMoves() { // cut, fly, surf, strength, flash, // dig, teleport, whirlpool, waterfall, // rock smash, headbutt, sweet scent // not softboiled or milk drink return Gen2Constants.fieldMoves; } @Override public List getEarlyRequiredHMMoves() { // just cut return Gen2Constants.earlyRequiredHMMoves; } @Override public boolean isRomValid() { return romEntry.expectedCRC32 == actualCRC32; } @Override public BufferedImage getMascotImage() { Pokemon mascot = randomPokemon(); while (mascot.number == Species.unown) { // Unown is banned as handling it would add a ton of extra effort. mascot = randomPokemon(); } // Each Pokemon has a front and back pic with a bank and a pointer // (3*2=6) // There is no zero-entry. int picPointer = romEntry.getValue("PicPointers") + (mascot.number - 1) * 6; int picWidth = mascot.picDimensions & 0x0F; int picHeight = (mascot.picDimensions >> 4) & 0x0F; int picBank = (rom[picPointer] & 0xFF); if (romEntry.isCrystal) { // Crystal pic banks are offset by x36 for whatever reason. picBank += 0x36; } else { // Hey, G/S are dumb too! Arbitrarily redirected bank numbers. if (picBank == 0x13) { picBank = 0x1F; } else if (picBank == 0x14) { picBank = 0x20; } else if (picBank == 0x1F) { picBank = 0x2E; } } int picOffset = calculateOffset(picBank, readWord(picPointer + 1)); Gen2Decmp mscSprite = new Gen2Decmp(rom, picOffset, picWidth, picHeight); int w = picWidth * 8; int h = picHeight * 8; // Palette? // Two colors per Pokemon + two more for shiny, unlike pics there is a // zero-entry. // Black and white are left alone at the start and end of the palette. int[] palette = new int[] { 0xFFFFFFFF, 0xFFAAAAAA, 0xFF666666, 0xFF000000 }; int paletteOffset = romEntry.getValue("PokemonPalettes") + mascot.number * 8; if (random.nextInt(10) == 0) { // Use shiny instead paletteOffset += 4; } for (int i = 0; i < 2; i++) { palette[i + 1] = GFXFunctions.conv16BitColorToARGB(readWord(paletteOffset + i * 2)); } byte[] data = mscSprite.getFlattenedData(); BufferedImage bim = GFXFunctions.drawTiledImage(data, palette, w, h, 8); GFXFunctions.pseudoTransparency(bim, palette[0]); return bim; } @Override public void writeCheckValueToROM(int value) { if (romEntry.getValue("CheckValueOffset") > 0) { int cvOffset = romEntry.getValue("CheckValueOffset"); for (int i = 0; i < 4; i++) { rom[cvOffset + i] = (byte) ((value >> (3 - i) * 8) & 0xFF); } } } }