package com.sneed.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 . --*/
/*----------------------------------------------------------------------------*/
import com.sneed.pkrandom.FileFunctions;
import com.sneed.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 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);
}
}