diff --git a/.gitignore b/.gitignore index f7fcb5a..af9628a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,7 @@ build/tmp hs_err_pid*.log #Run -/run +/run* /backups # output diff --git a/build/generated/kaitai/com/gamebuster19901/excite/modding/game/file/kaitai/ResMonster.java b/build/generated/kaitai/com/gamebuster19901/excite/modding/game/file/kaitai/ResMonster.java index 9aac540..32a7636 100644 --- a/build/generated/kaitai/com/gamebuster19901/excite/modding/game/file/kaitai/ResMonster.java +++ b/build/generated/kaitai/com/gamebuster19901/excite/modding/game/file/kaitai/ResMonster.java @@ -155,7 +155,7 @@ public Data(KaitaiStream _io, ResMonster _parent, ResMonster _root) { _read(); } private void _read() { - if (_root().header().compressed() == 128) { + if ( ((_root().header().compressed() == 128) || (_root().header().compressed() == 1152)) ) { this.compressedData = new QuicklzRcmp(this._io); } if (_root().header().compressed() == 0) { diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/Batch.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/Batch.java new file mode 100644 index 0000000..54a6f87 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/Batch.java @@ -0,0 +1,252 @@ +package com.gamebuster19901.excite.modding.concurrent; + +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.Callable; + +import static java.lang.Thread.State; + +public class Batch implements Batcher { + + private final String name; + private final Set> runnables = new HashSet<>(); + private final LinkedHashSet listeners = new LinkedHashSet<>(); + private volatile boolean accepting = true; + + public Batch(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public void addRunnable(BatchedCallable batchedCallable) { + if(accepting) { + runnables.add(batchedCallable); + updateListeners(); + } + else { + notAccepting(); + } + } + + @Override + public void addRunnable(Callable runnable) { + if(accepting) { + BatchedCallable b = new BatchedCallable<>(this, runnable); + runnables.add(new BatchedCallable<>(this, runnable)); + updateListeners(); + } + else { + notAccepting(); + } + } + + @Override + public void addRunnable(Runnable runnable) { + if(accepting) { + BatchedCallable b = new BatchedCallable<>(this, runnable); + runnables.add(new BatchedCallable<>(this, runnable)); + updateListeners(); + } + else { + notAccepting(); + } + } + + @Override + public void addBatchListener(BatchListener listener) { + if(accepting) { + if(!listeners.add(listener)) { + System.out.println("Warning: duplicate batch listener ignored."); + } + } + else { + notAccepting(); + } + } + + @Override + public Collection> getRunnables() { + return Set.copyOf(runnables); + } + + @Override + @SuppressWarnings("unchecked") + public Collection getListeners() { + return (Collection) listeners.clone(); + } + + public void shutdownNow() throws InterruptedException { + accepting = false; + Shutdown shutdown = new Shutdown(); + for(BatchedCallable r : runnables) { + if(r.getState() == State.NEW) { + r.shutdown(shutdown); + } + } + System.err.println(runnables.size()); + } + + public void updateListeners() { + for(BatchListener listener : listeners) { + listener.update(); + } + } + + private void notAccepting() { + throw new IllegalStateException("Batch is not accepting new tasks or listeners."); + } + + /** + * A wrapper class for a {@link Callable} object that participates in a batch execution managed by a {@link Batcher}. + * + * This class allows for the creation of callables that can be tracked and managed by a batching system. It provides methods + * to get the execution state, retrieve the result after completion, and handle exceptions. + * + * @param the type of the result produced by the wrapped {@link Callable} + */ + public static class BatchedCallable implements Callable { + + private final Batcher batch; + private final Callable child; + private volatile WeakReference threadRef; + protected volatile Throwable thrown; + private final Object startLock = new Object(); + private volatile Boolean started = false; + protected volatile boolean finished = false; + protected volatile T result; + + + /** + * Creates a new BatchedCallable instance for a provided {@link Runnable} object. + *

