diff options
Diffstat (limited to 'src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java')
-rw-r--r-- | src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java b/src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java new file mode 100644 index 0000000..94b7111 --- /dev/null +++ b/src/com/pkrandom/romhandlers/Abstract3DSRomHandler.java @@ -0,0 +1,350 @@ +package com.pkrandom.romhandlers; + +/*----------------------------------------------------------------------------*/ +/*-- Abstract3DSRomHandler.java - a base class for 3DS rom handlers --*/ +/*-- which standardises common 3DS functions. --*/ +/*-- --*/ +/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/ +/*-- Pokemon and any associated names and the like are --*/ +/*-- trademark and (C) Nintendo 1996-2020. --*/ +/*-- --*/ +/*-- The custom code written here is licensed under the terms of the GPL: --*/ +/*-- --*/ +/*-- This program is free software: you can redistribute it and/or modify --*/ +/*-- it under the terms of the GNU General Public License as published by --*/ +/*-- the Free Software Foundation, either version 3 of the License, or --*/ +/*-- (at your option) any later version. --*/ +/*-- --*/ +/*-- This program is distributed in the hope that it will be useful, --*/ +/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/ +/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/ +/*-- GNU General Public License for more details. --*/ +/*-- --*/ +/*-- You should have received a copy of the GNU General Public License --*/ +/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/ +/*----------------------------------------------------------------------------*/ + +import com.pkrandom.FileFunctions; +import com.pkrandom.ctr.GARCArchive; +import com.pkrandom.ctr.NCCH; +import com.pkrandom.exceptions.CannotWriteToLocationException; +import com.pkrandom.exceptions.EncryptedROMException; +import com.pkrandom.exceptions.RandomizerIOException; +import com.pkrandom.pokemon.Type; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Random; + +public abstract class Abstract3DSRomHandler extends AbstractRomHandler { + + private NCCH baseRom; + private NCCH gameUpdate; + private String loadedFN; + + public Abstract3DSRomHandler(Random random, PrintStream logStream) { + super(random, logStream); + } + + @Override + public boolean loadRom(String filename) { + String productCode = getProductCodeFromFile(filename); + String titleId = getTitleIdFromFile(filename); + if (!this.detect3DSRom(productCode, titleId)) { + return false; + } + // Load inner rom + try { + baseRom = new NCCH(filename, productCode, titleId); + if (!baseRom.isDecrypted()) { + throw new EncryptedROMException(filename); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + loadedFN = filename; + this.loadedROM(productCode, titleId); + return true; + } + + protected abstract boolean detect3DSRom(String productCode, String titleId); + + @Override + public String loadedFilename() { + return loadedFN; + } + + protected abstract void loadedROM(String productCode, String titleId); + + protected abstract void savingROM() throws IOException; + + protected abstract String getGameAcronym(); + + @Override + public boolean saveRomFile(String filename, long seed) { + try { + savingROM(); + baseRom.saveAsNCCH(filename, getGameAcronym(), seed); + } catch (IOException | NoSuchAlgorithmException e) { + if (e.getMessage().contains("Access is denied")) { + throw new CannotWriteToLocationException("The randomizer cannot write to this location: " + filename); + } else { + throw new RandomizerIOException(e); + } + } + return true; + } + + @Override + public boolean saveRomDirectory(String filename) { + try { + savingROM(); + baseRom.saveAsLayeredFS(filename); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + return true; + } + + protected abstract boolean isGameUpdateSupported(int version); + + @Override + public boolean hasGameUpdateLoaded() { + return gameUpdate != null; + } + + @Override + public boolean loadGameUpdate(String filename) { + String productCode = getProductCodeFromFile(filename); + String titleId = getTitleIdFromFile(filename); + try { + gameUpdate = new NCCH(filename, productCode, titleId); + if (!gameUpdate.isDecrypted()) { + throw new EncryptedROMException(filename); + } + int version = gameUpdate.getVersion(); + if (!this.isGameUpdateSupported(version)) { + System.out.println("Game Update: Supplied unexpected version " + version); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + this.loadedROM(baseRom.getProductCode(), baseRom.getTitleId()); + return true; + } + + @Override + public void removeGameUpdate() { + gameUpdate = null; + this.loadedROM(baseRom.getProductCode(), baseRom.getTitleId()); + } + + protected abstract String getGameVersion(); + + @Override + public String getGameUpdateVersion() { + return getGameVersion(); + } + + @Override + public void printRomDiagnostics(PrintStream logStream) { + baseRom.printRomDiagnostics(logStream, gameUpdate); + } + + public void closeInnerRom() throws IOException { + baseRom.closeROM(); + } + + @Override + public boolean hasPhysicalSpecialSplit() { + // Default value for Gen4+. + // Handlers can override again in case of ROM hacks etc. + return true; + } + + protected byte[] readCode() throws IOException { + if (gameUpdate != null) { + return gameUpdate.getCode(); + } + return baseRom.getCode(); + } + + protected void writeCode(byte[] data) throws IOException { + baseRom.writeCode(data); + } + + protected GARCArchive readGARC(String subpath, boolean skipDecompression) throws IOException { + return new GARCArchive(readFile(subpath),skipDecompression); + } + + protected GARCArchive readGARC(String subpath, List<Boolean> compressThese) throws IOException { + return new GARCArchive(readFile(subpath),compressThese); + } + + protected void writeGARC(String subpath, GARCArchive garc) throws IOException { + this.writeFile(subpath,garc.getBytes()); + } + + protected byte[] readFile(String location) throws IOException { + if (gameUpdate != null && gameUpdate.hasFile(location)) { + return gameUpdate.getFile(location); + } + return baseRom.getFile(location); + } + + protected void writeFile(String location, byte[] data) throws IOException { + writeFile(location, data, 0, data.length); + } + + protected void readByteIntoFlags(byte[] data, boolean[] flags, int offsetIntoFlags, int offsetIntoData) { + int thisByte = data[offsetIntoData] & 0xFF; + for (int i = 0; i < 8 && (i + offsetIntoFlags) < flags.length; i++) { + flags[offsetIntoFlags + i] = ((thisByte >> i) & 0x01) == 0x01; + } + } + + protected byte getByteFromFlags(boolean[] flags, int offsetIntoFlags) { + int thisByte = 0; + for (int i = 0; i < 8 && (i + offsetIntoFlags) < flags.length; i++) { + thisByte |= (flags[offsetIntoFlags + i] ? 1 : 0) << i; + } + return (byte) thisByte; + } + + protected int readWord(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8); + } + + protected void writeWord(byte[] data, int offset, int value) { + data[offset] = (byte) (value & 0xFF); + data[offset + 1] = (byte) ((value >> 8) & 0xFF); + } + + protected int readLong(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8) | ((data[offset + 2] & 0xFF) << 16) + | ((data[offset + 3] & 0xFF) << 24); + } + + protected void writeLong(byte[] data, int offset, int value) { + data[offset] = (byte) (value & 0xFF); + data[offset + 1] = (byte) ((value >> 8) & 0xFF); + data[offset + 2] = (byte) ((value >> 16) & 0xFF); + data[offset + 3] = (byte) ((value >> 24) & 0xFF); + } + + protected void writeFile(String location, byte[] data, int offset, int length) throws IOException { + if (offset != 0 || length != data.length) { + byte[] newData = new byte[length]; + System.arraycopy(data, offset, newData, 0, length); + data = newData; + } + baseRom.writeFile(location, data); + if (gameUpdate != null && gameUpdate.hasFile(location)) { + gameUpdate.writeFile(location, data); + } + } + + public String getTitleIdFromLoadedROM() { + return baseRom.getTitleId(); + } + + protected static String getProductCodeFromFile(String filename) { + try { + long ncchStartingOffset = NCCH.getCXIOffsetInFile(filename); + if (ncchStartingOffset == -1) { + return null; + } + FileInputStream fis = new FileInputStream(filename); + fis.skip(ncchStartingOffset + 0x150); + byte[] productCode = FileFunctions.readFullyIntoBuffer(fis, 0x10); + fis.close(); + return new String(productCode, "UTF-8").trim(); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + public static String getTitleIdFromFile(String filename) { + try { + long ncchStartingOffset = NCCH.getCXIOffsetInFile(filename); + if (ncchStartingOffset == -1) { + return null; + } + FileInputStream fis = new FileInputStream(filename); + fis.skip(ncchStartingOffset + 0x118); + byte[] programId = FileFunctions.readFullyIntoBuffer(fis, 0x8); + fis.close(); + reverseArray(programId); + return bytesToHex(programId); + } catch (IOException e) { + throw new RandomizerIOException(e); + } + } + + private static void reverseArray(byte[] bytes) { + for (int i = 0; i < bytes.length / 2; i++) { + byte temp = bytes[i]; + bytes[i] = bytes[bytes.length - i - 1]; + bytes[bytes.length - i - 1] = temp; + } + } + + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int i = 0; i < bytes.length; i++) { + int unsignedByte = bytes[i] & 0xFF; + hexChars[i * 2] = HEX_ARRAY[unsignedByte >>> 4]; + hexChars[i * 2 + 1] = HEX_ARRAY[unsignedByte & 0x0F]; + } + return new String(hexChars); + } + + protected int typeTMPaletteNumber(Type t, boolean isGen7) { + if (t == null) { + return 322; // CURSE + } + switch (t) { + case DARK: + return 309; + case DRAGON: + return 310; + case PSYCHIC: + return 311; + case NORMAL: + return 312; + case POISON: + return 313; + case ICE: + return 314; + case FIGHTING: + return 315; + case FIRE: + return 316; + case WATER: + return 317; + case FLYING: + return 323; + case GRASS: + return 318; + case ROCK: + return 319; + case ELECTRIC: + return 320; + case GROUND: + return 321; + case GHOST: + default: + return 322; // for CURSE + case STEEL: + return 324; + case BUG: + return 325; + case FAIRY: + return isGen7 ? 555 : 546; + } + } +} |