package com.pkrandom.newnds;
/*----------------------------------------------------------------------------*/
/*-- NARCArchive.java - class for packing/unpacking GARC archives --*/
/*-- --*/
/*-- 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 . --*/
/*----------------------------------------------------------------------------*/
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class NARCArchive {
private List filenames = new ArrayList<>();
public List files = new ArrayList<>();
private boolean hasFilenames = false;
public NARCArchive() {
// creates a new empty NARC with no filenames by default
}
public NARCArchive(byte[] data) throws IOException {
Map frames = readNitroFrames(data);
if (!frames.containsKey("FATB") || !frames.containsKey("FNTB") || !frames.containsKey("FIMG")) {
throw new IOException("Not a valid narc file");
}
// File contents
byte[] fatbframe = frames.get("FATB");
byte[] fimgframe = frames.get("FIMG");
int fileCount = readLong(fatbframe, 0);
for (int i = 0; i < fileCount; i++) {
int startOffset = readLong(fatbframe, 4 + i * 8);
int endOffset = readLong(fatbframe, 8 + i * 8);
int length = (endOffset - startOffset);
byte[] thisFile = new byte[length];
System.arraycopy(fimgframe, startOffset, thisFile, 0, length);
files.add(thisFile);
}
// Filenames?
byte[] fntbframe = frames.get("FNTB");
int unk1 = readLong(fntbframe, 0);
if (unk1 == 8) {
// Filenames exist
hasFilenames = true;
int offset = 8;
for (int i = 0; i < fileCount; i++) {
int fnLength = (fntbframe[offset] & 0xFF);
offset++;
byte[] filenameBA = new byte[fnLength];
System.arraycopy(fntbframe, offset, filenameBA, 0, fnLength);
String filename = new String(filenameBA, "US-ASCII");
filenames.add(filename);
}
} else {
hasFilenames = false;
for (int i = 0; i < fileCount; i++) {
filenames.add(null);
}
}
}
public byte[] getBytes() throws IOException {
// Get bytes required for FIMG frame
int bytesRequired = 0;
for (byte[] file : files) {
bytesRequired += Math.ceil(file.length / 4.0) * 4;
}
// FIMG frame & FATB frame build
// 4 for numentries, 8*size for entries, 8 for nitro header
byte[] fatbFrame = new byte[4 + files.size() * 8 + 8];
// bytesRequired + 8 for nitro header
byte[] fimgFrame = new byte[bytesRequired + 8];
// Nitro headers
fatbFrame[0] = 'B';
fatbFrame[1] = 'T';
fatbFrame[2] = 'A';
fatbFrame[3] = 'F';
writeLong(fatbFrame, 4, fatbFrame.length);
fimgFrame[0] = 'G';
fimgFrame[1] = 'M';
fimgFrame[2] = 'I';
fimgFrame[3] = 'F';
writeLong(fimgFrame, 4, fimgFrame.length);
int offset = 0;
writeLong(fatbFrame, 8, files.size());
for (int i = 0; i < files.size(); i++) {
byte[] file = files.get(i);
int bytesRequiredForFile = (int) (Math.ceil(file.length / 4.0) * 4);
System.arraycopy(file, 0, fimgFrame, offset + 8, file.length);
for (int filler = file.length; filler < bytesRequiredForFile; filler++) {
fimgFrame[offset + 8 + filler] = (byte) 0xFF;
}
writeLong(fatbFrame, 12 + i * 8, offset);
writeLong(fatbFrame, 16 + i * 8, offset + file.length);
offset += bytesRequiredForFile;
}
// FNTB Frame
int bytesForFNTBFrame = 16;
if (hasFilenames) {
for (String filename : filenames) {
bytesForFNTBFrame += filename.getBytes("US-ASCII").length + 1;
}
}
byte[] fntbFrame = new byte[bytesForFNTBFrame];
fntbFrame[0] = 'B';
fntbFrame[1] = 'T';
fntbFrame[2] = 'N';
fntbFrame[3] = 'F';
writeLong(fntbFrame, 4, fntbFrame.length);
if (hasFilenames) {
writeLong(fntbFrame, 8, 8);
writeLong(fntbFrame, 12, 0x10000);
int fntbOffset = 16;
for (String filename : filenames) {
byte[] fntbfilename = filename.getBytes("US-ASCII");
fntbFrame[fntbOffset] = (byte) fntbfilename.length;
System.arraycopy(fntbfilename, 0, fntbFrame, fntbOffset + 1, fntbfilename.length);
fntbOffset += 1 + fntbfilename.length;
}
} else {
writeLong(fntbFrame, 8, 4);
writeLong(fntbFrame, 12, 0x10000);
}
// Now for the actual Nitro file
int nitrolength = 16 + fatbFrame.length + fntbFrame.length + fimgFrame.length;
byte[] nitroFile = new byte[nitrolength];
nitroFile[0] = 'N';
nitroFile[1] = 'A';
nitroFile[2] = 'R';
nitroFile[3] = 'C';
writeWord(nitroFile, 4, 0xFFFE);
writeWord(nitroFile, 6, 0x0100);
writeLong(nitroFile, 8, nitrolength);
writeWord(nitroFile, 12, 0x10);
writeWord(nitroFile, 14, 3);
System.arraycopy(fatbFrame, 0, nitroFile, 16, fatbFrame.length);
System.arraycopy(fntbFrame, 0, nitroFile, 16 + fatbFrame.length, fntbFrame.length);
System.arraycopy(fimgFrame, 0, nitroFile, 16 + fatbFrame.length + fntbFrame.length, fimgFrame.length);
return nitroFile;
}
private Map readNitroFrames(byte[] data) throws IOException {
// Read the number of frames
int frameCount = readWord(data, 0x0E);
// each frame
int offset = 0x10;
Map frames = new TreeMap<>();
for (int i = 0; i < frameCount; i++) {
byte[] magic = new byte[] { data[offset + 3], data[offset + 2], data[offset + 1], data[offset] };
String magicS = new String(magic, "US-ASCII");
int frame_size = readLong(data, offset + 4);
// Patch for BB/VW and other DS hacks which don't update
// the size of their expanded NARCs correctly
if (i == frameCount - 1 && offset + frame_size < data.length) {
frame_size = data.length - offset;
}
byte[] frame = new byte[frame_size - 8];
System.arraycopy(data, offset + 8, frame, 0, frame_size - 8);
frames.put(magicS, frame);
offset += frame_size;
}
return frames;
}
private int readWord(byte[] data, int offset) {
return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
}
private 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);
}
private void writeWord(byte[] data, int offset, int value) {
data[offset] = (byte) (value & 0xFF);
data[offset + 1] = (byte) ((value >> 8) & 0xFF);
}
private 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);
}
}