/* * com/mmbreakfast/unlod/lod/LodFile.java * * Copyright (C) 2000 Sil Veritas (sil_the_follower_of_dark@hotmail.com) */ /* This file is part of Unlod. * * Unlod 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 2 of the License, or * (at your option) any later version. * * Unlod 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 Unlod; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* Unlod * * Copyright (C) 2000 Sil Veritas. All Rights Reserved. This work is * distributed under the W3C(R) Software License [1] 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. * [1] http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 */ package com.mmbreakfast.unlod.lod; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.gamenet.application.mm8leveleditor.lod.FileBasedModifiedLodResource; import org.gamenet.application.mm8leveleditor.lod.LodEntry; import org.gamenet.application.mm8leveleditor.lod.LodResource; import org.gamenet.util.ByteConversions; import org.gamenet.util.TaskObserver; import com.mmbreakfast.unlod.app.LodFileList.LodEntryComparator; import com.mmbreakfast.util.RandomAccessFileOutputStream; public abstract class LodFile { protected static final Class[] ENTRY_CONSTRUCTOR = new Class[] {LodFile.class, Long.TYPE}; protected File file; protected RandomAccessFileInputStream in; protected byte fileHeader[] = null; // IMPLEMENT: check up on use of this variable in subclasses protected HashMap entries = new HashMap(); protected List orderedEntries = new ArrayList(); // static protected final long LOD_SIGNATURE_OFFSET = 0L; // 3 bytes // static protected final String LOD_SIGNATURE = "LOD"; // static protected final long LOD_SIGNATURE_END_OFFSET = 3L; // 0 bytes // // static protected final long GAME_NAME_SIGNATURE_OFFSET = 4L; // 0x04 -- 3 bytes // static protected final String GAME_NAME_SIGNATURE = "MMVI"; // // static protected final long UNKNOWN1_OFFSET = 72L; // 0x48 -- 2 bytes = 0x0034 // static protected final long UNKNOWN2_OFFSET = 74L; // 0x4A -- 2 bytes = 0x004c // static protected final long UNKNOWN3_OFFSET = 76L; // 0x4C -- 4 bytes = 0x00000002 // static protected final long UNKNOWN4_OFFSET = 80L; // 0x50 -- 4 bytes = 0x00000057 // // static protected final long DESCRIPTION_OFFSET = 84L; // // 0x54 -- 17/18 bytes = "Bitmaps for MMVI" + zero // // static protected final long UNKNOWN5_OFFSET = DESCRIPTION_OFFSET; // // + description length ; // // 0x66 -- 2 bytes = 0x0002 // static protected final long UNKNOWN6_OFFSET = 104L; // 0x68 -- 4 bytes = 0x00000000 // static protected final long UNKNOWN7_OFFSET = 108L; // // 0x6c -- 4 bytes = 0x77f121fb = 2012291579 = 8699, 30705 // static protected final long UNKNOWN8_OFFSET = 112L; // 0x70 -- 4 bytes = 0xffffffff // static protected final long UNKNOWN9_OFFSET = 116L; // // 0x74 -- 4 bytes = 0x0012fe04 = 1244676 // static protected final long UNKNOWN10_OFFSET = 120L; // 0x78 -- 4 bytes = 0x00000000 // static protected final long UNKNOWN11_OFFSET = 124L; // // 0x7c -- 4 bytes = 0x0012fe08 = 1244680 // static protected final long UNKNOWN12_OFFSET = 128L; // 0x80 -- 4 bytes = 0x00000000 // static protected final long UNKNOWN13_OFFSET = 132L; // 0x84 -- 4 bytes = 0x00000040 // static protected final long UNKNOWN14_OFFSET = 136L; // // 0x88 -- 4 bytes = 0x000186a0 = 100000 // static protected final long UNKNOWN15_OFFSET = 140L; // // 0x8c -- 4 bytes = 0x0000ffd0 = 65488 // static protected final long UNKNOWN16_OFFSET = 144L; // // 0x90 -- 4 bytes = 0x000186a0 = 100000 // static protected final long UNKNOWN17_OFFSET = 148L; // // 0x94 -- 4 bytes = 0x0041b50c = 4306188 // static protected final long UNKNOWN18_OFFSET = 152L; // 0x98 -- 4 bytes = 0x00000000 // static protected final long UNKNOWN19_OFFSET = 156L; // // 0x9c -- 4 bytes = 0x0012fdcc = 1244620 // static protected final long UNKNOWN20_OFFSET = 160L; // // 0xa0 -- 4 bytes = 0x0012fe08 = 1244680 // static protected final long UNKNOWN21_OFFSET = 164L; // 0xa4 -- 4 bytes = 0x00000064 = 100 // static protected final long UNKNOWN22_OFFSET = 168L; // 0xa8 -- 4 bytes = 0x00000000 // static protected final long UNKNOWN23_OFFSET = 172L; // 0xac -- 4 bytes = 0x00000001 // static protected final long UNKNOWN24_OFFSET = 176L; // 0xb0 -- 4 bytes = 0xffffffff // static protected final long UNKNOWN25_OFFSET = 180L; // // 0xb4 -- 4 bytes = 0x0012fe14 = 1244692 // static protected final long UNKNOWN26_OFFSET = 184L; // // 0xb8 -- 4 bytes = 0x77f1217d = 2012291453 = 8573, 30705 // static protected final long UNKNOWN27_OFFSET = 188L; // 0xbc -- 4 bytes = 0xffffffff // static protected final long UNKNOWN28_OFFSET = 192L; // // 0xc0 -- 4 bytes = 0x000186a0 = 100008 // static protected final long UNKNOWN29_OFFSET = 196L; // 0xc4 -- 4 bytes = 0x00000001 // static protected final long UNKNOWN30_OFFSET = 200L; // // 0xc8 -- 2 bytes = 0x0030 = 48 // data header? data content offset? // static protected final long UNKNOWN31_OFFSET = 202L; // 0xca -- 2 bytes = 0x00b4 = 180 // static protected final long UNKNOWN32_OFFSET = 204L; // 0xcc -- 2 bytes = 0x0000 = 0 // static protected final long UNKNOWN33_OFFSET = 206L; // 0xce -- 2 bytes = 0x00b4 = 180 // static protected final long UNKNOWN34_OFFSET = 208L; // // 0xd0 -- 4 bytes = 0x004131da = 4272602 = 12762, 65 // static protected final long UNKNOWN35_OFFSET = 212L; // // 0xd4 -- 4 bytes = 0x00019000 = 102400 = 36864, 1 // static protected final long UNKNOWN36_OFFSET = 216L; // 0xd8 -- 2 bytes = 0x0000 = 0 // static protected final long UNKNOWN37_OFFSET = 218L; // 0xda -- 2 bytes = 0x00b4 = 180 // static protected final long UNKNOWN38_OFFSET = 220L; // // 0xdc -- 4 bytes = 0x0012ffac = 1245100 = 65452,18 // static protected final long UNKNOWN39_OFFSET = 224L; // 0xe0 -- 2 bytes = 0x002c = 44 // static protected final long UNKNOWN40_OFFSET = 226L; // 0xe2 -- 2 bytes = 0x00b4 = 180 // static protected final long UNKNOWN41_OFFSET = 228L; // 0xe4 -- 4 bytes = 0x00000005 // static protected final long UNKNOWN42_OFFSET = 232L; // // 0xe8 -- 4 bytes = 0x0041536c = 4281196 = 21356, 65 // static protected final long UNKNOWN43_OFFSET = 236L; // // 0xec -- 4 bytes = 0x00000020 = 32 // entry header size? // static protected final long UNKNOWN44_OFFSET = 240L; // // 0xf0 -- 4 bytes = 0x00412aae = 4270766 = 10926, 65 // static protected final long UNKNOWN45_OFFSET = 244L; // // 0xf4 -- 4 bytes = 0x0012fe54 = 1244756 = 65108, 18 // static protected final long UNKNOWN46_OFFSET = 248L; // // 0xf8 -- 4 bytes = 0x0041e6c7 = 4318919 = 59079, 65 // static protected final long UNKNOWN47_OFFSET = 252L; // // 0xfc -- 4 bytes = 0x004156e1 = 5282081 = 22241, 65 // static protected final long UNKNOWN48_OFFSET = 256L; // // 0x0100 - 8 bytes = "bitmaps" + zero // static protected final String UNKNOWN48_STRING = "bitmaps"; // // 0x0100 - 8 bytes = "bitmaps" + zero // static protected final long UNKNOWN49_OFFSET = 264L; // 0x0108 -- 2 bytes = 183 // static protected final long UNKNOWN50_OFFSET = 266L; // 0x010a -- 2 bytes = 179 // static protected final long UNKNOWN51_OFFSET = 268L; // // 0x010c -- 4 bytes = 0x00415346 = 4270918 = 11078, 65 static protected final long FILE_HEADER_SIZE_OFFSET = 272L; // // 0x0110 -- 4 bytes = 0x00000120 = 288 static protected final long FILE_SIZE_MINUS_FILE_HEADER_SIZE_OFFSET = 276L; // // 0x0114 -- 4 bytes = 0x02c82ef1 = 46673649 // static protected final long UNKNOWN54_OFFSET = 280L; // 0x0118 -- 4 bytes = 0x00000000 // static protected final long ENTRY_COUNT_OFFSET = 284L; // // 0x011c -- 4 bytes = 0x000007a6 = 1958, 0 :: 071a = 1818 protected LodFile(File file, RandomAccessFileInputStream inputStream) throws IOException, InvalidLodFileException { this.file = file; this.in = inputStream; RandomAccessFile raf = in.getFile(); verify(raf); fileHeader = readFileHeader(raf); long entryCount = readEntryCount(raf); readEntries(raf, entryCount); } public File getFile() { return file; } public String getFileName() { return file.getName(); } protected void verifySignature(RandomAccessFile raf) throws IOException, InvalidLodFileException { } protected void verify(RandomAccessFile raf) throws IOException, InvalidLodFileException { verifySignature(raf); } protected byte[] readFileHeader(RandomAccessFile raf) throws IOException { int headerEndLocation = (int)getHeaderOffset(); byte[] fileHeader = new byte[headerEndLocation]; raf.seek(0L); raf.read(fileHeader, 0, headerEndLocation); return fileHeader; } protected long readEntryCount(RandomAccessFile raf) throws IOException, InvalidLodFileException { raf.seek(getEntriesNumberOffset()); long entryCount = 0L; int aByte = raf.read(); if (aByte != -1) entryCount = (long)aByte; for (int byteIndex = 1; byteIndex < 4 && aByte != -1; byteIndex++) { aByte = raf.read(); entryCount += (long)aByte << 8 * byteIndex; } if (entryCount == 0L) throw new InvalidLodFileException("Number of entries is NUL"); return entryCount; } protected void readEntries(RandomAccessFile raf, long entryCount) throws IOException { raf.seek(getHeaderOffset()); LodEntry currentEntry; for (int entryIndex = 0; ((long)entryIndex < entryCount && (currentEntry = getNextEntry(entryIndex, raf)) != null); entryIndex++) { String entryNameToStore = currentEntry.getFileName().toLowerCase(); if (null != entries.get(entryNameToStore)) { if (entryNameToStore.equals("header.bin")) { System.out.println("Unclear what to do with duplicate header.bin in new.lod files."); entryNameToStore = "header.bin.duplicate"; } else { throw new RuntimeException("Duplicate entry " + entryIndex + " named '" + currentEntry.getFileName() + "'"); } } entries.put(entryNameToStore, currentEntry); orderedEntries.add(currentEntry); } } private LodResource findFileBasedModifiedLodResource(List modifiedFileList, LodEntry lodEntry) { Iterator fileIterator = modifiedFileList.iterator(); while (fileIterator.hasNext()) { File file = (File)fileIterator.next(); String fileName = file.getName(); if (lodEntry.getFormatConverter().isFileNameComponentForLodEntryFileName(file.getName(), lodEntry.getFileName())) return new FileBasedModifiedLodResource(lodEntry.getFormatConverter(), file); } return null; } public LodEntry findLodEntryForFile(File file) { List lodEntries = new ArrayList(getLodEntries().values()); Iterator lodEntryIterator = lodEntries.iterator(); while (lodEntryIterator.hasNext()) { LodEntry lodEntry = (LodEntry)lodEntryIterator.next(); if (lodEntry.getFormatConverter().isFileNameComponentForLodEntryFileName(file.getName(), lodEntry.getFileName())) return lodEntry; } return null; } public byte[] getFileHeader() { return fileHeader; } public Map getLodEntries() { return entries; } // needed to retrieve palettes from Bitmap.lod for sprite.lod entries public LodEntry findLodEntryByFileName(String string) throws NoSuchEntryException { if (false == entries.containsKey(string.toLowerCase())) throw new NoSuchEntryException(string); return (LodEntry)entries.get(string.toLowerCase()); } protected LodEntry getNextEntry(int entriesSoFar, RandomAccessFile raf) throws IOException { long offset = this.getHeaderOffset() + this.getEntryHeaderLength() * (long)entriesSoFar; raf.seek(offset); // The class we choose for instantiation is that of lodFile.getEntryClass() try { return (LodEntry) this.getEntryClass().getConstructor(ENTRY_CONSTRUCTOR).newInstance(new Object[] {this, new Long(offset)}); } catch (NoSuchMethodException e) { e.printStackTrace(); return null; } catch (InvocationTargetException e) { e.printStackTrace(); return null; } catch (InstantiationException e) { e.printStackTrace(); return null; } catch (IllegalAccessException e) { e.printStackTrace(); return null; } } public void write(TaskObserver taskObserver, RandomAccessFile randomAccessFile, List modifiedFileList, LodResource resourceToImport) throws IOException, InterruptedException { RandomAccessFileOutputStream randomAccessFileOutputStream = new RandomAccessFileOutputStream(randomAccessFile); // process data offets in order of current dataOffset ordering LodEntryComparator dataOffsetLodEntryComparator = new LodEntryComparator() { public int compare(Object o1, Object o2) { LodEntry le1 = (LodEntry)o1; LodEntry le2 = (LodEntry)o2; return le1.getDataOffset() < le2.getDataOffset() ? -1 : 1; } public String getDisplayName() { return "data offset"; } }; List lodEntriesSortedByDataOffset = new ArrayList(getLodEntries().values()); Collections.sort(lodEntriesSortedByDataOffset, dataOffsetLodEntryComparator); // Compute offsets int oldEntriesCount = lodEntriesSortedByDataOffset.size(); int newEntriesCount = 0; // IMPLEMENT: append new entries at end int totalEntriesCount = oldEntriesCount + newEntriesCount; // start dataOffset after entries long computedDataOffset = this.getFileHeader().length + totalEntriesCount * this.getEntryHeaderLength(); float counter = 0; Iterator lodEntryByDataOffsetIterator = lodEntriesSortedByDataOffset.iterator(); while (lodEntryByDataOffsetIterator.hasNext()) { if (Thread.currentThread().isInterrupted()) throw new InterruptedException("write thread was interrupted."); long newComputedDataOffset = 0; LodEntry lodEntry = (LodEntry)lodEntryByDataOffsetIterator.next(); // write out data randomAccessFileOutputStream.getFile().seek(computedDataOffset); LodResource lodResource = null; if (null != modifiedFileList) { lodResource = findFileBasedModifiedLodResource(modifiedFileList, lodEntry); } if (null != resourceToImport) { if (resourceToImport.getName().equals(lodEntry.getEntryName())) { lodResource = resourceToImport; } } if (null == lodResource) { taskObserver.taskProgress("Reusing data " + lodEntry.getFileName(), counter++ / totalEntriesCount); System.out.println("Reusing data " + lodEntry.getFileName()); newComputedDataOffset = lodEntry.rewriteData(randomAccessFileOutputStream, computedDataOffset); } else { taskObserver.taskProgress("Replacing data " + lodEntry.getFileName(), counter++ / totalEntriesCount); System.out.println("Replacing data " + lodEntry.getFileName()); newComputedDataOffset = lodEntry.updateData(lodResource, randomAccessFileOutputStream, computedDataOffset); } // System.out.println(lodEntry.getName() + ": new dataoffset " + computedDataOffset); if (Thread.currentThread().isInterrupted()) throw new InterruptedException("write thread was interrupted."); // entryOffset for this entry long entryOffset = lodEntry.getEntryOffset(); // dataOffset is set for next entry // System.out.println(lodEntry.getName() + " - computedOffset: " + String.valueOf(computedDataOffset)); // write out entry randomAccessFileOutputStream.getFile().seek(entryOffset); if (null == lodResource) { System.out.println("Reusing entry " + lodEntry.getFileName()); long newDataOffset = lodEntry.rewriteEntry(randomAccessFileOutputStream, this.getFileHeader().length, entryOffset, computedDataOffset); if (newDataOffset != newComputedDataOffset) { throw new RuntimeException("newDataOffset:" + newDataOffset + " != newComputedDataOffset:" + newComputedDataOffset); } } else { System.out.println("Replacing entry " + lodEntry.getFileName()); long newDataOffset = lodEntry.updateEntry(lodResource, randomAccessFileOutputStream, this.getFileHeader().length, entryOffset, computedDataOffset); if (newDataOffset != newComputedDataOffset) { throw new RuntimeException("newDataOffset:" + newDataOffset + " != newComputedDataOffset:" + newComputedDataOffset); } } // System.out.println(); computedDataOffset = newComputedDataOffset; } // System.out.println("Wrote entries.\n\n"); // copy file header byte newFileHeader[] = new byte[this.fileHeader.length]; System.arraycopy(this.fileHeader, 0, newFileHeader, 0, this.fileHeader.length); // update file header ByteConversions.setIntegerInByteArrayAtPosition(totalEntriesCount, newFileHeader, (int)this.getEntriesNumberOffset()); if (-1 != getFileHeaderSizeOffset()) ByteConversions.setIntegerInByteArrayAtPosition(this.getFileHeader().length, newFileHeader, (int)getFileHeaderSizeOffset()); if (-1 != getFileSizeMinusFileHeaderSizeOffset()) ByteConversions.setIntegerInByteArrayAtPosition(computedDataOffset - this.getFileHeader().length, newFileHeader, (int)getFileSizeMinusFileHeaderSizeOffset()); // write out file header randomAccessFile.seek(0L); randomAccessFile.write(newFileHeader); // System.out.println("Wrote header.\n\n"); } public void updateByAppendingData(TaskObserver taskObserver, RandomAccessFile randomAccessFile, LodResource resourceToImport) throws IOException, InterruptedException { taskObserver.taskProgress(resourceToImport.getName(), 0); LodEntry lodEntryToUpdate = null; float counter = 0; Iterator lodEntryByDataOffsetIterator = getLodEntries().values().iterator(); while (lodEntryByDataOffsetIterator.hasNext()) { if (Thread.currentThread().isInterrupted()) throw new InterruptedException("append thread was interrupted."); LodEntry lodEntry = (LodEntry)lodEntryByDataOffsetIterator.next(); if (resourceToImport.getName().equals(lodEntry.getEntryName())) { lodEntryToUpdate = lodEntry; break; } else if (resourceToImport.getName().equals(lodEntry.getFileName())) { lodEntryToUpdate = lodEntry; break; } } taskObserver.taskProgress(resourceToImport.getName(), 0.10f); if (null == lodEntryToUpdate) { throw new RuntimeException("Unable to find lod entry for '" + resourceToImport.getName() + "'."); } long oldFileSize = lodEntryToUpdate.getLodFile().getFile().length(); RandomAccessFileOutputStream randomAccessFileOutputStream = new RandomAccessFileOutputStream(randomAccessFile); // append data content randomAccessFileOutputStream.getFile().seek(oldFileSize); long newFileSize = lodEntryToUpdate.updateData(resourceToImport, randomAccessFileOutputStream, oldFileSize); taskObserver.taskProgress(resourceToImport.getName(), 0.90f); if (Thread.currentThread().isInterrupted()) throw new InterruptedException("append thread was interrupted."); // update entry content long entryOffset = lodEntryToUpdate.getEntryOffset(); randomAccessFileOutputStream.getFile().seek(entryOffset); long newFileSize2 = lodEntryToUpdate.updateEntry(resourceToImport, randomAccessFileOutputStream, this.getFileHeader().length, entryOffset, oldFileSize); if (newFileSize != newFileSize2) throw new RuntimeException("Data size was " + newFileSize + ", but entry computed size as " + newFileSize2); if (Thread.currentThread().isInterrupted()) throw new InterruptedException("append thread was interrupted."); taskObserver.taskProgress(resourceToImport.getName(), 0.99f); // update file header long fileSizeMinusFileHeaderSize = newFileSize - this.getFileHeader().length; if (-1 != getFileSizeMinusFileHeaderSizeOffset()) { byte newFileSizeMinusFileHeaderSize[] = new byte[4]; ByteConversions.setIntegerInByteArrayAtPosition(fileSizeMinusFileHeaderSize, newFileSizeMinusFileHeaderSize, 0); randomAccessFile.seek(getFileSizeMinusFileHeaderSizeOffset()); randomAccessFile.write(newFileSizeMinusFileHeaderSize); } } public RandomAccessFileInputStream getRandomAccessFileInputStream() { return in; } protected long getFileHeaderSizeOffset() { return FILE_HEADER_SIZE_OFFSET; } protected long getFileSizeMinusFileHeaderSizeOffset() { return FILE_SIZE_MINUS_FILE_HEADER_SIZE_OFFSET; } public abstract long getHeaderOffset(); public abstract long getEntriesNumberOffset(); public abstract long getEntryHeaderLength(); public abstract Class getEntryClass(); }