diff options
Diffstat (limited to 'src/com/pkrandom/ctr/AMX.java')
-rw-r--r-- | src/com/pkrandom/ctr/AMX.java | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/src/com/pkrandom/ctr/AMX.java b/src/com/pkrandom/ctr/AMX.java new file mode 100644 index 0000000..d99ba7f --- /dev/null +++ b/src/com/pkrandom/ctr/AMX.java @@ -0,0 +1,227 @@ +package com.pkrandom.ctr; + +/*----------------------------------------------------------------------------*/ +/*-- AMX.java - class for handling AMX script archives --*/ +/*-- --*/ +/*-- Contains code based on "pk3DS", copyright (C) Kaphotics --*/ +/*-- Contains code based on "pkNX", copyright (C) Kaphotics --*/ +/*-- Contains code based on "poketools", copyright (C) FireyFly --*/ +/*-- Additional contributions by the UPR-ZX team --*/ +/*-- --*/ +/*-- 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.exceptions.RandomizerIOException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class AMX { + + public byte[] decData; + public int scriptOffset = 0; + + private int amxMagic = 0x0A0AF1E0; + private int amxMagicDebug = 0x0A0AF1EF; + private long mask = 0xFF; + + private int length; + + private int scriptInstrStart; + private int scriptMovementStart; + private int finalOffset; + private int allocatedMemory; + + private int compLength; + private int decompLength; + + private int ptrOffset; + private int ptrCount; + + private byte[] extraData; + + public AMX(byte[] data, int scriptNum) throws IOException { + int found = 0; + for (int i = 0; i < data.length - 3; i++) { + int val = FileFunctions.readFullInt(data,i); + if (val == amxMagic) { + if (found == scriptNum) { + int length = FileFunctions.readFullInt(data,i-4); + readHeaderAndDecompress(Arrays.copyOfRange(data,i-4,i-4+length)); + scriptOffset = i-4; + break; + } else { + found++; + } + } + } + } + + public AMX(byte[] encData) throws IOException { + readHeaderAndDecompress(encData); + } + + // Credit to the creators of pk3DS (Kaphotics et al) + private void readHeaderAndDecompress(byte[] encData) throws IOException { + length = FileFunctions.readFullInt(encData,0); + int magic = FileFunctions.readFullInt(encData,4); + if (magic != amxMagic) { + throw new IOException(); + } + + ptrOffset = FileFunctions.read2ByteInt(encData,8); + ptrCount = FileFunctions.read2ByteInt(encData,0xA); + + scriptInstrStart = FileFunctions.readFullInt(encData,0xC); + scriptMovementStart = FileFunctions.readFullInt(encData,0x10); + finalOffset = FileFunctions.readFullInt(encData,0x14); + allocatedMemory = FileFunctions.readFullInt(encData,0x18); + + compLength = length - scriptInstrStart; + byte[] compressedBytes = Arrays.copyOfRange(encData,scriptInstrStart,length); + decompLength = finalOffset - scriptInstrStart; + + decData = decompressBytes(compressedBytes, decompLength); + extraData = Arrays.copyOfRange(encData,0x1C,scriptInstrStart); + } + + // Credit to FireyFly + private byte[] decompressBytes(byte[] data, int length) { + byte[] code = new byte[length]; + int i = 0, j = 0, x = 0, f = 0; + while (i < code.length) { + int b = data[f++]; + int v = b & 0x7F; + if (++j == 1) { + x = ((((v >>> 6 == 0 ? 1 : 0) - 1 ) << 6) | v); + } else { + x = (x << 7) | (v & 0xFF); + } + if ((b & 0x80) != 0) continue; + code[i++] = (byte)(x & 0xFF); + code[i++] = (byte)((x >>> 8) & 0xFF); + code[i++] = (byte)((x >>> 16) & 0xFF); + code[i++] = (byte)((x >>> 24) & 0xFF); + j = 0; + } + return code; + } + + public byte[] getBytes() { + + ByteBuffer bbuf = ByteBuffer.allocate(length*2); + + bbuf.order(ByteOrder.LITTLE_ENDIAN); + + bbuf.putInt(length); + bbuf.putInt(amxMagic); + bbuf.putShort((short)ptrOffset); + bbuf.putShort((short)ptrCount); + bbuf.putInt(scriptInstrStart); + bbuf.putInt(scriptMovementStart); + bbuf.putInt(finalOffset); + bbuf.putInt(allocatedMemory); + bbuf.put(extraData); + bbuf.put(compressScript(decData)); + bbuf.flip(); + bbuf.putInt(bbuf.limit()); + + return Arrays.copyOfRange(bbuf.array(),0,bbuf.limit()); + } + + private byte[] compressScript(byte[] data) { + if (data == null || data.length % 4 != 0) { + return null; + } + ByteBuffer inBuf = ByteBuffer.wrap(data); + inBuf.order(ByteOrder.LITTLE_ENDIAN); + + ByteArrayOutputStream out = new ByteArrayOutputStream(compLength); + + try { + while (inBuf.position() < data.length) { + compressBytes(inBuf, out); + } + } catch (IOException e) { + throw new RandomizerIOException(e); + } + + return out.toByteArray(); + } + + // Modified version of the AMX script compression algorithm from pkNX + private void compressBytes(ByteBuffer inBuf, ByteArrayOutputStream out) throws IOException { + List<Byte> bytes = new ArrayList<>(); + int instructionTemp = inBuf.getInt(inBuf.position()); + long instruction = Integer.toUnsignedLong(instructionTemp); + boolean sign = (instruction & 0x80000000) > 0; + + // Signed (negative) values are handled opposite of unsigned (positive) values. + // Positive values are "done" when we've shifted the value down to zero, but + // we don't need to store the highest 1s in a signed value. We handle this by + // tracking the loop via a NOTed shadow copy of the instruction if it's signed. + int shadowTemp = sign ? ~instructionTemp : instructionTemp; + long shadow = Integer.toUnsignedLong(shadowTemp); + do + { + long least7 = instruction & 0b01111111; + byte byteVal = (byte)least7; + + if (bytes.size() > 0) + { + // Continuation bit on all but the lowest byte + byteVal |= 0x80; + } + + bytes.add(byteVal); + + instruction >>= 7; + shadow >>= 7; + } + while (shadow != 0); + + if (bytes.size() < 5) + { + // Ensure "sign bit" (bit just to the right of highest continuation bit) is + // correct. Add an extra empty continuation byte if we need to. Values can't + // be longer than 5 bytes, though. + + int signBit = sign ? 0x40 : 0x00; + + if ((bytes.get(bytes.size() - 1) & 0x40) != signBit) + bytes.add((byte)(sign ? 0xFF : 0x80)); + } + + // Reverse for endianess + for (int i = 0; i < bytes.size() / 2; i++) { + byte temp = bytes.get(i); + bytes.set(i, bytes.get(bytes.size() - i - 1)); + bytes.set(bytes.size() - i - 1, temp); + } + + byte[] ret = new byte[bytes.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = bytes.get(i); + } + + inBuf.position(inBuf.position() + 4); + out.write(ret); + } +} |