/* ** 2014 December 03 ** ** The author disclaims copyright to this source code. In place of ** a legal notice, here is a blessing: ** May you do good and not evil. ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. */ package info.ata4.junity.bundle; import info.ata4.io.DataWriter; import info.ata4.io.DataWriters; import info.ata4.io.lzma.LzmaEncoderProps; import info.ata4.junity.progress.Progress; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import static java.nio.file.StandardOpenOption.*; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import net.contrapunctus.lzma.LzmaOutputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.tuple.MutablePair; /** * * @author Nico Bergemann <barracuda415 at yahoo.de> */ public class BundleWriter implements Closeable { private final DataWriter out; private final Map<BundleEntry, MutablePair<Long, Long>> levelOffsetMap = new LinkedHashMap<>(); private final Path dataFile; private Bundle bundle; public BundleWriter(Path file) throws IOException { out = DataWriters.forFile(file, CREATE, WRITE, TRUNCATE_EXISTING); dataFile = Files.createTempFile(file.getParent(), "uncompressedData", null); } public void write(Bundle bundle, Progress progress) throws IOException { this.bundle = bundle; // add offset placeholders levelOffsetMap.clear(); bundle.entries().stream() .filter(entry -> { if (bundle.entries().size() == 1) { return true; } String name = entry.name(); return name.equals("mainData") || name.startsWith("level"); }) .forEach(entry -> levelOffsetMap.put(entry, new MutablePair<>(0L, 0L))); BundleHeader header = bundle.header(); header.levelByteEnd().clear(); header.levelByteEnd().addAll(levelOffsetMap.values()); header.numberOfLevelsToDownload(levelOffsetMap.size()); // write header out.writeStruct(header); header.headerSize((int) out.position()); // write bundle data if (header.compressed()) { // write data to temporary file try (DataWriter outData = DataWriters.forFile(dataFile, CREATE, WRITE, TRUNCATE_EXISTING)) { writeData(outData, progress); } // configure LZMA encoder LzmaEncoderProps props = new LzmaEncoderProps(); props.setDictionarySize(1 << 23); // 8 MiB props.setNumFastBytes(273); // maximum props.setUncompressedSize(Files.size(dataFile)); props.setEndMarkerMode(true); // stream the temporary bundle data compressed into the bundle file try (OutputStream os = new LzmaOutputStream(new BufferedOutputStream(out.stream()), props)) { Files.copy(dataFile, os); } for (MutablePair<Long, Long> levelOffset : levelOffsetMap.values()) { levelOffset.setLeft(out.size()); } } else { // write data directly to file writeData(out, progress); } // update header int fileSize = (int) out.size(); header.completeFileSize(fileSize); header.minimumStreamedBytes(fileSize); out.position(0); out.writeStruct(header); } private void writeData(DataWriter out, Progress progress) throws IOException { // write entry list List<BundleEntry> entries = bundle.entries(); long baseOffset = out.position(); out.writeInt(entries.size()); List<BundleEntryInfo> entryInfos = new ArrayList<>(entries.size()); for (BundleEntry entry : entries) { BundleEntryInfo entryInfo = new BundleEntryInfo(); entryInfo.name(entry.name()); entryInfo.size(entry.size()); out.writeStruct(entryInfo); entryInfos.add(entryInfo); } // write entry data for (int i = 0; i < entries.size(); i++) { out.align(4); BundleEntry entry = entries.get(i); BundleEntryInfo entryInfo = entryInfos.get(i); progress.update(Optional.of(entry.name()), i / (double) entries.size()); entryInfo.offset(out.position() - baseOffset); if (i == 0) { bundle.header().dataHeaderSize(entryInfo.offset()); } try ( InputStream is = entry.inputStream(); OutputStream os = out.stream(); ) { IOUtils.copy(is, os); } MutablePair<Long, Long> levelOffset = levelOffsetMap.get(entry); if (levelOffset != null) { long offset = out.position() - baseOffset; levelOffset.setLeft(offset); levelOffset.setRight(offset); } } // update offsets out.position(baseOffset + 4); for (BundleEntryInfo entryInfo : entryInfos) { out.writeStruct(entryInfo); } } @Override public void close() throws IOException { out.close(); Files.deleteIfExists(dataFile); } }