From e22b37babecb01c4dcc839ad8f958cd3e0666aad Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Tue, 7 Jul 2015 18:45:48 +0200 Subject: [PATCH] Created ReplayAsset interface and AssetRepository class which can contain such ReplayAssets. ReplayAssets allow users to add custom Files (so far only images, might be extended by .obj files) to the Replay which will later be used for CustomImageObjects. Created GuiReplayManager to allow users to manage their assets. Created BoundingUtils class to provide a simple method to fit an image into given bounds. This implementation fixes https://trello.com/c/WTnicWkJ/ --- .../replaymod/assets/AssetRepository.java | 68 +++++++ .../replaymod/assets/ReplayAsset.java | 22 +++ .../replaymod/assets/ReplayImageAsset.java | 85 +++++++++ .../events/handlers/KeyInputHandler.java | 7 +- .../replaymod/gui/GuiAssetManager.java | 175 ++++++++++++++++++ .../gui/elements/GuiFileChooser.java | 76 ++++---- .../replaymod/registry/KeybindRegistry.java | 4 +- .../replaymod/registry/ResourceHelper.java | 11 +- .../replaymod/replay/ReplayHandler.java | 2 + .../replaymod/utils/BoundingUtils.java | 25 +++ .../replaymod/utils/ReplayFile.java | 31 ++++ .../replaymod/video/frame/FrameRenderer.java | 16 +- .../assets/replaymod/lang/en_US.lang | 8 +- .../resources/assets/replaymod/replay_gui.png | Bin 6833 -> 6852 bytes 14 files changed, 468 insertions(+), 62 deletions(-) create mode 100644 src/main/java/eu/crushedpixel/replaymod/assets/AssetRepository.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/assets/ReplayAsset.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/assets/ReplayImageAsset.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiAssetManager.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/utils/BoundingUtils.java diff --git a/src/main/java/eu/crushedpixel/replaymod/assets/AssetRepository.java b/src/main/java/eu/crushedpixel/replaymod/assets/AssetRepository.java new file mode 100644 index 00000000..9d4763d3 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/assets/AssetRepository.java @@ -0,0 +1,68 @@ +package eu.crushedpixel.replaymod.assets; + +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import eu.crushedpixel.replaymod.utils.ReplayFile; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.ArrayUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class AssetRepository { + + private List replayAssets; + + public AssetRepository() { + replayAssets = new ArrayList(); + } + + public ReplayAsset addAsset(String assetFileName, InputStream inputStream) throws IOException { + ReplayAsset asset = assetFromFileName(assetFileName); + + asset.loadFromStream(inputStream); + + replayAssets.add(asset); + + return asset; + } + + public void removeAsset(ReplayAsset asset) { + replayAssets.remove(asset); + } + + public void saveAssets() { + for(ReplayAsset asset : replayAssets) { + try { + String filepath = ReplayFile.ENTRY_ASSET_FOLDER + asset.getDisplayString() + "." + asset.getSavedFileExtension(); + + File toAdd = File.createTempFile(asset.getDisplayString(), asset.getSavedFileExtension()); + FileOutputStream fos = new FileOutputStream(toAdd); + asset.writeToStream(fos); + + ReplayMod.replayFileAppender.registerModifiedFile(toAdd, filepath, ReplayHandler.getReplayFile()); + } catch(IOException e) { + e.printStackTrace(); + } + } + + } + + public static ReplayAsset assetFromFileName(String filename) { + String baseName = FilenameUtils.getBaseName(filename); + String extension = FilenameUtils.getExtension(filename); + + if(ArrayUtils.contains(AssetFileUtils.fileExtensionsForAssetClass(ReplayImageAsset.class), extension)) { + return new ReplayImageAsset(baseName); + } + + throw new UnsupportedOperationException("Can't create ReplayAsset from File "+filename); + } + + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/assets/ReplayAsset.java b/src/main/java/eu/crushedpixel/replaymod/assets/ReplayAsset.java new file mode 100644 index 00000000..d4a71f48 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/assets/ReplayAsset.java @@ -0,0 +1,22 @@ +package eu.crushedpixel.replaymod.assets; + +import eu.crushedpixel.replaymod.holders.GuiEntryListEntry; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface ReplayAsset extends GuiEntryListEntry { + + String getSavedFileExtension(); + + void loadFromStream(InputStream inputStream) throws IOException; + void writeToStream(OutputStream outputStream) throws IOException; + + void drawToScreen(int x, int y, int maxWidth, int maxHeight); + + void setAssetName(String name); + + T getObject(); + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/assets/ReplayImageAsset.java b/src/main/java/eu/crushedpixel/replaymod/assets/ReplayImageAsset.java new file mode 100644 index 00000000..501a6f90 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/assets/ReplayImageAsset.java @@ -0,0 +1,85 @@ +package eu.crushedpixel.replaymod.assets; + +import eu.crushedpixel.replaymod.registry.ResourceHelper; +import eu.crushedpixel.replaymod.utils.BoundingUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.util.Dimension; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +public class ReplayImageAsset implements ReplayAsset { + + private final Minecraft mc = Minecraft.getMinecraft(); + + private BufferedImage object; + + private String name; + + public ReplayImageAsset(String name) { + this.name = name; + } + + @Override + public void setAssetName(String name) { + this.name = name; + } + + @Override + public String getDisplayString() { + return name; + } + + @Override + public String getSavedFileExtension() { + return "png"; + } + + @Override + public void loadFromStream(InputStream inputStream) throws IOException { + this.object = ImageIO.read(inputStream); + ResourceHelper.freeResource(previewResource); + } + + @Override + public void writeToStream(OutputStream outputStream) throws IOException { + ImageIO.write(object, "png", outputStream); + } + + @Override + public BufferedImage getObject() { + return object; + } + + private ResourceLocation previewResource = new ResourceLocation("/asset/"+ UUID.randomUUID().toString()); + private DynamicTexture dynamicTexture; + + @Override + public void drawToScreen(int x, int y, int maxWidth, int maxHeight) { + if(object == null) return; + if(!ResourceHelper.isRegistered(previewResource) || dynamicTexture == null) { + dynamicTexture = new DynamicTexture(object); + mc.getTextureManager().loadTexture(previewResource, dynamicTexture); + ResourceHelper.registerResource(previewResource); + } + + mc.renderEngine.bindTexture(previewResource); + + Dimension dimension = BoundingUtils.fitIntoBounds(new Dimension(object.getWidth(), object.getHeight()), new Dimension(maxWidth, maxHeight)); + + Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, object.getWidth(), object.getHeight(), dimension.getWidth(), dimension.getHeight(), object.getWidth(), object.getHeight()); + } + + @Override + protected void finalize() throws Throwable { + ResourceHelper.freeResource(previewResource); + super.finalize(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/events/handlers/KeyInputHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/KeyInputHandler.java index 5dcf511b..be3afd57 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/handlers/KeyInputHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/KeyInputHandler.java @@ -1,8 +1,9 @@ package eu.crushedpixel.replaymod.events.handlers; import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.assets.AssetRepository; import eu.crushedpixel.replaymod.entities.CameraEntity.MoveDirection; -import eu.crushedpixel.replaymod.gui.GuiAssetAdder; +import eu.crushedpixel.replaymod.gui.GuiAssetManager; import eu.crushedpixel.replaymod.gui.GuiKeyframeRepository; import eu.crushedpixel.replaymod.gui.GuiMouseInput; import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; @@ -202,8 +203,8 @@ public class KeyInputHandler { ReplayMod.replaySettings.setShowPathPreview(!ReplayMod.replaySettings.showPathPreview()); } - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ADD_ASSETS) && (kb.isPressed() || kb.getKeyCode() == keyCode)) { - mc.displayGuiScreen(new GuiAssetAdder()); + if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ASSET_MANAGER) && (kb.isPressed() || kb.getKeyCode() == keyCode)) { + mc.displayGuiScreen(new GuiAssetManager(new AssetRepository())); } } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiAssetManager.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiAssetManager.java new file mode 100644 index 00000000..6307463a --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiAssetManager.java @@ -0,0 +1,175 @@ +package eu.crushedpixel.replaymod.gui; + +import eu.crushedpixel.replaymod.assets.AssetFileUtils; +import eu.crushedpixel.replaymod.assets.AssetRepository; +import eu.crushedpixel.replaymod.assets.ReplayAsset; +import eu.crushedpixel.replaymod.gui.elements.*; +import eu.crushedpixel.replaymod.gui.elements.listeners.FileChooseListener; +import eu.crushedpixel.replaymod.gui.elements.listeners.SelectionListener; +import eu.crushedpixel.replaymod.utils.MouseUtils; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import org.lwjgl.opengl.GL11; +import org.lwjgl.util.Point; + +import java.awt.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public class GuiAssetManager extends GuiScreen { + + private String screenTitle; + + private boolean initialized = false; + private GuiEntryList assetGuiEntryList; + private GuiAdvancedButton removeButton; + private GuiAdvancedTextField assetNameInput; + private GuiFileChooser fileChooser, addButton; + + private ComposedElement inputElements; + + private ComposedElement composedElement; + + private AssetRepository assetRepository; + + private ReplayAsset currentAsset; + + public GuiAssetManager(AssetRepository assetRepository) { + this.assetRepository = assetRepository; + } + + @Override + public void initGui() { + if(!initialized) { + screenTitle = I18n.format("replaymod.gui.assets.title"); + + assetGuiEntryList = new GuiEntryList(fontRendererObj, 0, 0, 0, 0); + addButton = new GuiFileChooser(0, 0, 0, I18n.format("replaymod.gui.add"), null, AssetFileUtils.getAllAvailableExtensions()) { + @Override + protected void updateDisplayString() { + this.displayString = baseString; + } + }; + + addButton.addFileChooseListener(new FileChooseListener() { + @Override + public void onFileChosen(File file) { + try { + ReplayAsset newAsset = assetRepository.addAsset(file.getName(), new FileInputStream(file)); + assetGuiEntryList.addElement(newAsset); + } catch(Exception e) { + e.printStackTrace(); + } + } + }); + + removeButton = new GuiAdvancedButton(0, 0, 0, I18n.format("replaymod.gui.remove")) { + @Override + public void performAction() { + assetRepository.removeAsset(currentAsset); + assetGuiEntryList.removeElement(assetGuiEntryList.getSelectionIndex()); + } + }; + + assetNameInput = new GuiAdvancedTextField(fontRendererObj, 0, 0, 150, 20); + assetNameInput.hint = I18n.format("replaymod.gui.assets.namehint"); + + fileChooser = new GuiFileChooser(0, 0, 0, I18n.format("replaymod.gui.assets.changefile"), null, new String[0]) { + @Override + protected void updateDisplayString() { + this.displayString = baseString; + } + }; + fileChooser.addFileChooseListener(new FileChooseListener() { + @Override + public void onFileChosen(File file) { + if(currentAsset == null) return; + try { + currentAsset.loadFromStream(new FileInputStream(file)); + } catch(IOException e) { + e.printStackTrace(); + } + } + }); + + fileChooser.width = 150; + + inputElements = new ComposedElement(assetNameInput, fileChooser); + composedElement = new ComposedElement(assetGuiEntryList, addButton, removeButton, inputElements); + + inputElements.setEnabled(false); + + assetGuiEntryList.addSelectionListener(new SelectionListener() { + @Override + public void onSelectionChanged(int selectionIndex) { + currentAsset = assetGuiEntryList.getElement(selectionIndex); + inputElements.setEnabled(currentAsset != null); + + assetNameInput.setText(currentAsset != null ? currentAsset.getDisplayString() : ""); + fileChooser.setAllowedExtensions(currentAsset != null ? AssetFileUtils.fileExtensionsForAssetClass(currentAsset.getClass()) : new String[0]); + } + }); + } + + int visibleEntries = (int)Math.floor(((double)this.height-(45+20+15+20))/14); + + assetGuiEntryList.width = 150; + assetGuiEntryList.xPosition = width/2 - assetGuiEntryList.width - 5; + assetGuiEntryList.setVisibleElements(visibleEntries); + + assetGuiEntryList.yPosition = assetNameInput.yPosition = 45; + + addButton.xPosition = assetGuiEntryList.xPosition-1; + addButton.width = 77; + removeButton.width = 76; + removeButton.xPosition = addButton.xPosition+addButton.width; + + addButton.yPosition = removeButton.yPosition = assetGuiEntryList.yPosition+assetGuiEntryList.height+2; + + assetNameInput.xPosition = fileChooser.xPosition = this.width / 2 + 5; + fileChooser.yPosition = assetNameInput.yPosition + 20 + 5; + + initialized = true; + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.drawCenteredString(fontRendererObj, screenTitle, this.width / 2, 5, Color.WHITE.getRGB()); + + int leftBorder = 10; + int topBorder = 20; + + drawGradientRect(leftBorder, topBorder, width - leftBorder, this.height - 10, -1072689136, -804253680); + + composedElement.draw(mc, mouseX, mouseY); + + if(currentAsset != null) { + int y = fileChooser.yPosition + 20 + 5; + int height = (addButton.yPosition+addButton.height)-y; + + GL11.glColor4f(1, 1, 1, 1); + currentAsset.drawToScreen(fileChooser.xPosition, y, 150, height); + } + + super.drawScreen(mouseX, mouseY, partialTicks); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + composedElement.mouseClick(mc, mouseX, mouseY, mouseButton); + super.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + Point mousePos = MouseUtils.getMousePos(); + composedElement.buttonPressed(mc, mousePos.getX(), mousePos.getY(), typedChar, keyCode); + super.keyTyped(typedChar, keyCode); + } + + @Override + public void updateScreen() { + composedElement.tick(mc); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiFileChooser.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiFileChooser.java index 8a3d67df..5c876ff6 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiFileChooser.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiFileChooser.java @@ -2,7 +2,7 @@ package eu.crushedpixel.replaymod.gui.elements; import eu.crushedpixel.replaymod.gui.elements.listeners.FileChooseListener; import lombok.Getter; -import net.minecraft.client.Minecraft; +import lombok.Setter; import java.awt.*; import java.io.File; @@ -20,8 +20,9 @@ public class GuiFileChooser extends GuiAdvancedButton { updateDisplayString(); } - private String baseString; + protected String baseString; + @Setter private String[] allowedExtensions = null; private List listeners = new ArrayList(); @@ -45,49 +46,44 @@ public class GuiFileChooser extends GuiAdvancedButton { } @Override - public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) { - boolean success = super.mousePressed(mc, mouseX, mouseY); - if (success) { - new Thread(new Runnable() { - @Override - public void run() { - Frame frame = new Frame(); - FileDialog fileDialog = new FileDialog(frame); + public void performAction() { + new Thread(new Runnable() { + @Override + public void run() { + Frame frame = new Frame(); + FileDialog fileDialog = new FileDialog(frame); - fileDialog.setFilenameFilter(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - if(allowedExtensions == null) return true; - for(String extension : allowedExtensions) { - String[] split = name.split("\\."); - String ext = split[split.length-1]; - if(extension.equalsIgnoreCase(ext)) return true; - } - - return false; + fileDialog.setFilenameFilter(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + if(allowedExtensions == null) return true; + for(String extension : allowedExtensions) { + String[] split = name.split("\\."); + String ext = split[split.length-1]; + if(extension.equalsIgnoreCase(ext)) return true; } - }); - //frame.setVisible(true); - fileDialog.setVisible(true); - - String filename = fileDialog.getFile(); - String directory = fileDialog.getDirectory(); - if(filename != null) { - selectedFile = new File(directory, filename); - - updateDisplayString(); - - for(FileChooseListener listener : listeners) { - listener.onFileChosen(selectedFile); - } + return false; } + }); - frame.dispose(); + //frame.setVisible(true); + fileDialog.setVisible(true); + + String filename = fileDialog.getFile(); + String directory = fileDialog.getDirectory(); + if(filename != null) { + selectedFile = new File(directory, filename); + + updateDisplayString(); + + for(FileChooseListener listener : listeners) { + listener.onFileChosen(selectedFile); + } } - }, "replaymod-file-chooser").start(); - } - return success; - } + frame.dispose(); + } + }, "replaymod-file-chooser").start(); + } } diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java b/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java index ed1c6071..ef2d0bc8 100755 --- a/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java @@ -22,7 +22,7 @@ public class KeybindRegistry { public static final String KEY_PLAY_PAUSE = "replaymod.input.playpause"; public static final String KEY_ADD_MARKER = "replaymod.input.marker"; public static final String KEY_PATH_PREVIEW = "replaymod.input.pathpreview"; - public static final String KEY_ADD_ASSETS = "replaymod.input.addassets"; + public static final String KEY_ASSET_MANAGER = "replaymod.input.assetmanager"; private static Minecraft mc = Minecraft.getMinecraft(); public static void initialize() { @@ -40,7 +40,7 @@ public class KeybindRegistry { bindings.add(new KeyBinding(KEY_ROLL_COUNTERCLOCKWISE, Keyboard.KEY_J, "replaymod.title")); bindings.add(new KeyBinding(KEY_PLAY_PAUSE, Keyboard.KEY_P, "replaymod.title")); bindings.add(new KeyBinding(KEY_PATH_PREVIEW, Keyboard.KEY_H, "replaymod.title")); - bindings.add(new KeyBinding(KEY_ADD_ASSETS, Keyboard.KEY_G, "replaymod.title")); + bindings.add(new KeyBinding(KEY_ASSET_MANAGER, Keyboard.KEY_G, "replaymod.title")); mc.gameSettings.keyBindings = bindings.toArray(new KeyBinding[bindings.size()]); diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/ResourceHelper.java b/src/main/java/eu/crushedpixel/replaymod/registry/ResourceHelper.java index 7532d4fd..b0da9596 100755 --- a/src/main/java/eu/crushedpixel/replaymod/registry/ResourceHelper.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/ResourceHelper.java @@ -30,9 +30,14 @@ public class ResourceHelper { return openResources.contains(loc); } - public static void freeResource(ResourceLocation loc) { - Minecraft.getMinecraft().getTextureManager().deleteTexture(loc); - openResources.remove(loc); + public static void freeResource(final ResourceLocation loc) { + Minecraft.getMinecraft().addScheduledTask(new Runnable() { + @Override + public void run() { + Minecraft.getMinecraft().getTextureManager().deleteTexture(loc); + openResources.remove(loc); + } + }); } public static void freeAllResources() { diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java index a971f451..6daf301d 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -562,6 +562,7 @@ public class ReplayHandler { try { ReplayMod.overlay.resetUI(true); } catch(Exception e) { + e.printStackTrace(); // TODO: Fix exceptionsudo } @@ -584,6 +585,7 @@ public class ReplayHandler { t.printStackTrace(); } }; + INetHandlerPlayClient pc = new NetHandlerPlayClient(mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player")); networkManager.setNetHandler(pc); diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/BoundingUtils.java b/src/main/java/eu/crushedpixel/replaymod/utils/BoundingUtils.java new file mode 100644 index 00000000..6358007f --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/utils/BoundingUtils.java @@ -0,0 +1,25 @@ +package eu.crushedpixel.replaymod.utils; + +import org.lwjgl.util.Dimension; + +public class BoundingUtils { + + public static Dimension fitIntoBounds(Dimension toFit, Dimension bounds) { + int width = toFit.getWidth(); + int height = toFit.getHeight(); + + float w = (float)width/bounds.getWidth(); + float h = (float)height/bounds.getHeight(); + + if(w > h) { + height = (int)(height/w); + width = (int)(width/w); + } else { + height = (int)(height/h); + width = (int)(width/h); + } + + return new Dimension(width, height); + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java index 7236a860..3e15e0a3 100644 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java @@ -4,6 +4,7 @@ import com.google.common.base.Supplier; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import eu.crushedpixel.replaymod.assets.AssetRepository; import eu.crushedpixel.replaymod.holders.KeyframeSet; import eu.crushedpixel.replaymod.holders.MarkerKeyframe; import eu.crushedpixel.replaymod.holders.PlayerVisibility; @@ -14,6 +15,7 @@ import org.apache.commons.compress.archivers.zip.ZipFile; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; +import java.util.Enumeration; import java.util.HashMap; import java.util.Map; @@ -32,6 +34,7 @@ public class ReplayFile extends ZipFile { public static final String ENTRY_VISIBILITY_OLD = "visibility"; public static final String ENTRY_VISIBILITY = "visibility" + JSON_FILE_EXTENSION; public static final String ENTRY_MARKERS = "markers" + JSON_FILE_EXTENSION; + public static final String ENTRY_ASSET_FOLDER = "asset/"; private final File file; @@ -220,4 +223,32 @@ public class ReplayFile extends ZipFile { } }; } + + public Supplier assetRepository() { + return new Supplier() { + @Override + @SuppressWarnings("unchecked") + public AssetRepository get() { + AssetRepository assetRepository = new AssetRepository(); + + Enumeration entries = getEntries(); + + while(entries.hasMoreElements()) { + + try { + ZipArchiveEntry entry = entries.nextElement(); + + if(entry.getName().startsWith(ENTRY_ASSET_FOLDER)) { + String name = entry.getName().substring(ENTRY_ASSET_FOLDER.length()); + assetRepository.addAsset(name, getInputStream(entry)); + } + } catch(IOException e) { + e.printStackTrace(); + } + } + + return assetRepository; + } + }; + } } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java index 2d9c0faa..9b7bc955 100644 --- a/src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java @@ -1,6 +1,7 @@ package eu.crushedpixel.replaymod.video.frame; import eu.crushedpixel.replaymod.settings.RenderOptions; +import eu.crushedpixel.replaymod.utils.BoundingUtils; import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; @@ -9,6 +10,7 @@ import net.minecraft.client.renderer.texture.TextureUtil; import net.minecraft.util.ResourceLocation; import net.minecraft.util.Timer; import org.lwjgl.BufferUtils; +import org.lwjgl.util.Dimension; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; @@ -120,18 +122,12 @@ public abstract class FrameRenderer { * @param height Height of the box */ public void renderPreview(int x, int y, int width, int height) { - int actualWidth = width; - int actualHeight = height; - if (width / height > getVideoWidth() / getVideoHeight()) { - actualWidth = height * getVideoWidth() / getVideoHeight(); - } else { - actualHeight = width * getVideoHeight() / getVideoWidth(); - } + Dimension dimension = BoundingUtils.fitIntoBounds(new Dimension(x, y), new Dimension(width, height)); - x += (width - actualWidth) / 2; - y += (height - actualHeight) / 2; + x += (width - dimension.getWidth()) / 2; + y += (height - dimension.getHeight()) / 2; - drawPreviewTexture(x, y, actualWidth, actualHeight); + drawPreviewTexture(x, y, dimension.getWidth(), dimension.getHeight()); } public static void renderNoPreview(int x, int y, int width, int height) { diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 3bbe7e2c..1d4ced1c 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -257,7 +257,7 @@ replaymod.input.resettilt=Reset Camera Tilt replaymod.input.playpause=Play/Pause Replay replaymod.input.marker=Add Event Marker replaymod.input.pathpreview=Toggle Path Preview -replaymod.input.addassets=Add Assets +replaymod.input.assetmanager=Open Asset Manager #Keyframe Presets GUI replaymod.gui.keyframerepository.title=Keyframe Repository @@ -357,10 +357,10 @@ replaymod.gui.ingame.unnamedmarker=Unnamed Event Marker replaymod.gui.clearcallback.title=Clear all Keyframes? replaymod.gui.clearcallback.message=This Callback can be disabled in the Replay Settings. -#Asset Adder Gui +#Asset Manager Gui replaymod.gui.assets.title=Asset Manager -replaymod.gui.assets.filechooser=Image File +replaymod.gui.assets.filechooser=Asset File replaymod.gui.assets.defaultname=New Asset replaymod.gui.assets.emptylist=No Assets available replaymod.gui.assets.namehint=Asset Name -replaymod.gui.assets.backvisible=Visible from behind \ No newline at end of file +replaymod.gui.assets.changefile=Change Asset File \ No newline at end of file diff --git a/src/main/resources/assets/replaymod/replay_gui.png b/src/main/resources/assets/replaymod/replay_gui.png index 64648ab26315dc3b8c838d0b278491440138ecc1..04dbf78be398d56ae03012fa0d201a604a07e8f7 100755 GIT binary patch delta 6424 zcmYLOcQoA3*WO)=U4+$3^e%cAy+#R9A|lZVf<$LU@UcRa5F5P{ED_KGk4B??#w**&P<*BH+l38`Ag7ME@~@J1%W_tUxNn$AP^<} ze+3N6$z!?dBo8z+(IcNHCu4%LOxTi=fk15LZyW>wum@P*Kg5^8ne}7q#QL>(q|?MC zvoowrx!%f-oh`f0)GYQ6YAgOaJjnKtee#2XrU{Jbb74zJnA5s2aA{4jzktbBUp5=QTN(xLtBUGx)K}-`oh^ z!@}Z1u%_`wKn+r$(FlaLr>wb13_mS(TbLtj3ECpNQ-Hrifz1^Lc`5bCKJIbisBzCvqw{0uOW+m8RH?ThisOKV0-(SD zX!TT*7bg7Cc$`d)**@YagF7*(MW=RYt1E``?m2e7rnM%$V{iU+-*R95=vTc2Aipqe z@ZNMOwvs(CTlHy$4Vf$fQwZ>merhQkcse1hK^0L)M83s9~ z@>%|Z&_W7AyFPGzdBm_l{8hsJ8ktsBVNSJ?w!C6K#Fv!dyVmaRxu2rTZ4%J^VYTI~ z+b{9gRh0CCT?O>H0M-7pAE`IG ze*b>htxKr8iGYs$8cFcIvZj2f0$^dpZwLW}EW|KB^zdA%OX_$qR0&6ZF%#NAK4Z}I zI^bu*Dhg{MxD$#KGOI0FZ<&nvlfSx0oI0C=E$cviI!?L)#Q0>hRH6z!$u2pntL4f1h)(>)|{i02GXu7TB|QF~M~ zg#%37+T%MqF3T^wketWgnpXQ8>ED^;rqSuYW|!V+%3pU+ymvs>0ND>!qaq^_va=x^ zYn*nMcUs>ug~fMrcMn^@dW4nAha$=k#%Zcn!9DVdbpM@iGsa6s>`{bsftl%|&eFNb0_P~UcdgZsrei-GusIlum-Ec%NnlNq38%w0 zmwyloh=toaI4JV0Ro1qzoeu~<-}(&3nJ#~SEBJSA@fyi)$UWK{e#i7{9;0cRD&aA7 zXwEn`OSgsAR8Dec_6K>QX^zEtUA8We>2_JF+`8uvzjJ;5XB9>qw@Ec8r-w>hT)L({ zCRezAw#ZexU)Q1IBY8Qi5iu=lEq)|)#cw^G&%UI#kh6oIr;d{g%6d7R=9qt*X%^l~}$CqRGFPq14_J=}@7aibFZ7d-hBn-Wp;WYR<^Ge^}?CL=) z`St>ns|<3aYgw8Ta()ACpg2Qml!O$20?TK+P0Y&u#9`jqu)Y564F}>9HHM+^(}v39 z--}uPg!_{LVUem-RVScoVAzAcfJxb7Dt0pdJeEyxC>CvQ#A9;TCrgQ%UKrEm%QEpN zQ)z(Ri|Y}{-O%cI*bI6v7-vfa==VG+XM0#qa*_ro zD`xfI$5c*a4jd&VC(u3aUHCdnr??jfpHi15%_0UF zz58X*`7d5iNTJ%v`J)l?9oQadgVUx$K1?(BIkOBWF8c@Vr>s_1H^`WkyT;#@lb^Ux zAmeL9ZcrN2`#TN5QyzLH8&cmz(@fe4^0>tiXXBz*+fIqja_>(-B}!i1&=Z+-lWc5% z|8T6IEQ-tf2jp{CZGXhMlzs)&&1S+PDqb8_4Wr+mZ4tmSeqO(Mig#|aAIw@6?6Wg8 z9j|GbyZGrJjlqPO&c(v>1nJ+^M-~`!d)QFe|D)_U<2eDGsMp_8i|>m>Z|%x%)|XVj zgd2~H=DzhG^oU`Jg(oidz1j)T(HZclQ$3PK1q+%rHyIasdkdQ7;%RLBB6@SSfU4f? z?$&E-S@Dp``d%S3MGAwIM^`9JizM2Vn5wfh^WhER=63~$+Q)uELru0fQgiL(wJe>& zKwcV-TYzZftCkUxnt*d{m{gDN+0hQ0a3EbDho;|^8>S-MiTlEd%_c9bbll95LU{`g zt?;*GW8q)RE22vy_DgnqlEX^fw7hX=jV)NHg4*Y?GfNGF} zpc%|byP-@O^&o@CkPgOUoF6G@?3gjNe-;E8v1G_1K{cOB3kxI#K@NF=fDY)96e_r< zUxHEbX})F>d{iEcu}rt{8ySbsygKV|BOS_CYb!V*V}7@?I3lN)!rz_R7-U>z@eXi= zq`&7y&CZ0Uz^KSX(xRI0;O&=vHW{(8aH=Sf`(ObnI#YG*-`4oVgeZ-eaRPiP@6YC- ztwRZN%TCdfm{dtmT%<`BslTT}dGRDPbsMRtgbL2^mSbD!BNn5V^0sXclAy$4C?#PT@W6E8#1fbFNq1(CZuT8 zmpeu676IhN($u-Bj3y6s)2W3Q5lp81)pRfOkFzQ{w+D|vWf zp_|VxYT^X0PQsdyPu|D!^nh3zE(J%lUzX;%u=VvediMhDoS!|)Vd@t^o_}R}fXzZ} z(wx!-!5(}=NTizsIb%+rqpt411pA|;;O0wbjGuLApETI2cL-xEQIfT8Z&F4{0E}-L zasN~L;RV40Y-{Y8Q>GOBH;XonDI?;|w}vdN64TKc$>2CKzx|T8ON!+c{K+CsY#i;l z*irqe$W!$HjSpFV%~bW7~nTrH1$-UisaN(FW(7~_AUNc#6PG6aj7 zn~!r>ny$Q<8&z$UDz)YD~+)5yG6MNv|y~g}NU} zYW-geo8Z3TyvZr99*S*#)l$FO51~K$g$!ro3ZE!Ys5dkA*#;La`)(-s*+Q>_Vpk3+pwLDN2>PPnS5sfzb<=wL2XRP|SSwe^J%W@3L6uQ$CQfbE_ErqZ=?q8po<6r>581+}V*8FN<` z82dL_tu1~KyS~|9CHh|4^|3FQr^B-M#R%RK3|GQ_16m^0Pa4;PaU4kQ z1YW3?lr*7c1g%-oNF07T^@4eamPK_wfBFn8mk{R!Kj+X0s=4Dy{V->Jf#%tlkhDx0`{c5;N?? zf;lY}Du}~$O9VFozsx682oCO|z!pGTh!O&QKz4(~AZN9dqk=zb21zc8`RQL}W#iz0 zd)hqvQi`fP6#2mdTK?MM{{L79KX8{B5mU7@+o>{Zylg%#H|#$>>AfSYwFHywrGsiI zjUivE>gKqb8AX|s@l5C2v%N$QOpnM33m&#YN>?9Uw)+SJeUuWp>>&PY-&tOOc!ysP>>(V~i;U<&OtTurklpjGAGeTcYQ= z{3b<~HBQfKH@JxJd|{Rifm^8wcDx9Y+pWbAaEQovOr-0)Cb7g@98` z`VeDjhf5Q4fJcH_BRsFRLDgl?gDsaT5*G&*&B$5fOZ6YUTBx#`eGGF5>mMksS?e%^mGRor<=l0Bc~ zS^WVwmSil&Nt?Z8-^szeNDKs7l`MXy3oA8DEJB0;85DeHw#P;b`fcC2$Tr>kX8CI| zZQ8jq6dxw$Ilhc+l;wJRBI$Iv4M9vGUaw8QM|=OOhw4sA#-9I`N+*p>x%hAo$hGYoWx9VHjuvNGaGUDtdfi`#y%Frjn|5l>rHUtCDI6FwBE}I!M;+*N~O!b zT|AfooQsRlXy1iAIto7u-&;cq3>J{wc+E`uQo1-6d84AiOB`_V$XsYB1owsER;>>A zThP0%>-W)SXtec4T~0(s_);J7(>a(sT?*I!td~_wlk!prJp0G6%$~tfmbP)v4SvPU zl=Vi!r77LU#vDG`46F0+^OIt9zZqZ#tBAlST;lsKpZkq}xk$Ab2=kd`A}r--tIgux z_`@>@J+TZR-L}%GbMsOEl(r-|IxtC^$Xl37!2ku9e2>n9=j+B90U~cC_s!f`5v1eJ zSvLZ2cJ@ehdMfJXV9*s+u5e6B`d+#*J^}|NON?T8TlKn6I4&Dihi*ih98QG804Ld! z#e-(?m(jXnmfo&ah`L@lnc-Sce(3|5I1QQ)^U|nnVfRtb<^rXKhJyUtBUE)}>3?Ud zh({;47ZDIzJ~#MK1UZe8Qhz4*^Of{HtTjDSKNtmv`Yv?Egs-19IYYRfep+QJX&G^X z;C|USLXd{wC&t_^RJhk1U#IE;cfVKqkApD?Mezp@X{%w7JlUDO^cCMj_ZlBe`7iC( zD|azg8$k*RSx&zX0%c0rK|r41Ui+|x^!xUm$-vp7Oavh5NkJ4aKfM_m zMeJy^r}4;%M3M#7j=1HSoD@kGauhCK}mV!VF+Iw%s^zL$SAQg~tc z@PF5hq8{jPj5Ug#{v8ssvD_64ALVRMf(N>swswU5ijHoYN47|#X1$r`&o_~UPuA)6 zG}%eO56;otF&FGsgg`e|KdxXP zlScSO*?;H8blK>jmZ`PGBJDLrdz+f?6s6H=!&JAiB#l7l!z-F65ClJp<$j_rM9^sq z&jgJo!)HQNZn3+!TE%YVc-2T7RmYHlDsMwAL?VU1c|Z`P6}zK6T&8&{^=F!cIlwDHNBNfwcfZe`1$CmJuFmG*KRNJ%_EQL}JZB2rI30$>s%}iy^ z@Nv0|N&##`K3W$lxyD6-x{UgKdlJzg?i+5CVpnI1^=%wQzEjK*%a6+g{o})L724l) zs1}`Sgkw^^8G15|9Qig41=UwZx0+mqEP@}B=DeLDdt2-*JZAX?~M` zFu!9oFYG~o;fLkMEooG{V4B#3GZu!?ut;yTQ*ribuTJE1Kg?##Hv-bPwrYb(z8O+f z0G1N?F4n)d?5GyQj@e!5Yz_C?e4g-IZ=w^PTt#;-bz*W-Z;7~N?thR3>!mXJKRgNU z_B`A?>0R;wE$hmY7M;F-4~G0sZyV@Rh173nzLPa?*xp+fa~N*WTh+t*BP$BvwxbK8 z`NdlAhbVVhahh7JT6wPDr5TQ&5UTRhfiwhFDoYx|H>bSgW-bTEgVNC;ppA8O5t?Wg zZ63Yw_;DUCL0dTGlu*S_K3{me^vd!^sM=WdFmTQy%NgxHC|BFDggv3!&1S^0YW`$Z5>@sF?yy>j^wZVNEcY-M zMYA|KAu@M57+Scn!r&l5X7MLK?JsOI2+H}r91yh3__*LbVGlP}hR(4{u!6$9Y_$2Z zSRN}!WrrDETV%0@1)i$sYcs!llV$g?P5@4-pWjO9t0Q{8L}#u?cPQe?i%yV5*S8Vn z_6V<3E!KA=it|>fe_3y^UcL78{74n@VW05v@c8j1g$bmfpy0dFIFTyJCSe(}crBH2 zG@eJzjO~)h8~%A#BI11A;Xa*_sHi9l71MHm#fVvTzr{| f=jXR>9|0gW@2t1Zn_sWKIY5SbcXVsC9i#pan1)5u delta 6419 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