+ * This convenience constructor takes a Runnable and converts it to a Callable that simply calls the {@link Runnable#run} method + * and returns null. It then delegates to the main constructor with the converted callable. + * + * @param batch the {@link Batcher} instance managing this callable + * @param r the {@link Runnable} object to be wrapped + */ + public BatchedCallable(Batcher batch, Runnable r) { + this(batch, () -> { + r.run(); + return null; + }); + } + + /** + * Creates a new BatchedCallable instance for the provided {@link Callable} object. + *

+ * This constructor wraps a given Callable and associates it with the specified Batcher. The Batcher is + * notified of updates to the state of this callable. + * + * @param batch the {@link Batcher} instance managing this callable + * @param c the {@link Callable} object to be wrapped + */ + public BatchedCallable(Batcher batch, Callable c) { + this.batch = batch; + this.child = c; + batch.updateListeners(); + } + + /** + * Implements the `call` method of the {@link Callable} interface. + *

+ * This method executes the wrapped `Callable` object and stores the result. It also updates the state of this object + * and notifies the associated Batcher before and after execution. If any exceptions occur during execution, they are + * stored but not re-thrown by this method. The caller of this method is responsible for handling any exceptions. + * + * The behavior of this callable is undefined if call() is executed more than once. + * + * @return the result of the wrapped callable's execution (which may be null), or null if an exception occurred + */ + @Override + public T call() { + Thread thread; + try { + thread = Thread.currentThread(); + threadRef = new WeakReference<>(thread); + batch.updateListeners(); + result = child.call(); + return result; + } + catch(Throwable t) { + this.thrown = t; + } + finally { + finished = true; + batch.updateListeners(); + } + return null; + } + + /** + * Gets the current execution state of the wrapped callable. + * + * This method examines the internal state and thread reference to determine the current execution state. It can return one of the following states: + * + *

    + *
  • NEW: The callable has not yet been submitted for execution. + *
  • TERMINATED: The callable has finished execution, either successfully or with an exception. + *
  • The actual state of the thread running the callable (e.g., `RUNNING`, `WAITING`): + *

    If a thread is currently executing the callable, this state reflects the thread's lifecycle. + *

+ * + * @return the current state of the callable execution, as described above + */ + public State getState() { + if(finished) { //the thread is no longer working on this runnable + return State.TERMINATED; + } + if(threadRef == null) { + return State.NEW; + } + Thread thread = threadRef.get(); + if(thread == null) { + return State.TERMINATED; //thread has been garbage collected, so it has been terminated. + } + return thread.getState(); + } + + /** + * Retrieves the result of the computation after the {@link #call} method has finished executing. + * + * @throws IllegalStateException if the {@link #call} method has not yet finished executing. + * @return the result of the computation (which may be null), or {@code null} if the computation threw an exception. + */ + public T getResult() { + if(!finished) { + throw new IllegalStateException("Cannot obtain the result before it is calculated!"); + } + return result; + } + + /** + * Gets the exception thrown by the wrapped callable, if any. + *

+ * This method returns the exception that was thrown during the execution of the wrapped callable, or null if no exception + * occurred. + * + * @return the exception thrown by the wrapped callable, or null if no exception occurred + */ + public Throwable getThrown() { + return thrown; + } + + /** + * Sets the state of this callable to a proper shutdown state + * + * @param shutdown a dummy exception representing that this thread has failed to complete due to being shutdown. + */ + protected void shutdown(Shutdown shutdown) { + finished = true; + thrown = shutdown; + } + } + + } diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchContainer.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchContainer.java new file mode 100644 index 0000000..6938af8 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchContainer.java @@ -0,0 +1,37 @@ +package com.gamebuster19901.excite.modding.concurrent; + +import java.util.Collection; +import java.util.LinkedHashSet; + +import com.gamebuster19901.excite.modding.concurrent.Batch.BatchedCallable; + +public interface BatchContainer { + + public abstract String getName(); + + public Collection> getRunnables(); + + public default Collection getResults() throws IllegalStateException { + LinkedHashSet results = new LinkedHashSet<>(); + for(BatchedCallable callable : getRunnables()) { + T result = callable.getResult(); + if(result != null || (result == null && callable.getThrown() == null)) { + results.add(result); + } + } + return results; + } + + public Collection getListeners(); + + public void addBatchListener(BatchListener listener); + + public default void updateListeners() { + for(BatchListener l : getListeners()) { + l.update(); + } + } + + public void shutdownNow() throws InterruptedException; + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchListener.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchListener.java new file mode 100644 index 0000000..ba66094 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchListener.java @@ -0,0 +1,6 @@ +package com.gamebuster19901.excite.modding.concurrent; + +@FunctionalInterface +public interface BatchListener { + public void update(); +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchRunner.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchRunner.java new file mode 100644 index 0000000..974240a --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchRunner.java @@ -0,0 +1,119 @@ +package com.gamebuster19901.excite.modding.concurrent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import com.gamebuster19901.excite.modding.concurrent.Batch.BatchedCallable; + +public class BatchRunner implements BatchWorker, BatcherContainer { + + private final String name; + private final ExecutorService executor; + private final LinkedHashSet> batches = new LinkedHashSet>(); + private volatile boolean listenerAdded = false; + + public BatchRunner(String name) { + this(name, Runtime.getRuntime().availableProcessors()); + } + + public BatchRunner(String name, int threads) throws IllegalArgumentException { + this(name, Executors.newFixedThreadPool(threads)); + } + + public BatchRunner(String name, ExecutorService executor) { + this.name = name; + this.executor = executor; + } + + @Override + public String getName() { + return name; + } + + @Override + public void addBatch(Batcher batcher) { + synchronized(executor) { + if(listenerAdded) { + throw new IllegalStateException("Cannot add a batch after a BatchListener has been added! Add all batches before adding listeners!"); + } + if(executor.isShutdown()) { + throw new IllegalStateException("Cannot add more batches after the batch runner has shut down!"); + } + synchronized(batches) { + batches.add(batcher); + } + } + } + + @Override + public void startBatch() throws InterruptedException { + synchronized(executor) { + if(executor.isShutdown()) { + throw new IllegalStateException("BatchRunner has already been started!"); + } + synchronized(batches) { + ArrayList> batchers = new ArrayList<>(getRunnables()); + Collections.shuffle(batchers); //So multiple threads don't wait for a single archive to lazily load, allows multiple archives to lazily load at a time + executor.invokeAll(batchers); + executor.close(); + } + } + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return executor.awaitTermination(timeout, unit); + } + + @Override + public void shutdownNow() throws InterruptedException { + executor.shutdownNow(); // Force shutdown of any remaining tasks + try { + executor.awaitTermination(5, TimeUnit.SECONDS); + } + finally { + for(Batcher batch : batches) { + batch.shutdownNow(); + } + } + } + + @Override + public Collection> getRunnables() { + LinkedHashSet> ret = new LinkedHashSet<>(); + for(Batcher batch : batches) { + ret.addAll(batch.getRunnables()); + } + return ret; + } + + @Override + public Collection getListeners() { + LinkedHashSet ret = new LinkedHashSet<>(); + for(Batcher batch : batches) { + ret.addAll(batch.getListeners()); + } + return ret; + } + + @Override + public void addBatchListener(BatchListener listener) { + if(!listenerAdded) { + listenerAdded = true; + } + for(Batcher batch : batches) { + batch.addBatchListener(listener); + } + } + + @Override + public Collection> getBatches() { + return (Collection>) batches.clone(); + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchWorker.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchWorker.java new file mode 100644 index 0000000..86def76 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatchWorker.java @@ -0,0 +1,13 @@ +package com.gamebuster19901.excite.modding.concurrent; + +import java.util.concurrent.TimeUnit; + +public interface BatchWorker extends BatchContainer { + + public abstract void startBatch() throws InterruptedException; + + public abstract boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; + + public void shutdownNow() throws InterruptedException; + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/Batcher.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/Batcher.java new file mode 100644 index 0000000..60debc1 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/Batcher.java @@ -0,0 +1,35 @@ +package com.gamebuster19901.excite.modding.concurrent; + +import java.util.Collection; +import java.util.concurrent.Callable; + +public interface Batcher extends BatchContainer { + + public abstract void addRunnable(Callable runnable); + + public abstract void addRunnable(Runnable runnable); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public default void addRunnables(Collection runnables) { + if(runnables.size() > 0) { + Object o = runnables.iterator().next(); + if(o instanceof Callable) { + runnables.forEach((r) -> { + addRunnable((Callable)r); + }); + } + else if (o instanceof Runnable) { + runnables.forEach((r) -> { + addRunnable((Runnable)r); + }); + } + else { + throw new IllegalArgumentException(o.getClass().toString()); + } + } + else { + return; + } + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatcherContainer.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatcherContainer.java new file mode 100644 index 0000000..c744692 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/BatcherContainer.java @@ -0,0 +1,11 @@ +package com.gamebuster19901.excite.modding.concurrent; + +import java.util.Collection; + +public interface BatcherContainer extends BatchContainer{ + + public abstract Collection> getBatches(); + + public abstract void addBatch(Batcher batcher); + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/Shutdown.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/Shutdown.java new file mode 100644 index 0000000..def151d --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/Shutdown.java @@ -0,0 +1,3 @@ +package com.gamebuster19901.excite.modding.concurrent; + +class Shutdown extends Throwable {Shutdown(){}} \ No newline at end of file diff --git a/src/main/java/com/gamebuster19901/excite/modding/concurrent/package-info.java b/src/main/java/com/gamebuster19901/excite/modding/concurrent/package-info.java new file mode 100644 index 0000000..4d7194f --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/concurrent/package-info.java @@ -0,0 +1 @@ +package com.gamebuster19901.excite.modding.concurrent; \ No newline at end of file diff --git a/src/main/java/com/gamebuster19901/excite/modding/debugging/AssetAnalyzer.java b/src/main/java/com/gamebuster19901/excite/modding/debugging/AssetAnalyzer.java new file mode 100644 index 0000000..ba4a730 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/debugging/AssetAnalyzer.java @@ -0,0 +1,96 @@ +package com.gamebuster19901.excite.modding.debugging; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import org.apache.commons.lang3.tuple.Pair; + +import com.gamebuster19901.excite.modding.concurrent.Batch; +import com.gamebuster19901.excite.modding.concurrent.BatchRunner; +import com.gamebuster19901.excite.modding.game.file.kaitai.TocMonster.Details; +import com.gamebuster19901.excite.modding.unarchiver.QuickAccessArchive; +import com.gamebuster19901.excite.modding.unarchiver.Toc; +import com.gamebuster19901.excite.modding.unarchiver.Unarchiver; +import com.thegamecommunity.excite.modding.game.file.AssetType; + +public class AssetAnalyzer { + + private static final Path current = Path.of("").toAbsolutePath(); + private static final Path run = current.resolve("run"); + private static final Path assets = current.resolve("gameData"); + + public static void main(String[] args) throws IOException, InterruptedException { + System.out.println(current); + System.out.println(run); + System.out.println(assets); + + Unarchiver unarchiver = new Unarchiver(assets, run); + Collection tocs = unarchiver.getTocs(); + BatchRunner>> runner = new BatchRunner<>("Analyzer"); + + for(Path tocFile : tocs) { + String tocName = tocFile.getFileName().toString(); + Batch>> batch = new Batch>>(tocName); + Toc toc = new Toc(tocFile); + List

details = toc.getFiles(); + + final QuickAccessArchive QArchive = unarchiver.getArchive(toc); + for(Details resource : details) { + String resourceName = resource.name(); + AssetType type = AssetType.getAssetType(resourceName); + if(type == AssetType.UNRECOGNIZED) { + throw new AssertionError("Unrecognized asset type: " + resourceName + " in " + tocName); + } + batch.addRunnable(() -> { + return Pair.of(type, Pair.of(resource.typeCode(), resource.typeCodeInt())); + }); + } + runner.addBatch(batch); + } + + runner.startBatch(); + Collection>> results = runner.getResults(); + HashMap> stringCodes = new HashMap>(); + HashMap> intCodes = new HashMap>(); + for(AssetType type : AssetType.values()) { + stringCodes.put(type, new HashSet<>()); + intCodes.put(type, new HashSet<>()); + } + for(Pair> pair : results) { + Pair pair2 = pair.getValue(); + stringCodes.get(pair.getKey()).add(pair2.getKey()); + intCodes.get(pair.getKey()).add(pair2.getValue()); + } + + for(AssetType assetType : AssetType.values()) { + HashSet strings = stringCodes.get(assetType); + HashSet ints = intCodes.get(assetType); + System.out.println("============" + assetType.toString().toUpperCase() + "============"); + System.out.println("Analysis of '" + assetType + "' type:"); + System.out.println("Total string typecodes: " + strings.size()); + System.out.println("Total int typecodes: " + ints.size()); + System.out.println("List of string typecodes below: \n"); + stringCodes.get(assetType).forEach((forCode) -> { + if(forCode.indexOf('\0') != -1) { + System.out.print('['); + for(char c : forCode.toCharArray()) { + System.out.print(Integer.toHexString((int)c) + " "); + } + System.out.println(']'); + } + else { + System.out.println(forCode); + } + }); + System.out.println("List of int typecodes below: \n"); + intCodes.get(assetType).forEach((forCode) -> { + System.out.println(forCode); + }); + } + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/debugging/package-info.java b/src/main/java/com/gamebuster19901/excite/modding/debugging/package-info.java new file mode 100644 index 0000000..0a8f913 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/debugging/package-info.java @@ -0,0 +1 @@ +package com.gamebuster19901.excite.modding.debugging; \ No newline at end of file diff --git a/src/main/java/com/gamebuster19901/excite/modding/quicklz/DecodingException.java b/src/main/java/com/gamebuster19901/excite/modding/quicklz/DecodingException.java new file mode 100644 index 0000000..fdcbee5 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/quicklz/DecodingException.java @@ -0,0 +1,15 @@ +package com.gamebuster19901.excite.modding.quicklz; + +import java.io.IOException; + +public class DecodingException extends IOException { + + public DecodingException(int exitCode) { + super("QuickLZ process failed with exit code (" + exitCode + "): " + QuickLZ.ExitCode.get(exitCode)); + } + + public DecodingException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/quicklz/QuickLZ.java b/src/main/java/com/gamebuster19901/excite/modding/quicklz/QuickLZ.java index 0a1ea82..eb79f0f 100644 --- a/src/main/java/com/gamebuster19901/excite/modding/quicklz/QuickLZ.java +++ b/src/main/java/com/gamebuster19901/excite/modding/quicklz/QuickLZ.java @@ -19,6 +19,26 @@ public static enum Mode { decompress } + public static enum ExitCode { + SUCCESS, + HELP_SUCCESS, + UNKNOWN_MODE, + MISSING_MODE_ARG, + MISSING_SOURCE_ARG, + MISSING_DEST_ARG, + TOO_MANY_ARGS, + SOURCE_ERR, + DEST_ERR, + UNKNOWN; + + public static ExitCode get(int i) { + if(i < values().length && i >= 0) { + return values()[i]; + } + return UNKNOWN; + } + } + private static final QuickLZImpl impl = QuickLZImpl.get(); public static void compress(Path file, Path dest) { diff --git a/src/main/java/com/gamebuster19901/excite/modding/quicklz/QuicklzDumper.java b/src/main/java/com/gamebuster19901/excite/modding/quicklz/QuicklzDumper.java index eef767c..28adfab 100644 --- a/src/main/java/com/gamebuster19901/excite/modding/quicklz/QuicklzDumper.java +++ b/src/main/java/com/gamebuster19901/excite/modding/quicklz/QuicklzDumper.java @@ -5,9 +5,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.concurrent.TimeUnit; -import com.gamebuster19901.excite.modding.FileUtils; +import com.gamebuster19901.excite.modding.quicklz.QuickLZ.ExitCode; +import com.gamebuster19901.excite.modding.util.FileUtils; public class QuicklzDumper { @@ -28,12 +28,17 @@ public byte[] decode(byte[] _raw_data) { try { Files.write(input, _raw_data, StandardOpenOption.CREATE); Process process = QuickLZ.decompress(input, output); - process.waitFor(5000, TimeUnit.SECONDS); + int exitCode = process.waitFor(); + if(exitCode != ExitCode.SUCCESS.ordinal()) { + throw new DecodingException(exitCode); + } return Files.readAllBytes(output); } catch(IOException | InterruptedException e) { - throw new IOError(e); + IOError err = new IOError(e); + err.printStackTrace(); + throw err; } } - + } diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/BatchOperationComponent.java b/src/main/java/com/gamebuster19901/excite/modding/ui/BatchOperationComponent.java new file mode 100644 index 0000000..bf20cca --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/BatchOperationComponent.java @@ -0,0 +1,79 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.BorderLayout; +import java.util.Collection; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +import com.gamebuster19901.excite.modding.concurrent.Batch.BatchedCallable; +import com.gamebuster19901.excite.modding.concurrent.BatchContainer; +import com.gamebuster19901.excite.modding.concurrent.BatchListener; + +public class BatchOperationComponent extends JPanel implements BatchContainer, BatchListener { + + private BatchedImageComponent image; + private JLabel nameLabel; + + public BatchOperationComponent(BatchContainer batch) { + this(new BatchedImageComponent(batch)); + setName(batch.getName()); + } + + /** + @wbp.parser.constructor + **/ + public BatchOperationComponent(BatchedImageComponent batch) { + this.image = batch; + setLayout(new BorderLayout(0, 0)); + + add(batch, BorderLayout.CENTER); + + String name = batch.getName(); + + nameLabel = new JLabel(name); + nameLabel.setHorizontalAlignment(SwingConstants.CENTER); + add(nameLabel, BorderLayout.SOUTH); + this.setName(name); + } + + @Override + public void setName(String name) { + super.setName(name); + this.setToolTipText(name); + this.nameLabel.setText(name); + } + + @Override + public void addBatchListener(BatchListener listener) { + image.addBatchListener(listener); + } + + @Override + public void shutdownNow() throws InterruptedException { + image.shutdownNow(); + } + + @Override + public Collection> getRunnables() { + return image.getRunnables(); + } + + @Override + public Collection getListeners() { + return image.getListeners(); + } + + @Override + public void updateListeners() { + image.updateListeners(); + } + + @Override + public void update() { + image.update(); + repaint(); + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/BatchedImageComponent.java b/src/main/java/com/gamebuster19901/excite/modding/ui/BatchedImageComponent.java new file mode 100644 index 0000000..78e9ad3 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/BatchedImageComponent.java @@ -0,0 +1,113 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.Color; +import java.awt.Image; +import java.util.Collection; +import java.util.LinkedHashMap; + +import org.apache.commons.lang3.tuple.Pair; + +import com.gamebuster19901.excite.modding.concurrent.BatchListener; +import com.gamebuster19901.excite.modding.unarchiver.concurrent.DecisionType; +import com.gamebuster19901.excite.modding.concurrent.Batch.BatchedCallable; +import com.gamebuster19901.excite.modding.concurrent.BatchContainer; + +public class BatchedImageComponent extends ImageComponent implements BatchContainer, BatchListener { + + private final BatchContainer batch; + + public BatchedImageComponent(BatchContainer batch) { + super(batch.getName()); + this.batch = batch; + addBatchListener(this); + } + + @Override + public Image getImage() { + System.out.println("Image"); + int _new = 0; + int skipped = 0; + int working = 0; + int success = 0; + int failure = 0; + int other = 0; + + for (BatchedCallable runnable : getRunnables()) { + switch (runnable.getState()) { + case NEW: + _new++; + continue; + case RUNNABLE: + working++; + continue; + case TERMINATED: + if(runnable.getThrown() != null) { + failure++; + continue; + } + Object r = runnable.getResult(); + if (r instanceof Pair) { + Object key = ((Pair) r).getKey(); + if(key instanceof DecisionType) { + DecisionType decision = (DecisionType) key; + switch(decision) { + case IGNORE: + failure++; + continue; + case PROCEED: + success++; + continue; + case SKIP: + skipped++; + continue; + default: + other++; + continue; + } + } + } + failure++; + continue; + default: + other++; + } + } + + LinkedHashMap colors = new LinkedHashMap<>(); + colors.put(Color.GREEN, success); + colors.put(Color.CYAN.darker(), skipped); + colors.put(Color.RED, failure); + colors.put(Color.WHITE, working); + colors.put(Color.ORANGE, other); + colors.put(Color.GRAY, _new); + + return StripedImageGenerator.generateImage(getWidth(), getHeight(), (LinkedHashMap) colors); + } + + @Override + public Collection> getRunnables() { + return batch.getRunnables(); + } + + @Override + public Collection getListeners() { + return batch.getListeners(); + } + + @Override + public void addBatchListener(BatchListener listener) { + batch.addBatchListener(listener); + } + + @Override + public void update() { + repaint(); + System.out.println("Updated"); + } + + @Override + public void shutdownNow() throws InterruptedException { + batch.shutdownNow(); + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/ColorKeyComponent.java b/src/main/java/com/gamebuster19901/excite/modding/ui/ColorKeyComponent.java new file mode 100644 index 0000000..6b8c326 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/ColorKeyComponent.java @@ -0,0 +1,47 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.Color; + +import javax.swing.JPanel; +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import java.awt.FlowLayout; +import java.awt.GridBagLayout; +import java.awt.GridBagConstraints; +import java.awt.Insets; +import javax.swing.SwingConstants; + +public class ColorKeyComponent extends JPanel { + + private static final long serialVersionUID = 1L; + + public ColorKeyComponent(Color color, String name) { + this.setBorder(BorderFactory.createEmptyBorder()); + GridBagLayout gridBagLayout = new GridBagLayout(); + gridBagLayout.columnWidths = new int[]{0, 0}; + gridBagLayout.rowHeights = new int[]{25}; + gridBagLayout.columnWeights = new double[]{0.0, 0.0}; + gridBagLayout.rowWeights = new double[]{0.0}; + setLayout(gridBagLayout); + JLabel coloring = new JLabel("■"); + coloring.setBorder(BorderFactory.createEmptyBorder()); + coloring.setHorizontalAlignment(SwingConstants.TRAILING); + FlowLayout flowLayout = new FlowLayout(); + flowLayout.setAlignment(FlowLayout.LEFT); + coloring.setForeground(color); + GridBagConstraints gbc_coloring = new GridBagConstraints(); + gbc_coloring.anchor = GridBagConstraints.EAST; + gbc_coloring.insets = new Insets(0, 0, 2, 0); + gbc_coloring.gridx = 0; + gbc_coloring.gridy = 0; + add(coloring, gbc_coloring); + JLabel lblName = new JLabel(":" + name); + GridBagConstraints gbc_lblName = new GridBagConstraints(); + gbc_lblName.anchor = GridBagConstraints.WEST; + gbc_lblName.gridx = 1; + gbc_lblName.gridy = 0; + add(lblName, gbc_lblName); + + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/CustomProgressBar.java b/src/main/java/com/gamebuster19901/excite/modding/ui/CustomProgressBar.java new file mode 100644 index 0000000..8f77905 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/CustomProgressBar.java @@ -0,0 +1,18 @@ +package com.gamebuster19901.excite.modding.ui; + +import javax.swing.JProgressBar; + +public class CustomProgressBar extends JProgressBar { + + @Override + public String getString() { + if(this.getPercentComplete() <= 0d) { + return "Not Started"; + } + if(this.getPercentComplete() < 1d) { + return "Progress: " + ((int)(this.getPercentComplete() * 100)) + "%"; + } + return "Complete"; + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/EJTabbedPane.java b/src/main/java/com/gamebuster19901/excite/modding/ui/EJTabbedPane.java new file mode 100644 index 0000000..b8317ab --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/EJTabbedPane.java @@ -0,0 +1,166 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.Component; + +import javax.swing.Icon; +import javax.swing.JTabbedPane; + +/** + * A custom tabbed pane component that extends {@link javax.swing.JTabbedPane} and provides additional functionalities for managing tabs. + * + * This class offers a convenience layer on top of the standard `JTabbedPane` by: + * * Encapsulating tab information within a {@link Tab} record. + * * Providing methods for adding and retrieving tabs using `Tab` objects. + * * Propagating `IndexOutOfBoundsException` for methods that interact with tabs to ensure consistent error handling. + * + * @see javax.swing.JTabbedPane + * @see Tab + */ +public class EJTabbedPane extends JTabbedPane { + + /** + * Constructs a new EJTabbedPane with the default tab placement (TOP). + */ + public EJTabbedPane() { + super(); + } + + /** + * Constructs a new EJTabbedPane with the specified tab placement. + * + * @param tabPlacement The placement of the tabs. This can be one of the following values from {@link javax.swing.JTabbedPane}: + * * {@link JTabbedPane#TOP} + * * {@link JTabbedPane#BOTTOM} + * * {@link JTabbedPane#LEFT} + * * {@link JTabbedPane#RIGHT} + * @throws IllegalArgumentException if the tabPlacement is not a valid value. + */ + public EJTabbedPane(int tabPlacement) throws IllegalArgumentException { + super(tabPlacement); + } + + /** + * Constructs a new EJTabbedPane with the specified tab placement and layout policy. + * + * @param tabPlacement The placement of the tabs. Refer to {@link #EJTabbedPane(int)} for valid values. + * @param tabLayoutPolicy The layout policy for the tabs. This can be one of the following values from {@link javax.swing.JTabbedPane}: + * * {@link JTabbedPane#WRAP_TAB_LAYOUT} + * * {@link JTabbedPane#SCROLL_TAB_LAYOUT} + * @throws IllegalArgumentException if the tabPlacement or tabLayoutPolicy is not a valid value. + */ + public EJTabbedPane(int tabPlacement, int tabLayoutPolicy) throws IllegalArgumentException { + super(tabPlacement, tabLayoutPolicy); + } + + /** + * Adds a new tab to the EJTabbedPane. + * + * This method extracts the title and component from the provided {@link Tab} object and calls the underlying `JTabbedPane` method to add the tab. + * + * @param tab The tab object containing the information for the new tab. + * @return The same `tab` object that was provided as input. + */ + public Tab addTab(Tab tab) { + this.addTab(tab.title, tab.icon, tab.component, tab.tip); + return tab; + } + + /** + * Inserts a new tab at the specified index within the EJTabbedPane. + * + * This method uses the information from the provided {@link Tab} object (title, icon, component, tooltip) to call the underlying `JTabbedPane` method for insertion. + * + * @param tab The tab object containing the information for the new tab. + * @param index The index at which to insert the tab. + * @return The same `tab` object that was provided as input. + * @throws IndexOutOfBoundsException if the index is invalid. + */ + public Tab putTab(Tab tab, int index) throws IndexOutOfBoundsException { + this.insertTab(tab.title, tab.icon, tab.component, tab.tip, index); + return tab; + } + + /** + * Retrieves a {@link Tab} object from the EJTabbedPane by its title. + * + * This method first finds the index of the tab with the matching title and then calls the `getTab(int index)` method. + * + * @param title The title of the tab to retrieve. + * @return A `Tab` object representing the tab with the specified title, or null if the tab is not found. + */ + public Tab getTab(String title) throws IndexOutOfBoundsException { + int index = this.indexOfTab(title); + return getTab(index); + } + + /** + * Retrieves a {@link Tab} object from the EJTabbedPane by its index. + * + * This method extracts the title, icon, component, and tooltip from the underlying `JTabbedPane` at the specified index and creates a new `Tab` object with this information. + * + * @param index The index of the tab to retrieve. + * @return A `Tab` object representing the tab at the specified index, or null if the index is invalid. + */ + public Tab getTab(int index) { + if(index < 0 || index > this.getComponentCount()) { + return null; + } + String title = this.getTitleAt(index); + Icon icon = this.getIconAt(index); + Component component = this.getComponentAt(index); + String tip = this.getToolTipTextAt(index); + return new Tab(title, icon, component, tip); + } + + public Tab getSelectedTab() { + return getTab(getSelectedIndex()); + } + + public int getIndex(Tab tab) { + if(tab.title != null) { + return this.indexOfTab(tab.title); + } + if(tab.icon != null) { + return this.indexOfTab(tab.icon); + } + throw new IllegalArgumentException("Tab has null title and null icon, cannot obtain index"); + } + + public void setSelectedTab(Tab tab) { + this.setSelectedIndex(getIndex(tab)); + this.invalidate(); + } + + @Override + protected void fireStateChanged() { + super.fireStateChanged(); + } + + /** + * A record that encapsulates information about a single tab. + */ + public static final record Tab(String title, Icon icon, Component component, String tip) { + + /** + * Constructs a new Tab object with the specified title and component. + * + * @param title The title of the tab. + * @param component The component to be displayed in the tab. + */ + public Tab(String title, Component component) { + this(title, null, component, null); + } + + /** + * Constructs a new Tab object with the specified title, icon, and component. + * + * @param title The title of the tab. + * @param icon The icon to be displayed on the tab. + * @param component The component to be displayed in the tab. + */ + public Tab(String title, Icon icon, Component component) { + this(title, icon, component, null); + } + } +} + diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/ImageComponent.java b/src/main/java/com/gamebuster19901/excite/modding/ui/ImageComponent.java new file mode 100644 index 0000000..fe910d0 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/ImageComponent.java @@ -0,0 +1,34 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.Graphics; +import java.awt.Image; + +import javax.swing.JComponent; + +public class ImageComponent extends JComponent { + + Image image = null; + + public ImageComponent(String name) { + this(name, null); + } + + public ImageComponent(String name, Image image) { + this.setName(name); + this.image = image; + } + + public Image getImage() { + return image; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Image image = getImage(); + if(image != null) { + g.drawImage(image, 0, 0, this); + } + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/JTextAreaOutputStream.java b/src/main/java/com/gamebuster19901/excite/modding/ui/JTextAreaOutputStream.java new file mode 100644 index 0000000..838e05b --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/JTextAreaOutputStream.java @@ -0,0 +1,30 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +public class JTextAreaOutputStream extends OutputStream { + + private final JTextArea dest; + + public JTextAreaOutputStream(JTextArea dest) { + this.dest = dest; + } + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte)b}, 0, 1); + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + String text = new String(buf, off, len); + SwingUtilities.invokeLater(() -> { + dest.append(text); + }); + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/StripedImageGenerator.java b/src/main/java/com/gamebuster19901/excite/modding/ui/StripedImageGenerator.java new file mode 100644 index 0000000..95f9465 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/StripedImageGenerator.java @@ -0,0 +1,57 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.LinkedHashMap; +import java.util.Map; + +public class StripedImageGenerator { + + public static BufferedImage generateImage(int width, int height, LinkedHashMap colorWeights) { + // Check for valid input + if (width <= 0 || height <= 0 || colorWeights == null || colorWeights.isEmpty()) { + throw new IllegalArgumentException("Invalid input parameters"); + } + + int totalWeight = 0; + for (int weight : colorWeights.values()) { + if (weight < 0) { + throw new IllegalArgumentException("Color weights must be 0 or a positive integer"); + } + totalWeight += weight; + } + + if (totalWeight == 0) { + totalWeight = height; + colorWeights = new LinkedHashMap(); + colorWeights.put(Color.GRAY, totalWeight); + } + double scaleFactor = (double) height / totalWeight; + + LinkedHashMap colorHeights = new LinkedHashMap<>(); + for (Map.Entry entry : colorWeights.reversed().entrySet()) { + Color color = entry.getKey(); + int weight = entry.getValue(); + int rowHeight = (int) Math.ceil(weight * scaleFactor); + colorHeights.put(color, rowHeight); + } + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + Graphics2D graphics = image.createGraphics(); + + int currentY = 0; + for (Map.Entry entry : colorHeights.entrySet()) { + Color color = entry.getKey(); + int rowHeight = entry.getValue(); + graphics.setColor(color); + graphics.fillRect(0, currentY, width, rowHeight); + currentY += rowHeight; + } + + graphics.dispose(); + + return image; + } +} \ No newline at end of file diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/TestWindow.java b/src/main/java/com/gamebuster19901/excite/modding/ui/TestWindow.java new file mode 100644 index 0000000..fc49b8a --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/TestWindow.java @@ -0,0 +1,156 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.gamebuster19901.excite.modding.concurrent.Batch; +import com.gamebuster19901.excite.modding.concurrent.BatchRunner; +import java.awt.GridBagLayout; +import java.awt.GridBagConstraints; +import javax.swing.JTabbedPane; +import java.awt.Insets; +import java.util.Random; +import javax.swing.JScrollPane; + +public class TestWindow { + + private JFrame frame; + + /** + * Launch the application. + */ + public static void main(String[] args) { + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + TestWindow window = new TestWindow(); + window.frame.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /** + * Create the application. + */ + public TestWindow() { + initialize(); + } + + /** + * Initialize the contents of the frame. + */ + private void initialize() { + System.out.println(Thread.currentThread()); + frame = new JFrame(); + frame.setBounds(100, 100, 450, 300); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + Batch batch = new Batch("Test"); + BatchRunner r = new BatchRunner("TestRunner"); + r.addBatch(batch); + + GridBagLayout gridBagLayout = new GridBagLayout(); + gridBagLayout.columnWidths = new int[]{450, 0}; + gridBagLayout.rowHeights = new int[]{263, 0}; + gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE}; + gridBagLayout.rowWeights = new double[]{1.0, Double.MIN_VALUE}; + frame.getContentPane().setLayout(gridBagLayout); + JPanel panel = new JPanel(); + GridBagLayout gbl_panel = new GridBagLayout(); + gbl_panel.columnWidths = new int[]{150, 39, 0}; + gbl_panel.rowHeights = new int[]{175, 0}; + gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE}; + gbl_panel.rowWeights = new double[]{1.0, Double.MIN_VALUE}; + panel.setLayout(gbl_panel); + for(int i = 0; i < 20; i++) { + final int j = i; + batch.addRunnable(() -> { + try { + Thread.sleep(new Random().nextInt(0, 1000)); + System.out.println(Thread.currentThread() + ": Ran " + j); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } + + + + + JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); + GridBagConstraints gbc_tabbedPane = new GridBagConstraints(); + gbc_tabbedPane.anchor = GridBagConstraints.WEST; + gbc_tabbedPane.gridx = 1; + gbc_tabbedPane.gridy = 0; + panel.add(tabbedPane, gbc_tabbedPane); + + JPanel panel_1 = new JPanel(); + tabbedPane.addTab("New tab", null, panel_1, null); + GridBagLayout gbl_panel_1 = new GridBagLayout(); + gbl_panel_1.columnWidths = new int[]{0, 0, 0}; + gbl_panel_1.rowHeights = new int[]{0, 178, 0}; + gbl_panel_1.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE}; + gbl_panel_1.rowWeights = new double[]{1.0, 0.0, Double.MIN_VALUE}; + panel_1.setLayout(gbl_panel_1); + + ColorKeyComponent lblNewLabel = new ColorKeyComponent(Color.RED, "Errorifying, really weird"); + GridBagConstraints gbc_lblNewLabel = new GridBagConstraints(); + gbc_lblNewLabel.insets = new Insets(0, 0, 5, 5); + gbc_lblNewLabel.gridx = 0; + gbc_lblNewLabel.gridy = 0; + panel_1.add(lblNewLabel, gbc_lblNewLabel); + + ColorKeyComponent lblNewLabel_1 = new ColorKeyComponent(Color.RED, "Error"); + GridBagLayout gridBagLayout_1 = (GridBagLayout) lblNewLabel_1.getLayout(); + gridBagLayout_1.rowWeights = new double[]{0.0}; + gridBagLayout_1.rowHeights = new int[]{25}; + gridBagLayout_1.columnWeights = new double[]{0.0, 1.0}; + gridBagLayout_1.columnWidths = new int[]{25, 78}; + GridBagConstraints gbc_lblNewLabel_1 = new GridBagConstraints(); + gbc_lblNewLabel_1.insets = new Insets(0, 0, 5, 0); + gbc_lblNewLabel_1.fill = GridBagConstraints.BOTH; + gbc_lblNewLabel_1.gridx = 1; + gbc_lblNewLabel_1.gridy = 0; + panel_1.add(lblNewLabel_1, gbc_lblNewLabel_1); + + + + BatchOperationComponent c = new BatchOperationComponent(r); + + JScrollPane scrollPane = new JScrollPane(c); + GridBagConstraints gbc_scrollPane = new GridBagConstraints(); + gbc_scrollPane.gridwidth = 2; + gbc_scrollPane.anchor = GridBagConstraints.NORTHWEST; + gbc_scrollPane.gridx = 0; + gbc_scrollPane.gridy = 1; + panel_1.add(scrollPane, gbc_scrollPane); + c.setPreferredSize(new Dimension(150, 175)); + GridBagConstraints gbc_c = new GridBagConstraints(); + gbc_c.anchor = GridBagConstraints.NORTHWEST; + gbc_c.insets = new Insets(0, 0, 0, 5); + gbc_c.gridx = 0; + gbc_c.gridy = 0; + GridBagConstraints gbc_panel = new GridBagConstraints(); + gbc_panel.fill = GridBagConstraints.BOTH; + gbc_panel.gridx = 0; + gbc_panel.gridy = 0; + frame.getContentPane().add(panel, gbc_panel); + frame.setVisible(true); + new Thread(() -> { + try { + Thread.sleep(500); + r.startBatch(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }).start(); + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/Window.java b/src/main/java/com/gamebuster19901/excite/modding/ui/Window.java new file mode 100644 index 0000000..2fca743 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/Window.java @@ -0,0 +1,865 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.Dimension; +import java.awt.EventQueue; + +import javax.swing.JFrame; + +import java.awt.GridBagLayout; +import java.awt.GridLayout; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; + +import java.awt.GridBagConstraints; +import java.awt.Insets; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.concurrent.Callable; + +import javax.swing.JTextField; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; + +import org.apache.commons.lang3.tuple.Pair; + +import com.gamebuster19901.excite.modding.concurrent.Batch; +import com.gamebuster19901.excite.modding.concurrent.BatchListener; +import com.gamebuster19901.excite.modding.concurrent.BatchRunner; +import com.gamebuster19901.excite.modding.concurrent.Batcher; +import com.gamebuster19901.excite.modding.ui.EJTabbedPane.Tab; +import com.gamebuster19901.excite.modding.unarchiver.Unarchiver; +import com.gamebuster19901.excite.modding.unarchiver.concurrent.DecisionType; +import com.gamebuster19901.excite.modding.util.FileUtils; +import com.gamebuster19901.excite.modding.util.SplitOutputStream; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JSeparator; +import javax.swing.JSlider; +import javax.swing.JProgressBar; +import javax.swing.JScrollPane; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; + +public class Window implements BatchListener { + + private static final String CONSOLE = "Console Output"; + private static final String STATUS = "Status"; + private static final String PROGRESS = "Progress"; + + private JFrame frame; + private JTextField textFieldSource; + private JTextField textFieldDest; + private JSlider threadSlider; + + private volatile Unarchiver unarchiver; + private volatile BatchRunner>> copyOperations; + private volatile BatchRunner processOperations; + + /** + * Launch the application. + */ + public static void main(String[] args) { + Window window; + EventQueue.invokeLater(new Runnable() { + public void run() { + try { + Window window = new Window(); + window.frame.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /** + * Create the application. + */ + public Window() { + initialize(); + } + + /** + * Initialize the contents of the frame. + * @throws IOException + */ + private void initialize() { + unarchiver = genUnarchiver(null, null); + + threadSlider = new JSlider(); + threadSlider.setSnapToTicks(true); + threadSlider.setMinimum(1); + threadSlider.setMaximum(Runtime.getRuntime().availableProcessors()); + threadSlider.setPreferredSize(new Dimension(77, 16)); + threadSlider.setBorder(null); + + copyOperations = genCopyBatches(); + processOperations = genProcessBatches(null); + + frame = new JFrame(); + frame.setBounds(100, 100, 1000, 680); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + GridBagLayout gridBagLayout = new GridBagLayout(); + gridBagLayout.columnWidths = new int[]{5, 0, 0, 0, 0, 0, 5, 0}; + gridBagLayout.rowHeights = new int[]{5, 0, 0, 5, 0, 0, 16, 0, 0}; + gridBagLayout.columnWeights = new double[]{0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, Double.MIN_VALUE}; + gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, Double.MIN_VALUE}; + frame.getContentPane().setLayout(gridBagLayout); + + JLabel lblSourceDirectory = new JLabel("Source Directory"); + GridBagConstraints gbc_lblSourceDirectory = new GridBagConstraints(); + gbc_lblSourceDirectory.gridwidth = 2; + gbc_lblSourceDirectory.fill = GridBagConstraints.VERTICAL; + gbc_lblSourceDirectory.anchor = GridBagConstraints.EAST; + gbc_lblSourceDirectory.insets = new Insets(0, 0, 5, 5); + gbc_lblSourceDirectory.gridx = 1; + gbc_lblSourceDirectory.gridy = 1; + frame.getContentPane().add(lblSourceDirectory, gbc_lblSourceDirectory); + + textFieldSource = new JTextField(); + GridBagConstraints gbc_textFieldSource = new GridBagConstraints(); + gbc_textFieldSource.insets = new Insets(0, 0, 5, 5); + gbc_textFieldSource.fill = GridBagConstraints.BOTH; + gbc_textFieldSource.gridx = 3; + gbc_textFieldSource.gridy = 1; + frame.getContentPane().add(textFieldSource, gbc_textFieldSource); + textFieldSource.setColumns(10); + + JButton btnChangeSource = new JButton("Change"); + GridBagConstraints gbc_btnChangeSource = new GridBagConstraints(); + gbc_btnChangeSource.fill = GridBagConstraints.VERTICAL; + gbc_btnChangeSource.insets = new Insets(0, 0, 5, 5); + gbc_btnChangeSource.gridx = 4; + gbc_btnChangeSource.gridy = 1; + frame.getContentPane().add(btnChangeSource, gbc_btnChangeSource); + + JButton btnExtract = new JButton("Extract!"); + GridBagConstraints gbc_btnExtract = new GridBagConstraints(); + gbc_btnExtract.insets = new Insets(0, 0, 5, 5); + gbc_btnExtract.fill = GridBagConstraints.VERTICAL; + gbc_btnExtract.gridheight = 2; + gbc_btnExtract.gridx = 5; + gbc_btnExtract.gridy = 1; + frame.getContentPane().add(btnExtract, gbc_btnExtract); + + JLabel lblDestinationDirectory = new JLabel("Destination Directory"); + GridBagConstraints gbc_lblDestinationDirectory = new GridBagConstraints(); + gbc_lblDestinationDirectory.gridwidth = 2; + gbc_lblDestinationDirectory.insets = new Insets(0, 0, 5, 5); + gbc_lblDestinationDirectory.anchor = GridBagConstraints.EAST; + gbc_lblDestinationDirectory.gridx = 1; + gbc_lblDestinationDirectory.gridy = 2; + frame.getContentPane().add(lblDestinationDirectory, gbc_lblDestinationDirectory); + + textFieldDest = new JTextField(); + GridBagConstraints gbc_textFieldDest = new GridBagConstraints(); + gbc_textFieldDest.insets = new Insets(0, 0, 5, 5); + gbc_textFieldDest.fill = GridBagConstraints.BOTH; + gbc_textFieldDest.gridx = 3; + gbc_textFieldDest.gridy = 2; + frame.getContentPane().add(textFieldDest, gbc_textFieldDest); + textFieldDest.setColumns(10); + + JButton btnChangeDest = new JButton("Change"); + GridBagConstraints gbc_btnChangeDest = new GridBagConstraints(); + gbc_btnChangeDest.insets = new Insets(0, 0, 5, 5); + gbc_btnChangeDest.gridx = 4; + gbc_btnChangeDest.gridy = 2; + frame.getContentPane().add(btnChangeDest, gbc_btnChangeDest); + + JSeparator separator = new JSeparator(); + separator.setPreferredSize(new Dimension(1,1)); + GridBagConstraints gbc_separator = new GridBagConstraints(); + gbc_separator.anchor = GridBagConstraints.SOUTH; + gbc_separator.fill = GridBagConstraints.HORIZONTAL; + gbc_separator.gridwidth = 5; + gbc_separator.insets = new Insets(0, 0, 5, 5); + gbc_separator.gridx = 1; + gbc_separator.gridy = 3; + frame.getContentPane().add(separator, gbc_separator); + + JLabel lblThreads = new JLabel("Threads: "); + GridBagConstraints gbc_lblThreads = new GridBagConstraints(); + gbc_lblThreads.anchor = GridBagConstraints.WEST; + gbc_lblThreads.insets = new Insets(0, 0, 5, 5); + gbc_lblThreads.gridx = 1; + gbc_lblThreads.gridy = 4; + frame.getContentPane().add(lblThreads, gbc_lblThreads); + + + GridBagConstraints gbc_slider = new GridBagConstraints(); + gbc_slider.fill = GridBagConstraints.HORIZONTAL; + gbc_slider.insets = new Insets(0, 0, 5, 5); + gbc_slider.gridx = 1; + gbc_slider.gridy = 5; + frame.getContentPane().add(threadSlider, gbc_slider); + + JProgressBar progressBar = new CustomProgressBar(); + progressBar.setStringPainted(true); + GridBagConstraints gbc_progressBar = new GridBagConstraints(); + gbc_progressBar.fill = GridBagConstraints.HORIZONTAL; + gbc_progressBar.gridwidth = 5; + gbc_progressBar.insets = new Insets(0, 0, 5, 5); + gbc_progressBar.gridx = 1; + gbc_progressBar.gridy = 6; + frame.getContentPane().add(progressBar, gbc_progressBar); + + EJTabbedPane tabbedPane = new EJTabbedPane(JTabbedPane.TOP); + setupTabbedPane(tabbedPane); + GridBagConstraints gbc_tabbedPane = new GridBagConstraints(); + gbc_tabbedPane.gridwidth = 5; + gbc_tabbedPane.insets = new Insets(0, 0, 0, 5); + gbc_tabbedPane.fill = GridBagConstraints.BOTH; + gbc_tabbedPane.gridx = 1; + gbc_tabbedPane.gridy = 7; + frame.getContentPane().add(tabbedPane, gbc_tabbedPane); + + btnChangeSource.addActionListener((e) -> { + File f; + Path path = Path.of(textFieldSource.getText().trim()).toAbsolutePath(); + if(Files.exists(path)) { + f = path.toAbsolutePath().toFile(); + } + else { + f = null; + } + + JFileChooser chooser = new JFileChooser(f); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int status = chooser.showOpenDialog(frame); + if(status == JFileChooser.APPROVE_OPTION) { + try { + selectDirectories(tabbedPane, chooser.getSelectedFile(), new File(textFieldDest.getText())); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + + btnChangeDest.addActionListener((e) -> { + File f; + Path path = Path.of(textFieldDest.getText().trim()).toAbsolutePath(); + if(Files.exists(path)) { + f = path.toAbsolutePath().toFile(); + } + else { + f = null; + } + + JFileChooser chooser = new JFileChooser(f); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int status = chooser.showOpenDialog(frame); + if(status == JFileChooser.APPROVE_OPTION) { + try { + selectDirectories(tabbedPane, new File(textFieldSource.getText()), chooser.getSelectedFile()); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + + btnExtract.addActionListener((e) -> { + try { + File folder = new File(textFieldDest.getText()); + if(folder.exists()) { + LinkedHashSet paths = FileUtils.getFilesRecursively(folder.toPath()); + if(!folder.isDirectory()) { + throw new NotDirectoryException(folder.getAbsolutePath().toString()); + } + if(paths.size() != 0) { + int result = JOptionPane.showOptionDialog(frame, "Are you sure? This directory has " + paths.size() + " pre-existing files.\n\nDuplicates will be overridden", "Overwrite?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, null, null); + if(result != 0) { + return; + } + + } + new Thread(() -> { + try { + copyOperations.startBatch(); + } catch (InterruptedException e1) { + throw new RuntimeException(e1); + } + }).start(); + } + + + } + catch(Throwable t) { + throw new RuntimeException(t); + } + }); + } + + private void selectDirectories(EJTabbedPane pane, File sourceDir, File destDir) throws InterruptedException { + Tab tab = pane.getSelectedTab(); + if(sourceDir != null && !sourceDir.getPath().isBlank()) { + textFieldSource.setText(sourceDir.getAbsolutePath()); + } + else { + textFieldSource.setText(""); + } + if(destDir != null && !destDir.getPath().isBlank()) { + textFieldDest.setText(destDir.getAbsolutePath()); + } + else { + textFieldDest.setText(""); + } + + copyOperations.shutdownNow(); + unarchiver = genUnarchiver(sourceDir, destDir); + copyOperations = genCopyBatches(); + setupTabbedPane(pane); + pane.setSelectedTab(tab); + System.out.println("Set tab!"); + update(); + } + + public EJTabbedPane setupTabbedPane(EJTabbedPane tabbedPane) { + Tab consoleTab = setupConsoleOutputTab(tabbedPane); + Tab statusTab = setupStatusTab(tabbedPane); + Tab progressTab = setupProgressTab(tabbedPane); + + tabbedPane.removeAll(); + tabbedPane.setTabLayoutPolicy(EJTabbedPane.SCROLL_TAB_LAYOUT); + tabbedPane.addTab(consoleTab); + tabbedPane.addTab(statusTab); + tabbedPane.addTab(progressTab); + + for(Batcher>> b : copyOperations.getBatches()) { + tabbedPane.addTab(b.getName(), new JPanel()); + } + + return tabbedPane; + } + + private Tab setupConsoleOutputTab(EJTabbedPane tabbedPane) { + Tab consoleTab = tabbedPane.getTab(CONSOLE); + if(consoleTab != null) { + return consoleTab; + } + + JPanel consolePanel = new JPanel(); + consolePanel.setLayout(new GridLayout(0, 2, 0, 0)); + JTextArea textArea = new JTextArea(); + textArea.setBorder(BorderFactory.createLoweredBevelBorder()); + textArea.setOpaque(true); + JTextAreaOutputStream textPaneOutputStream = new JTextAreaOutputStream(textArea); + System.setOut(new PrintStream(SplitOutputStream.splitSysOut(textPaneOutputStream))); + System.setErr(new PrintStream(SplitOutputStream.splitErrOut(textPaneOutputStream))); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + new Throwable().printStackTrace(pw); + textArea.setText(sw.toString()); + + JScrollPane scrollPaneConsole = new JScrollPane(textArea); + scrollPaneConsole.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + Tab tab = new Tab(CONSOLE, null, scrollPaneConsole, null); + tabbedPane.addTab(tab.title(), tab.icon(), tab.component(), tab.tip()); //need to add this separately so the window builder can see it + return tab; + } + + private Tab setupStatusTab(EJTabbedPane tabbedPane) { + Tab statusTab = tabbedPane.getTab(STATUS); + + if(statusTab != null) { + //remove all + } + + JPanel contents = setupStatusNavigationPanel(tabbedPane); + + + + Tab tab = new Tab(STATUS, null, contents, null); + JPanel statusGrid = new JPanel(new WrapLayout()); + JScrollPane statusGridScroller = new JScrollPane(statusGrid); + + Iterator>>> batches = copyOperations.getBatches().iterator(); + int i = 0; + while(batches.hasNext()) { + BatchOperationComponent b = new BatchOperationComponent(batches.next()); + b.setPreferredSize(new Dimension(150, 175)); + statusGrid.add(b); + i++; + } + + GridBagConstraints gbc_statusGridScroller = new GridBagConstraints(); + gbc_statusGridScroller.fill = GridBagConstraints.BOTH; + gbc_statusGridScroller.gridx = 0; + gbc_statusGridScroller.gridy = 1; + contents.add(statusGridScroller, gbc_statusGridScroller); + + statusGridScroller.getVerticalScrollBar().setUnitIncrement(175 / 2); + tabbedPane.addTab(tab.title(), tab.icon(), tab.component(), tab.tip()); //need to add this separately so the window builder can see it + + + return tab; + } + + private JPanel setupStatusNavigationPanel(EJTabbedPane tabbedPane) { + JPanel contents = new JPanel(); + GridBagLayout gbl_contents = new GridBagLayout(); + gbl_contents.columnWidths = new int[]{22, 0}; + gbl_contents.rowHeights = new int[]{0, 13, 0}; + gbl_contents.columnWeights = new double[]{1.0, Double.MIN_VALUE}; + gbl_contents.rowWeights = new double[]{0.0, 1.0, Double.MIN_VALUE}; + contents.setLayout(gbl_contents); + + JPanel NavigationPanel = new JPanel(); + GridBagConstraints gbc_NavigationPanel = new GridBagConstraints(); + gbc_NavigationPanel.insets = new Insets(0, 0, 5, 0); + gbc_NavigationPanel.fill = GridBagConstraints.BOTH; + gbc_NavigationPanel.gridx = 0; + gbc_NavigationPanel.gridy = 0; + contents.add(NavigationPanel, gbc_NavigationPanel); + NavigationPanel.setLayout(new BorderLayout(0, 0)); + + JLabel lblKey = new JLabel("Legend"); + lblKey.setHorizontalAlignment(SwingConstants.CENTER); + NavigationPanel.add(lblKey); + + JPanel keysPanel = new JPanel(); + FlowLayout flowLayout = (FlowLayout) keysPanel.getLayout(); + flowLayout.setHgap(20); + flowLayout.setVgap(0); + NavigationPanel.add(keysPanel, BorderLayout.SOUTH); + + keysPanel.add(new ColorKeyComponent(Color.GRAY, "Not Started")); + keysPanel.add(new ColorKeyComponent(Color.ORANGE, "Other")); + keysPanel.add(new ColorKeyComponent(Color.WHITE, "Working")); + keysPanel.add(new ColorKeyComponent(Color.RED, "Failure")); + keysPanel.add(new ColorKeyComponent(Color.CYAN.darker(), "Skipped")); + keysPanel.add(new ColorKeyComponent(Color.GREEN, "Success")); + + return contents; + } + + public Tab setupProgressTab(EJTabbedPane tabbedPane) { + Tab progressTab = tabbedPane.getTab(PROGRESS); + /*if(progressTab != null) { + return progressTab; + }*/ + + JPanel progressPanel = new JPanel(); + Tab tab = new Tab(PROGRESS, null, progressPanel, null); + tabbedPane.addTab(tab.title(), tab.icon(), tab.component(), tab.tip()); //need to add this separately so the window builder can see it + + progressPanel.setLayout(new GridLayout(0, 2, 0, 0)); + setupLeftProgressPane(progressPanel); + setupRightProgressPane(progressPanel); + + return tab; + } + + private void setupLeftProgressPane(JPanel progressPanel) { + JPanel leftPanel = new JPanel(); + progressPanel.add(leftPanel); + GridBagLayout gbl_leftPanel = new GridBagLayout(); + gbl_leftPanel.columnWidths = new int[] {100, 0, 70, 90, 0, 0, 11}; + gbl_leftPanel.rowHeights = new int[] {0, 15, 0, 0, 30, 0, 0, 30, 0, 0, 30, 0, 0, 29, 0}; + gbl_leftPanel.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE}; + gbl_leftPanel.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, Double.MIN_VALUE}; + leftPanel.setLayout(gbl_leftPanel); + + JLabel lblCopyOperation = new JLabel("Unarchive Operation"); + GridBagConstraints gbc_lblCopyOperation = new GridBagConstraints(); + gbc_lblCopyOperation.gridwidth = 6; + gbc_lblCopyOperation.insets = new Insets(0, 0, 5, 5); + gbc_lblCopyOperation.gridx = 0; + gbc_lblCopyOperation.gridy = 0; + leftPanel.add(lblCopyOperation, gbc_lblCopyOperation); + + JSeparator separator = new JSeparator(); + separator.setOrientation(SwingConstants.VERTICAL); + separator.setPreferredSize(new Dimension(1, 1)); + GridBagConstraints gbc_separator = new GridBagConstraints(); + gbc_separator.gridheight = 14; + gbc_separator.fill = GridBagConstraints.VERTICAL; + gbc_separator.gridx = 6; + gbc_separator.gridy = 0; + leftPanel.add(separator, gbc_separator); + BatchOperationComponent copyBatchComponent = new BatchOperationComponent(copyOperations); + copyOperations.addBatchListener(copyBatchComponent); + copyBatchComponent.setToolTipText("All Batches"); + System.out.println(copyOperations.getBatches().size() + " OISHDFPIOHDFIUOSPHUIFSIDOHUOFHUIOSDHIFUHOHIUO"); + + GridBagConstraints gbc_copyBatchComponent = new GridBagConstraints(); + gbc_copyBatchComponent.fill = GridBagConstraints.BOTH; + gbc_copyBatchComponent.gridheight = 13; + gbc_copyBatchComponent.insets = new Insets(0, 0, 0, 5); + gbc_copyBatchComponent.gridx = 0; + gbc_copyBatchComponent.gridy = 1; + leftPanel.add(copyBatchComponent, gbc_copyBatchComponent); + + JSeparator separator_1 = new JSeparator(); + GridBagConstraints gbc_separator_1 = new GridBagConstraints(); + gbc_separator_1.fill = GridBagConstraints.BOTH; + gbc_separator_1.gridheight = 10; + gbc_separator_1.insets = new Insets(0, 0, 5, 5); + gbc_separator_1.gridx = 1; + gbc_separator_1.gridy = 0; + leftPanel.add(separator_1, gbc_separator_1); + + JLabel lblTotalArchives = new JLabel("Total Archives:"); + lblTotalArchives.setHorizontalAlignment(SwingConstants.CENTER); + GridBagConstraints gbc_lblTotalArchives = new GridBagConstraints(); + gbc_lblTotalArchives.insets = new Insets(0, 0, 5, 5); + gbc_lblTotalArchives.anchor = GridBagConstraints.NORTHEAST; + gbc_lblTotalArchives.gridx = 2; + gbc_lblTotalArchives.gridy = 2; + leftPanel.add(lblTotalArchives, gbc_lblTotalArchives); + + JLabel lblTotalArchivesCount = new JLabel("0"); + GridBagConstraints gbc_lblTotalArchivesCount = new GridBagConstraints(); + gbc_lblTotalArchivesCount.anchor = GridBagConstraints.WEST; + gbc_lblTotalArchivesCount.insets = new Insets(0, 0, 5, 5); + gbc_lblTotalArchivesCount.gridx = 3; + gbc_lblTotalArchivesCount.gridy = 2; + leftPanel.add(lblTotalArchivesCount, gbc_lblTotalArchivesCount); + + JLabel lblFoundResources = new JLabel("Total Resources:"); + lblFoundResources.setHorizontalAlignment(SwingConstants.RIGHT); + GridBagConstraints gbc_lblFoundResources = new GridBagConstraints(); + gbc_lblFoundResources.anchor = GridBagConstraints.EAST; + gbc_lblFoundResources.insets = new Insets(0, 0, 5, 5); + gbc_lblFoundResources.gridx = 2; + gbc_lblFoundResources.gridy = 3; + leftPanel.add(lblFoundResources, gbc_lblFoundResources); + + JLabel lblTotalResourcesCount = new JLabel("0"); + GridBagConstraints gbc_lblTotalResourcesCount = new GridBagConstraints(); + gbc_lblTotalResourcesCount.anchor = GridBagConstraints.WEST; + gbc_lblTotalResourcesCount.insets = new Insets(0, 0, 5, 5); + gbc_lblTotalResourcesCount.gridx = 3; + gbc_lblTotalResourcesCount.gridy = 3; + leftPanel.add(lblTotalResourcesCount, gbc_lblTotalResourcesCount); + + JLabel lblArchivesCopied = new JLabel("Archives Copied:"); + GridBagConstraints gbc_lblArchivesCopied = new GridBagConstraints(); + gbc_lblArchivesCopied.anchor = GridBagConstraints.EAST; + gbc_lblArchivesCopied.insets = new Insets(0, 0, 5, 5); + gbc_lblArchivesCopied.gridx = 2; + gbc_lblArchivesCopied.gridy = 5; + leftPanel.add(lblArchivesCopied, gbc_lblArchivesCopied); + + JLabel lblArchivesCopiedCount = new JLabel("0"); + GridBagConstraints gbc_lblArchivesCopiedCount = new GridBagConstraints(); + gbc_lblArchivesCopiedCount.anchor = GridBagConstraints.WEST; + gbc_lblArchivesCopiedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblArchivesCopiedCount.gridx = 3; + gbc_lblArchivesCopiedCount.gridy = 5; + leftPanel.add(lblArchivesCopiedCount, gbc_lblArchivesCopiedCount); + + JLabel lblResourcesCopied = new JLabel("Resources Copied:"); + GridBagConstraints gbc_lblResourcesCopied = new GridBagConstraints(); + gbc_lblResourcesCopied.anchor = GridBagConstraints.EAST; + gbc_lblResourcesCopied.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesCopied.gridx = 2; + gbc_lblResourcesCopied.gridy = 6; + leftPanel.add(lblResourcesCopied, gbc_lblResourcesCopied); + + JLabel lblResourcesCopiedCount = new JLabel("0"); + GridBagConstraints gbc_lblResourcesCopiedCount = new GridBagConstraints(); + gbc_lblResourcesCopiedCount.anchor = GridBagConstraints.WEST; + gbc_lblResourcesCopiedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesCopiedCount.gridx = 3; + gbc_lblResourcesCopiedCount.gridy = 6; + leftPanel.add(lblResourcesCopiedCount, gbc_lblResourcesCopiedCount); + + JLabel lblResourcesCopiedPercent = new JLabel("0%"); + GridBagConstraints gbc_lblResourcesCopiedPercent = new GridBagConstraints(); + gbc_lblResourcesCopiedPercent.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesCopiedPercent.gridx = 4; + gbc_lblResourcesCopiedPercent.gridy = 6; + leftPanel.add(lblResourcesCopiedPercent, gbc_lblResourcesCopiedPercent); + + JLabel lblArchivesSkipped = new JLabel("Archives Skipped:"); + lblArchivesSkipped.setHorizontalAlignment(SwingConstants.RIGHT); + GridBagConstraints gbc_lblArchivesSkipped = new GridBagConstraints(); + gbc_lblArchivesSkipped.anchor = GridBagConstraints.EAST; + gbc_lblArchivesSkipped.insets = new Insets(0, 0, 5, 5); + gbc_lblArchivesSkipped.gridx = 2; + gbc_lblArchivesSkipped.gridy = 8; + leftPanel.add(lblArchivesSkipped, gbc_lblArchivesSkipped); + + JLabel lblArchivesSkippedCount = new JLabel("0"); + GridBagConstraints gbc_lblArchivesSkippedCount = new GridBagConstraints(); + gbc_lblArchivesSkippedCount.anchor = GridBagConstraints.WEST; + gbc_lblArchivesSkippedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblArchivesSkippedCount.gridx = 3; + gbc_lblArchivesSkippedCount.gridy = 8; + leftPanel.add(lblArchivesSkippedCount, gbc_lblArchivesSkippedCount); + + JLabel lblArchivesSkippedPercent = new JLabel("0%"); + GridBagConstraints gbc_lblArchivesSkippedPercent = new GridBagConstraints(); + gbc_lblArchivesSkippedPercent.insets = new Insets(0, 0, 5, 5); + gbc_lblArchivesSkippedPercent.gridx = 4; + gbc_lblArchivesSkippedPercent.gridy = 8; + leftPanel.add(lblArchivesSkippedPercent, gbc_lblArchivesSkippedPercent); + + JLabel lblResourcesSkipped = new JLabel("Resources Skipped:"); + GridBagConstraints gbc_lblResourcesSkipped = new GridBagConstraints(); + gbc_lblResourcesSkipped.anchor = GridBagConstraints.EAST; + gbc_lblResourcesSkipped.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesSkipped.gridx = 2; + gbc_lblResourcesSkipped.gridy = 9; + leftPanel.add(lblResourcesSkipped, gbc_lblResourcesSkipped); + + JLabel lblResourcesSkippedCount = new JLabel("0"); + GridBagConstraints gbc_lblResourcesSkippedCount = new GridBagConstraints(); + gbc_lblResourcesSkippedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesSkippedCount.anchor = GridBagConstraints.WEST; + gbc_lblResourcesSkippedCount.gridx = 3; + gbc_lblResourcesSkippedCount.gridy = 9; + leftPanel.add(lblResourcesSkippedCount, gbc_lblResourcesSkippedCount); + + JLabel labelResourcesSkippedPercent = new JLabel("0%"); + GridBagConstraints gbc_labelResourcesSkippedPercent = new GridBagConstraints(); + gbc_labelResourcesSkippedPercent.insets = new Insets(0, 0, 5, 5); + gbc_labelResourcesSkippedPercent.gridx = 4; + gbc_labelResourcesSkippedPercent.gridy = 9; + leftPanel.add(labelResourcesSkippedPercent, gbc_labelResourcesSkippedPercent); + + JLabel lblArchivesFailed = new JLabel("Archives Failed:"); + lblArchivesFailed.setHorizontalAlignment(SwingConstants.RIGHT); + GridBagConstraints gbc_lblArchivesFailed = new GridBagConstraints(); + gbc_lblArchivesFailed.anchor = GridBagConstraints.EAST; + gbc_lblArchivesFailed.insets = new Insets(0, 0, 5, 5); + gbc_lblArchivesFailed.gridx = 2; + gbc_lblArchivesFailed.gridy = 11; + leftPanel.add(lblArchivesFailed, gbc_lblArchivesFailed); + + JLabel lblArchivesFailedCount = new JLabel("0"); + GridBagConstraints gbc_lblArchivesFailedCount = new GridBagConstraints(); + gbc_lblArchivesFailedCount.anchor = GridBagConstraints.WEST; + gbc_lblArchivesFailedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblArchivesFailedCount.gridx = 3; + gbc_lblArchivesFailedCount.gridy = 11; + leftPanel.add(lblArchivesFailedCount, gbc_lblArchivesFailedCount); + + JLabel lblArchivesFailedPercent = new JLabel("0%"); + GridBagConstraints gbc_lblArchivesFailedPercent = new GridBagConstraints(); + gbc_lblArchivesFailedPercent.insets = new Insets(0, 0, 5, 5); + gbc_lblArchivesFailedPercent.gridx = 4; + gbc_lblArchivesFailedPercent.gridy = 11; + leftPanel.add(lblArchivesFailedPercent, gbc_lblArchivesFailedPercent); + + JLabel lblResourcesFailed = new JLabel("Resources Failed:"); + GridBagConstraints gbc_lblResourcesFailed = new GridBagConstraints(); + gbc_lblResourcesFailed.anchor = GridBagConstraints.EAST; + gbc_lblResourcesFailed.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesFailed.gridx = 2; + gbc_lblResourcesFailed.gridy = 12; + leftPanel.add(lblResourcesFailed, gbc_lblResourcesFailed); + + JLabel lblResourcesFailedCount = new JLabel("0"); + GridBagConstraints gbc_lblResourcesFailedCount = new GridBagConstraints(); + gbc_lblResourcesFailedCount.anchor = GridBagConstraints.WEST; + gbc_lblResourcesFailedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesFailedCount.gridx = 3; + gbc_lblResourcesFailedCount.gridy = 12; + leftPanel.add(lblResourcesFailedCount, gbc_lblResourcesFailedCount); + + JLabel lblResourcesFailedPercent = new JLabel("0%"); + GridBagConstraints gbc_lblResourcesFailedPercent = new GridBagConstraints(); + gbc_lblResourcesFailedPercent.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesFailedPercent.gridx = 4; + gbc_lblResourcesFailedPercent.gridy = 12; + leftPanel.add(lblResourcesFailedPercent, gbc_lblResourcesFailedPercent); + + copyOperations.addBatchListener(() -> { + SwingUtilities.invokeLater(() -> { + //lblResourcesCopiedCount.setText(copyBatchComponent.); + update(); + }); + }); + } + + private void setupRightProgressPane(JPanel progressPanel) { + JPanel rightPanel = new JPanel(); + progressPanel.add(rightPanel); + GridBagLayout gbl_rightPanel = new GridBagLayout(); + gbl_rightPanel.columnWidths = new int[] {0, 0, 90, 0, 30, 30, 0, 0}; + gbl_rightPanel.rowHeights = new int[]{0, 30, 0, 30, 0, 0, 0, 30, 30, 30, 30, 0, 0, 0, 0}; + gbl_rightPanel.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE}; + gbl_rightPanel.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, Double.MIN_VALUE}; + rightPanel.setLayout(gbl_rightPanel); + + BatchOperationComponent copyBatchComponent = new BatchOperationComponent(processOperations); + copyBatchComponent.setToolTipText("All Batches"); + + GridBagConstraints gbc_copyBatchComponent = new GridBagConstraints(); + gbc_copyBatchComponent.fill = GridBagConstraints.BOTH; + gbc_copyBatchComponent.gridheight = 13; + gbc_copyBatchComponent.insets = new Insets(0, 0, 5, 5); + gbc_copyBatchComponent.gridx = 0; + gbc_copyBatchComponent.gridy = 1; + rightPanel.add(copyBatchComponent, gbc_copyBatchComponent); + + JLabel lblNewLabel = new JLabel("Process Operation"); + GridBagConstraints gbc_lblNewLabel = new GridBagConstraints(); + gbc_lblNewLabel.gridwidth = 7; + gbc_lblNewLabel.insets = new Insets(0, 0, 5, 5); + gbc_lblNewLabel.gridx = 0; + gbc_lblNewLabel.gridy = 0; + rightPanel.add(lblNewLabel, gbc_lblNewLabel); + + JLabel lblTotalResources = new JLabel("Total Resources:"); + GridBagConstraints gbc_lblTotalResources = new GridBagConstraints(); + gbc_lblTotalResources.anchor = GridBagConstraints.EAST; + gbc_lblTotalResources.insets = new Insets(0, 0, 5, 5); + gbc_lblTotalResources.gridx = 1; + gbc_lblTotalResources.gridy = 2; + rightPanel.add(lblTotalResources, gbc_lblTotalResources); + + JLabel lblTotalResourcesCount = new JLabel("0"); + GridBagConstraints gbc_lblTotalResourcesCount = new GridBagConstraints(); + gbc_lblTotalResourcesCount.anchor = GridBagConstraints.WEST; + gbc_lblTotalResourcesCount.insets = new Insets(0, 0, 5, 5); + gbc_lblTotalResourcesCount.gridx = 2; + gbc_lblTotalResourcesCount.gridy = 2; + rightPanel.add(lblTotalResourcesCount, gbc_lblTotalResourcesCount); + + JLabel lblResourcesProcessed = new JLabel("Resources Processed:"); + GridBagConstraints gbc_lblResourcesProcessed = new GridBagConstraints(); + gbc_lblResourcesProcessed.anchor = GridBagConstraints.EAST; + gbc_lblResourcesProcessed.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesProcessed.gridx = 1; + gbc_lblResourcesProcessed.gridy = 4; + rightPanel.add(lblResourcesProcessed, gbc_lblResourcesProcessed); + + JLabel lblResourcesProcessedCount = new JLabel("0"); + GridBagConstraints gbc_lblResourcesProcessedCount = new GridBagConstraints(); + gbc_lblResourcesProcessedCount.anchor = GridBagConstraints.WEST; + gbc_lblResourcesProcessedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesProcessedCount.gridx = 2; + gbc_lblResourcesProcessedCount.gridy = 4; + rightPanel.add(lblResourcesProcessedCount, gbc_lblResourcesProcessedCount); + + JLabel lblResourcesProcessedPercent = new JLabel("0%"); + GridBagConstraints gbc_lblResourcesProcessedPercent = new GridBagConstraints(); + gbc_lblResourcesProcessedPercent.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesProcessedPercent.gridx = 3; + gbc_lblResourcesProcessedPercent.gridy = 4; + rightPanel.add(lblResourcesProcessedPercent, gbc_lblResourcesProcessedPercent); + + JLabel lblResourcesSkipped = new JLabel("Resources Skipped:"); + GridBagConstraints gbc_lblResourcesSkipped = new GridBagConstraints(); + gbc_lblResourcesSkipped.anchor = GridBagConstraints.EAST; + gbc_lblResourcesSkipped.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesSkipped.gridx = 1; + gbc_lblResourcesSkipped.gridy = 5; + rightPanel.add(lblResourcesSkipped, gbc_lblResourcesSkipped); + + JLabel lblResourcesSkippedCount = new JLabel("0"); + GridBagConstraints gbc_lblResourcesSkippedCount = new GridBagConstraints(); + gbc_lblResourcesSkippedCount.anchor = GridBagConstraints.WEST; + gbc_lblResourcesSkippedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesSkippedCount.gridx = 2; + gbc_lblResourcesSkippedCount.gridy = 5; + rightPanel.add(lblResourcesSkippedCount, gbc_lblResourcesSkippedCount); + + JLabel lblResourcesSkippedPercent = new JLabel("0%"); + GridBagConstraints gbc_lblResourcesSkippedPercent = new GridBagConstraints(); + gbc_lblResourcesSkippedPercent.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesSkippedPercent.gridx = 3; + gbc_lblResourcesSkippedPercent.gridy = 5; + rightPanel.add(lblResourcesSkippedPercent, gbc_lblResourcesSkippedPercent); + + JLabel lblResourcesFailed = new JLabel("Resources Failed:"); + GridBagConstraints gbc_lblResourcesFailed = new GridBagConstraints(); + gbc_lblResourcesFailed.anchor = GridBagConstraints.EAST; + gbc_lblResourcesFailed.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesFailed.gridx = 1; + gbc_lblResourcesFailed.gridy = 6; + rightPanel.add(lblResourcesFailed, gbc_lblResourcesFailed); + + JLabel lblResourcesFailedCount = new JLabel("0"); + GridBagConstraints gbc_lblResourcesFailedCount = new GridBagConstraints(); + gbc_lblResourcesFailedCount.anchor = GridBagConstraints.WEST; + gbc_lblResourcesFailedCount.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesFailedCount.gridx = 2; + gbc_lblResourcesFailedCount.gridy = 6; + rightPanel.add(lblResourcesFailedCount, gbc_lblResourcesFailedCount); + + JLabel lblResourcesFailedPercent = new JLabel("0%"); + GridBagConstraints gbc_lblResourcesFailedPercent = new GridBagConstraints(); + gbc_lblResourcesFailedPercent.insets = new Insets(0, 0, 5, 5); + gbc_lblResourcesFailedPercent.gridx = 3; + gbc_lblResourcesFailedPercent.gridy = 6; + rightPanel.add(lblResourcesFailedPercent, gbc_lblResourcesFailedPercent); + } + + private Unarchiver genUnarchiver(File source, File dest) { + try { + Unarchiver unarchiver = new Unarchiver(source.toPath(), dest.toPath()); + return unarchiver; + } + catch(Throwable t) { + return null; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private BatchRunner>> genCopyBatches() { + BatchRunner>> batchRunner = new BatchRunner("Unarchive", this.getUsableThreads()); + try { + if(unarchiver != null) { + if(unarchiver.isValid()) { + for(Batch>> batch : unarchiver.getCopyBatches()) { + batchRunner.addBatch(batch); + } + } + } + } + catch(Throwable t) { + Batch>> errBatch = new Batch<>("Error"); + errBatch.addRunnable(() -> { + throw t; + }); + batchRunner.addBatch(errBatch); + } + return batchRunner; + } + + private BatchRunner genProcessBatches(File dir) { + BatchRunner batchRunner = new BatchRunner("Process"); + if(dir != null) { + //stuff + } + return batchRunner; + } + + public static enum BatchResult { + SUCCESS, + FAILURE, + SKIP + } + + @Override + public void update() { + frame.invalidate(); + frame.repaint(); + } + + private int getUsableThreads() { + return Math.clamp(threadSlider.getValue(), 1, Runtime.getRuntime().availableProcessors()); + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/WrapLayout.java b/src/main/java/com/gamebuster19901/excite/modding/ui/WrapLayout.java new file mode 100644 index 0000000..2260ef3 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/WrapLayout.java @@ -0,0 +1,192 @@ +package com.gamebuster19901.excite.modding.ui; + +import java.awt.*; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +/** + * FlowLayout subclass that fully supports wrapping of components. + */ +public class WrapLayout extends FlowLayout +{ + private Dimension preferredLayoutSize; + + /** + * Constructs a new WrapLayout with a left + * alignment and a default 5-unit horizontal and vertical gap. + */ + public WrapLayout() + { + super(); + } + + /** + * Constructs a new FlowLayout with the specified + * alignment and a default 5-unit horizontal and vertical gap. + * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, + * or WrapLayout. + * @param align the alignment value + */ + public WrapLayout(int align) + { + super(align); + } + + /** + * Creates a new flow layout manager with the indicated alignment + * and the indicated horizontal and vertical gaps. + *

+ * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, + * or WrapLayout. + * @param align the alignment value + * @param hgap the horizontal gap between components + * @param vgap the vertical gap between components + */ + public WrapLayout(int align, int hgap, int vgap) + { + super(align, hgap, vgap); + } + + /** + * Returns the preferred dimensions for this layout given the + * visible components in the specified target container. + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container + */ + @Override + public Dimension preferredLayoutSize(Container target) + { + return layoutSize(target, true); + } + + /** + * Returns the minimum dimensions needed to layout the visible + * components contained in the specified target container. + * @param target the component which needs to be laid out + * @return the minimum dimensions to lay out the + * subcomponents of the specified container + */ + @Override + public Dimension minimumLayoutSize(Container target) + { + Dimension minimum = layoutSize(target, false); + minimum.width -= (getHgap() + 1); + return minimum; + } + + /** + * Returns the minimum or preferred dimension needed to layout the target + * container. + * + * @param target target to get layout size for + * @param preferred should preferred size be calculated + * @return the dimension to layout the target container + */ + private Dimension layoutSize(Container target, boolean preferred) + { + synchronized (target.getTreeLock()) + { + // Each row must fit with the width allocated to the containter. + // When the container width = 0, the preferred width of the container + // has not yet been calculated so lets ask for the maximum. + + int targetWidth = target.getSize().width; + Container container = target; + + while (container.getSize().width == 0 && container.getParent() != null) + { + container = container.getParent(); + } + + targetWidth = container.getSize().width; + + if (targetWidth == 0) + targetWidth = Integer.MAX_VALUE; + + int hgap = getHgap(); + int vgap = getVgap(); + Insets insets = target.getInsets(); + int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2); + int maxWidth = targetWidth - horizontalInsetsAndGap; + + // Fit components into the allowed width + + Dimension dim = new Dimension(0, 0); + int rowWidth = 0; + int rowHeight = 0; + + int nmembers = target.getComponentCount(); + + for (int i = 0; i < nmembers; i++) + { + Component m = target.getComponent(i); + + if (m.isVisible()) + { + Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); + + // Can't add the component to current row. Start a new row. + + if (rowWidth + d.width > maxWidth) + { + addRow(dim, rowWidth, rowHeight); + rowWidth = 0; + rowHeight = 0; + } + + // Add a horizontal gap for all components after the first + + if (rowWidth != 0) + { + rowWidth += hgap; + } + + rowWidth += d.width; + rowHeight = Math.max(rowHeight, d.height); + } + } + + addRow(dim, rowWidth, rowHeight); + + dim.width += horizontalInsetsAndGap; + dim.height += insets.top + insets.bottom + vgap * 2; + + // When using a scroll pane or the DecoratedLookAndFeel we need to + // make sure the preferred size is less than the size of the + // target containter so shrinking the container size works + // correctly. Removing the horizontal gap is an easy way to do this. + + Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target); + + if (scrollPane != null && target.isValid()) + { + dim.width -= (hgap + 1); + } + + return dim; + } + } + + /* + * A new row has been completed. Use the dimensions of this row + * to update the preferred size for the container. + * + * @param dim update the width and height when appropriate + * @param rowWidth the width of the row to add + * @param rowHeight the height of the row to add + */ + private void addRow(Dimension dim, int rowWidth, int rowHeight) + { + dim.width = Math.max(dim.width, rowWidth); + + if (dim.height > 0) + { + dim.height += getVgap(); + } + + dim.height += rowHeight; + } +} \ No newline at end of file diff --git a/src/main/java/com/gamebuster19901/excite/modding/ui/package-info.java b/src/main/java/com/gamebuster19901/excite/modding/ui/package-info.java new file mode 100644 index 0000000..b7d428a --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/ui/package-info.java @@ -0,0 +1 @@ +package com.gamebuster19901.excite.modding.ui; \ No newline at end of file diff --git a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/Archive.java b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/Archive.java index 4af6f2e..0260593 100644 --- a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/Archive.java +++ b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/Archive.java @@ -18,7 +18,7 @@ public class Archive { private final Path archiveFile; private final ResMonster archive; private final Toc toc; - private byte[] bytes; + private final byte[] bytes; private final LinkedHashMap files = new LinkedHashMap<>(); public Archive(Path archivePath, Path tocPath) throws IOException { @@ -29,6 +29,18 @@ public Archive(Path archivePath, Toc toc) throws IOException { this.archiveFile = archivePath; this.archive = ResMonster.fromFile(archivePath.toAbsolutePath().toString()); this.toc = toc; + if(isCompressed()) { + bytes = archive.data().compressedData().bytes(); + } + else { + bytes = archive.data().uncompressedData(); + } + if(bytes == null) { + throw new AssertionError(); + } + if(bytes.length == 0) { + throw new AssertionError(); + } for(TocMonster.Details fileDetails : getFileDetails()) { try { files.put(fileDetails.name(), new ArchivedFile(fileDetails, this)); @@ -37,6 +49,7 @@ public Archive(Path archivePath, Toc toc) throws IOException { //swallo } } + } public Toc getToc() { @@ -80,7 +93,7 @@ public long getHash() { } public boolean isCompressed() { - if (archive.header().compressed() == 128) { + if (((archive.header().compressed() >>> 7) & 1) != 0) { return true; } else if (archive.header().compressed() == 0) { @@ -92,14 +105,6 @@ else if (archive.header().compressed() == 0) { } public byte[] getBytes() { - if(bytes == null) { - if(isCompressed()) { - bytes = archive.data().compressedData().bytes(); - } - else { - bytes = archive.data().uncompressedData(); - } - } return Arrays.copyOf(bytes, bytes.length); } diff --git a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/ArchivedFile.java b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/ArchivedFile.java index b956de5..f45e7d8 100644 --- a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/ArchivedFile.java +++ b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/ArchivedFile.java @@ -12,28 +12,21 @@ public class ArchivedFile { private final TocMonster.Details fileDetails; private final Archive archive; - private final byte[] bytes; public ArchivedFile(TocMonster.Details fileDetails, Archive archive) { try { this.fileDetails = fileDetails; this.archive = archive; + System.out.println("Archive size: " + archive.getUncompressedSize()); System.out.println("File offset: " + fileDetails.fileOffset()); System.out.println("File size: " + fileDetails.fileSize()); System.out.println("File end: " + ((int)fileDetails.fileOffset() + (int)fileDetails.fileSize())); - System.out.println("Thread: " + Thread.currentThread().getName()); - System.out.println(archive.getBytes().length); - this.bytes = Arrays.copyOfRange(archive.getBytes(), (int)fileDetails.fileOffset(), (int)(fileDetails.fileOffset() + (int)fileDetails.fileSize())); + byte[] bytes = archive.getBytes(); + System.out.println("Array size: " + bytes.length); } catch(Throwable t) { - System.err.println("Could not extract resource " + getName() + " from " + archive.getArchiveFile().getFileName()); - java.util.Collection a1 = java.lang.Thread.getAllStackTraces().values(); - for (java.lang.StackTraceElement[] a2 : a1){ - System.out.println("========== "); - for (java.lang.StackTraceElement a3 : a2){ - System.out.println(a3.toString()); - } - } + System.err.println("Bad resource reference: " + getName() + " from " + archive.getArchiveFile().getFileName()); + t.printStackTrace(); throw t; } } @@ -42,8 +35,13 @@ public String getName() { return fileDetails.name(); } - public byte[] getBytes() { - return bytes; + public byte[] getBytes() throws IOException { + try { + return Arrays.copyOfRange(archive.getBytes(), (int)fileDetails.fileOffset(), (int)(fileDetails.fileOffset() + (int)fileDetails.fileSize())); + } + catch(Throwable t) { + throw new IOException(t); + } } public void writeTo(Path directory) throws IOException { diff --git a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/QuickAccessArchive.java b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/QuickAccessArchive.java new file mode 100644 index 0000000..e845a0d --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/QuickAccessArchive.java @@ -0,0 +1,30 @@ +package com.gamebuster19901.excite.modding.unarchiver; + +import java.io.IOException; +import java.nio.file.Path; + +import org.apache.commons.lang3.concurrent.ConcurrentException; + +public class QuickAccessArchive { + + private final Toc toc; + private final Path archivePath; + private volatile Archive archive; //This MUST be volatile or double checked locking will not work! + + public QuickAccessArchive(Toc toc, Path archivePath) { + this.toc = toc; + this.archivePath = archivePath; + } + + public Archive getArchive() throws IOException, ConcurrentException { + if(archive == null) { + synchronized(this) { + if(archive == null) { + archive = new Archive(archivePath, toc); + } + } + } + return archive; + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/Unarchiver.java b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/Unarchiver.java index 88d5ad7..36565ee 100644 --- a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/Unarchiver.java +++ b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/Unarchiver.java @@ -4,64 +4,93 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; -import java.util.Scanner; +import java.util.List; +import java.util.concurrent.Callable; import java.util.stream.Stream; -import com.gamebuster19901.excite.modding.game.file.kaitai.TocMonster; +import org.apache.commons.lang3.tuple.Pair; + +import com.gamebuster19901.excite.modding.concurrent.Batch; import com.gamebuster19901.excite.modding.game.file.kaitai.TocMonster.Details; +import com.gamebuster19901.excite.modding.unarchiver.concurrent.DecisionType; +import com.gamebuster19901.excite.modding.util.FileUtils; public class Unarchiver { - private static final Path runDir = Path.of(".").resolve("run"); + private final Path sourceDir; + private final Path destDir; public LinkedHashSet tocs = new LinkedHashSet<>(); public LinkedHashSet archives = new LinkedHashSet<>(); - - public void unarchiveDir(Path dir) throws IOException { - try(Stream fileStream = Files.walk(dir)) { - fileStream.filter(Files::isRegularFile).forEach((f) -> { - if(f.getFileName().toString().endsWith(".toc")) { - tocs.add(f); - } - else { - archives.add(f); - } - }); - } + + public Unarchiver(Path sourceDir, Path destDir) throws IOException { + this.sourceDir = sourceDir; + this.destDir = destDir; + refresh(); + } + + public Collection getTocs() { + return Collections.unmodifiableCollection(tocs); } - public void unarchive(Path tocFile) throws IOException { - TocMonster toc = TocMonster.fromFile(tocFile.toAbsolutePath().toString()); - ArrayList

details = toc.details(); - for(Details fileDetails : details) { - System.out.println(tocFile.getFileName() + "/" + fileDetails.name()); + public Collection>>> getCopyBatches() { + LinkedHashSet>>> batches = new LinkedHashSet<>(); + for(Path toc : tocs) { + batches.add(getCopyBatch(toc)); } - Archive archive = null; - for(Path archivePath : archives) { - if(getFileName(tocFile).equals(getFileName(archivePath))) { - archive = new Archive(archivePath, tocFile); - break; + return batches; + } + + public Batch>> getCopyBatch(Path tocFile) { + Batch>> batch = new Batch<>(tocFile.getFileName().toString()); + try { + Toc toc = new Toc(tocFile.toAbsolutePath()); + List
details = toc.getFiles(); + + final QuickAccessArchive QArchive = getArchive(toc); + for(Details resource : details) { + batch.addRunnable(() -> { + try { + String resourceName = resource.name(); + Path dest = destDir.resolve(tocFile.getFileName()); + System.out.println(tocFile.getFileName() + "/" + resourceName); + Archive archive = QArchive.getArchive(); + ArchivedFile f = archive.getFile(resourceName); + f.writeTo(dest); + if(resource.name().endsWith("tex")) { + return Pair.of(DecisionType.SKIP, () -> {return null;}); //the asset was successfully extracted, but we don't know how to process it + } + return Pair.of(DecisionType.PROCEED, () -> {return null;}); //the asset was successfully extracted, and will be submitted to the next batchRunner to convert into a viewable format + } + catch(Throwable t) { + return Pair.of(DecisionType.IGNORE, () -> {throw t;}); //let the next batchrunner that an error ocurred, and will not be submitted to the next batchrunner. + } + }); } + } - if(archive == null) { - throw new FileNotFoundException("Resource file for toc " + tocFile); + catch(Throwable t) { + batch.addRunnable(() -> { + return Pair.of(DecisionType.IGNORE, () -> {throw t;}); //let the next batchrunner know that an error occurred + }); } - - archive.writeTo(runDir); + return batch; } - @SuppressWarnings("resource") - public static void main(String[] args) throws IOException { - Unarchiver unarchiver = new Unarchiver(); - unarchiver.unarchiveDir(Path.of("./gameData")); - - for(Path toc : unarchiver.tocs) { - System.out.println("Unarchiving " + toc); - unarchiver.unarchive(toc); + public boolean isValid() { + return FileUtils.isDirectory(sourceDir) && FileUtils.isDirectory(destDir); + } + + public QuickAccessArchive getArchive(Toc toc) throws IOException { + for(Path archivePath : archives) { + if(getFileName(toc.getFile()).equals(getFileName(archivePath))) { + return new QuickAccessArchive(toc, archivePath); + } } - new Scanner(System.in).nextLine(); //wait to exit + throw new FileNotFoundException("Resource file for toc " + toc.getFile().getFileName()); } private static String getFileName(Path f) { @@ -70,4 +99,19 @@ private static String getFileName(Path f) { return (i == -1) ? fileName : fileName.substring(0, i); } + private void refresh() throws IOException { + tocs.clear(); + archives.clear(); + try(Stream fileStream = Files.walk(sourceDir)) { + fileStream.filter(Files::isRegularFile).forEach((f) -> { + if(f.getFileName().toString().endsWith(".toc")) { + tocs.add(f); + } + else { + archives.add(f); + } + }); + } + } + } diff --git a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/DecisionType.java b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/DecisionType.java new file mode 100644 index 0000000..85fef16 --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/DecisionType.java @@ -0,0 +1,9 @@ +package com.gamebuster19901.excite.modding.unarchiver.concurrent; + +public enum DecisionType { + + PROCEED, //Proceed with the process + SKIP, //Don't proceed with the process, we know we don't know how to handle it + IGNORE; //Don't proceed with the process, an unexpected exception occurred. + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/Skippable.java b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/Skippable.java new file mode 100644 index 0000000..b707c4f --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/Skippable.java @@ -0,0 +1,7 @@ +package com.gamebuster19901.excite.modding.unarchiver.concurrent; + +public interface Skippable { + + public boolean shouldSkip(); + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/package-info.java b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/package-info.java new file mode 100644 index 0000000..1c2836b --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/unarchiver/concurrent/package-info.java @@ -0,0 +1 @@ +package com.gamebuster19901.excite.modding.unarchiver.concurrent; \ No newline at end of file diff --git a/src/main/java/com/gamebuster19901/excite/modding/FileUtils.java b/src/main/java/com/gamebuster19901/excite/modding/util/FileUtils.java similarity index 67% rename from src/main/java/com/gamebuster19901/excite/modding/FileUtils.java rename to src/main/java/com/gamebuster19901/excite/modding/util/FileUtils.java index 2b20c6b..92154e6 100644 --- a/src/main/java/com/gamebuster19901/excite/modding/FileUtils.java +++ b/src/main/java/com/gamebuster19901/excite/modding/util/FileUtils.java @@ -1,4 +1,4 @@ -package com.gamebuster19901.excite.modding; +package com.gamebuster19901.excite.modding.util; import java.io.File; import java.io.IOError; @@ -13,6 +13,7 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.LinkedHashSet; public class FileUtils { @@ -65,7 +66,9 @@ public static String readNullTerminatedString(ByteBuffer buffer, Charset charset } public static Path createTempFile() throws IOException { - return Files.createTempFile(TEMP, null, null); + Path f = Files.createTempFile(TEMP, null, null); + System.out.println("Created temporary file " + f); + return f; } public static Path createTempFile(String name) throws IOException { @@ -118,4 +121,48 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } } + public static LinkedHashSet getFilesRecursively(Path path) throws IOException { + LinkedHashSet paths = new LinkedHashSet<>(); + if (Files.exists(path)) { + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + paths.add(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (Files.isSymbolicLink(dir)) { + //skip symbolic links + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + }); + } else { + System.out.println("The specified path does not exist: " + path); + } + return paths; + } + + public static String getFileName(Path f) { + String fileName = f.getFileName().toString(); + int i = fileName.lastIndexOf('.'); + return (i == -1) ? fileName : fileName.substring(0, i); + } + + public static String getExtension(String fileName) { + int i = fileName.lastIndexOf('.'); + return i == -1 ? "" : fileName.substring(i + 1); + } + + public static boolean isDirectory(Path dir) { + return Files.isDirectory(dir) && !Files.isSymbolicLink(dir); + } + + public static boolean isDirectory(File dir) { + return isDirectory(dir.getAbsoluteFile().toPath()); + } + } diff --git a/src/main/java/com/gamebuster19901/excite/modding/util/SplitOutputStream.java b/src/main/java/com/gamebuster19901/excite/modding/util/SplitOutputStream.java new file mode 100644 index 0000000..97b322c --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/util/SplitOutputStream.java @@ -0,0 +1,73 @@ +package com.gamebuster19901.excite.modding.util; + +import java.io.IOException; +import java.io.OutputStream; + +public class SplitOutputStream extends OutputStream { + + private OutputStream[] outputs; + + public SplitOutputStream(OutputStream... consumers) { + this.outputs = consumers; + } + + public static SplitOutputStream splitSysOut(OutputStream... consumers) { + OutputStream original = System.out; + OutputStream[] outputs = new OutputStream[consumers.length + 1]; + outputs[0] = original; + + for(int i = 0; i < consumers.length; i++) { + outputs[i + 1] = consumers[i]; + } + + return new SplitOutputStream(outputs); + } + + public static SplitOutputStream splitErrOut(OutputStream... consumers) { + OutputStream original = System.err; + OutputStream[] outputs = new OutputStream[consumers.length + 1]; + outputs[0] = original; + + for(int i = 0; i < consumers.length; i++) { + outputs[i + 1] = consumers[i]; + } + + return new SplitOutputStream(outputs); + } + + @Override + public void write(int b) throws IOException { + for(OutputStream o : outputs) { + o.write(b); + } + } + + @Override + public void write(byte[] b) throws IOException { + for(OutputStream o : outputs) { + o.write(b); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for(OutputStream o : outputs) { + o.write(b, off, len); + } + } + + @Override + public void flush() throws IOException{ + for(OutputStream o : outputs) { + o.flush(); + } + } + + @Override + public void close() throws IOException { + for(OutputStream o : outputs) { + o.close(); + } + } + +} diff --git a/src/main/java/com/gamebuster19901/excite/modding/util/package-info.java b/src/main/java/com/gamebuster19901/excite/modding/util/package-info.java new file mode 100644 index 0000000..908c39e --- /dev/null +++ b/src/main/java/com/gamebuster19901/excite/modding/util/package-info.java @@ -0,0 +1 @@ +package com.gamebuster19901.excite.modding.util; \ No newline at end of file diff --git a/src/main/resources/kaitai/monster_res.ksy b/src/main/resources/kaitai/monster_res.ksy index d92ce62..b7b5813 100644 --- a/src/main/resources/kaitai/monster_res.ksy +++ b/src/main/resources/kaitai/monster_res.ksy @@ -59,7 +59,7 @@ types: seq: - id: compressed_data type: quicklz_rcmp - if: _root.header.compressed == 128 + if: _root.header.compressed == 128 or _root.header.compressed == 1152 - id: uncompressed_data size-eos: true if: _root.header.compressed == 0