From f925d56ca2f094b5d4e41700726b3f4376d4e576 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 3 Oct 2015 17:36:03 +0200 Subject: [PATCH] Split mod into core, recording, replay, render[todo], paths[todo] and extras[wip] modules Move everything to com.replaymod package Add KeyBindingRegistry and SettingsRegistry Recreate settings GUI with new GUI API and dynamically from SettingsRegistry Use ReplayFile from ReplayStudio ReplayHandler is now object oriented Add GuiOverlay, GuiSlider and GuiTexturedButton to GUI API Rewrite both overlays to use new GUI API Fix size capping in vertical and horizontal layout Allow CustomLayouts to have parents Fix tooltip rendering when close to screen border Allow changing of columns in GridLayout --- build.gradle | 2 +- libs/replaystudio-0.0.1-SNAPSHOT-all.jar | Bin 6321000 -> 6327195 bytes .../replaymod/core/KeyBindingRegistry.java | 60 + .../replaymod/core}/ReplayMod.java | 149 +- .../com/replaymod/core/SettingsRegistry.java | 133 ++ .../replaymod/core/gui/GuiReplaySettings.java | 75 + src/main/java/com/replaymod/extras/Extra.java | 7 + .../com/replaymod/extras/HotkeyButtons.java | 101 ++ .../com/replaymod/extras/ReplayModExtras.java | 44 + .../recording/ReplayModRecording.java | 47 + .../java/com/replaymod/recording/Setting.java | 17 + .../recording/gui}/GuiRecordingOverlay.java | 19 +- .../handler}/ConnectionEventHandler.java | 105 +- .../handler/RecordingEventHandler.java} | 107 +- .../mixin/MixinNetHandlerPlayClient.java | 23 +- .../recording/mixin/MixinRenderGlobal.java | 32 + .../recording/packet/DataListener.java | 182 +++ .../recording/packet}/PacketListener.java | 26 +- .../recording/packet}/PacketSerializer.java | 2 +- .../replaymod/replay}/CameraEntity.java | 32 +- .../com/replaymod/replay/ReplayHandler.java | 349 ++++ .../com/replaymod/replay/ReplayModReplay.java | 58 + .../replaymod/replay/ReplaySender.java | 57 +- .../java/com/replaymod/replay/Setting.java | 17 + .../replay/events/ReplayCloseEvent.java | 24 + .../replay/events/ReplayOpenEvent.java | 24 + .../gui/overlay}/GuiMarkerTimeline.java | 101 +- .../replay/gui/overlay/GuiReplayOverlay.java | 140 ++ .../replay/gui/screen}/GuiRenameReplay.java | 5 +- .../replay/gui/screen}/GuiReplayViewer.java | 39 +- .../replay/gui/screen}/ReplayList.java | 2 +- .../replaymod/replay/handler/GuiHandler.java | 111 ++ .../gui/container/AbstractGuiOverlay.java | 239 +++ .../gui/container/AbstractGuiScreen.java | 14 +- .../minecraft/gui/container/GuiOverlay.java | 30 + .../gui/element/AbstractGuiElement.java | 3 + .../gui/element/AbstractGuiSlider.java | 189 +++ .../element/AbstractGuiTexturedButton.java | 218 +++ .../gui/element/AbstractGuiToggleButton.java | 2 +- .../minecraft/gui/element/GuiSlider.java | 39 + .../gui/element/GuiTexturedButton.java | 39 + .../minecraft/gui/element/IGuiSlider.java | 36 + .../gui/element/IGuiTexturedButton.java | 50 + .../element/advanced/AbstractGuiTimeline.java | 253 +++ .../gui/element/advanced/GuiTimeline.java | 39 + .../gui/element/advanced/IGuiTimeline.java | 88 + .../minecraft/gui/layout/CustomLayout.java | 23 +- .../minecraft/gui/layout/GridLayout.java | 7 +- .../gui/layout/HorizontalLayout.java | 4 +- .../minecraft/gui/layout/VerticalLayout.java | 2 + .../crushedpixel/replaymod/api/ApiClient.java | 2 +- .../replaymod/api/GsonApiClient.java | 52 +- .../replaymod/api/SimpleApiClient.java | 246 +-- .../replaymod/api/replay/FileUploader.java | 2 +- .../api/replay/ReplayModApiMethods.java | 48 +- .../api/replay/holders/FileInfo.java | 2 +- .../pagination/DownloadedFilePagination.java | 2 +- .../pagination/FavoritedFilePagination.java | 2 +- .../replay/pagination/SearchPagination.java | 2 +- .../replaymod/assets/AssetRepository.java | 63 +- .../replaymod/assets/CustomImageObject.java | 45 +- .../replaymod/assets/ReplayImageAsset.java | 12 +- .../replaymod/chat/ChatMessageHandler.java | 2 +- .../replaymod/coremod/LoadingPlugin.java | 1 + .../replaymod/events/ReplayExitEvent.java | 6 - .../handlers/CrosshairRenderHandler.java | 22 +- .../events/handlers/GuiEventHandler.java | 54 +- .../events/handlers/MinecraftTicker.java | 5 +- .../events/handlers/MouseInputHandler.java | 86 +- .../handlers/TickAndRenderListener.java | 135 +- .../handlers/keyboard/KeyInputHandler.java | 394 +++-- .../replaymod/gui/GuiAssetManager.java | 10 +- .../replaymod/gui/GuiEditKeyframe.java | 625 ++++---- .../replaymod/gui/GuiKeyframeRepository.java | 78 +- .../replaymod/gui/GuiMouseInput.java | 53 - .../replaymod/gui/GuiObjectManager.java | 1411 ++++++++--------- .../replaymod/gui/GuiPlayerOverview.java | 645 ++++---- .../replaymod/gui/GuiRenderSettings.java | 9 +- .../replaymod/gui/GuiReplaySaving.java | 2 +- .../replaymod/gui/GuiReplaySettings.java | 55 +- .../replaymod/gui/GuiReplaySpeedSlider.java | 5 +- .../gui/elements/GuiAdvancedButton.java | 2 +- .../gui/elements/GuiAdvancedCheckBox.java | 2 +- .../gui/elements/GuiArrowButton.java | 5 +- .../replaymod/gui/elements/GuiDropdown.java | 2 +- .../replaymod/gui/elements/GuiScrollbar.java | 8 +- .../gui/elements/GuiSettingsOnOffButton.java | 34 +- .../timelines/GuiKeyframeTimeline.java | 462 +++--- .../gui/elements/timelines/GuiTimeline.java | 10 +- .../replaymod/gui/online/GuiLoginPrompt.java | 2 +- .../replaymod/gui/online/GuiRegister.java | 2 +- .../replaymod/gui/online/GuiReplayCenter.java | 6 +- .../gui/online/GuiReplayDownloading.java | 6 +- .../gui/online/GuiReplayInstanceChooser.java | 10 +- .../replaymod/gui/online/GuiUploadFile.java | 43 +- .../gui/overlay/GuiReplayOverlay.java | 657 -------- .../replayeditor/GuiReplayEditingProcess.java | 2 +- .../gui/replayeditor/GuiReplayEditor.java | 2 +- .../replaymod/holders/Marker.java | 33 - .../replaymod/holders/PlayerVisibility.java | 12 - .../AdvancedPositionKeyframeList.java | 25 +- .../SpectatorDataInterpolation.java | 53 +- .../localization/LocalizedResourcePack.java | 2 +- .../replaymod/mixin/MixinEntityRenderer.java | 53 +- .../replaymod/mixin/MixinGuiSpectator.java | 9 +- .../replaymod/mixin/MixinMinecraft.java | 4 +- .../mixin/MixinPlayerControllerMP.java | 6 +- .../replaymod/mixin/MixinRenderArrow.java | 12 +- .../replaymod/mixin/MixinRenderGlobal.java | 32 - .../mixin/MixinRendererLivingEntity.java | 18 +- .../replaymod/mixin/MixinViewFrustum.java | 8 +- .../online/urischeme/LinuxUriScheme.java | 2 +- .../online/urischeme/WindowsUriScheme.java | 2 +- .../replaymod/recording/DataListener.java | 197 --- .../replaymod/recording/ReplayMetaData.java | 24 - .../registry/DownloadedFileHandler.java | 5 +- .../registry/FavoritedFileHandler.java | 2 +- .../replaymod/registry/KeybindRegistry.java | 1 - .../replaymod/registry/LightingHandler.java | 2 +- .../replaymod/registry/PlayerHandler.java | 71 - .../replaymod/registry/RatedFileHandler.java | 2 +- .../registry/ReplayFileAppender.java | 4 +- .../renderer/CustomObjectRenderer.java | 184 +-- .../renderer/InvisibilityRender.java | 11 +- .../renderer/PathPreviewRenderer.java | 166 +- .../replaymod/replay/ReplayHandler.java | 1109 ++++++------- .../replaymod/replay/ReplayProcess.java | 318 ---- .../replaymod/settings/ReplaySettings.java | 61 +- .../studio/ConnectMetadataFilter.java | 2 +- .../studio/StudioImplementation.java | 6 +- .../replaymod/timer/EnchantmentTimer.java | 27 +- .../replaymod/utils/CameraPathValidator.java | 2 +- .../replaymod/utils/ReplayFile.java | 269 ---- .../replaymod/utils/ReplayFileIO.java | 81 +- .../replaymod/video/ReplayScreenshot.java | 16 +- .../replaymod/video/VideoRenderer.java | 253 ++- src/main/resources/META-INF/replaymod_at.cfg | 3 + src/main/resources/assets/guiapi/gui.png | Bin 0 -> 4056 bytes src/main/resources/mcmod.info | 80 +- .../resources/mixins.recording.replaymod.json | 11 + src/main/resources/mixins.replaymod.json | 2 - 141 files changed, 6294 insertions(+), 5582 deletions(-) create mode 100644 src/main/java/com/replaymod/core/KeyBindingRegistry.java rename src/main/java/{eu/crushedpixel/replaymod => com/replaymod/core}/ReplayMod.java (82%) create mode 100644 src/main/java/com/replaymod/core/SettingsRegistry.java create mode 100644 src/main/java/com/replaymod/core/gui/GuiReplaySettings.java create mode 100644 src/main/java/com/replaymod/extras/Extra.java create mode 100644 src/main/java/com/replaymod/extras/HotkeyButtons.java create mode 100644 src/main/java/com/replaymod/extras/ReplayModExtras.java create mode 100644 src/main/java/com/replaymod/recording/ReplayModRecording.java create mode 100644 src/main/java/com/replaymod/recording/Setting.java rename src/main/java/{eu/crushedpixel/replaymod/gui/overlay => com/replaymod/recording/gui}/GuiRecordingOverlay.java (74%) rename src/main/java/{eu/crushedpixel/replaymod/recording => com/replaymod/recording/handler}/ConnectionEventHandler.java (52%) rename src/main/java/{eu/crushedpixel/replaymod/events/handlers/RecordingHandler.java => com/replaymod/recording/handler/RecordingEventHandler.java} (81%) rename src/main/java/{eu/crushedpixel/replaymod => com/replaymod/recording}/mixin/MixinNetHandlerPlayClient.java (72%) create mode 100644 src/main/java/com/replaymod/recording/mixin/MixinRenderGlobal.java create mode 100755 src/main/java/com/replaymod/recording/packet/DataListener.java rename src/main/java/{eu/crushedpixel/replaymod/recording => com/replaymod/recording/packet}/PacketListener.java (92%) rename src/main/java/{eu/crushedpixel/replaymod/recording => com/replaymod/recording/packet}/PacketSerializer.java (96%) rename src/main/java/{eu/crushedpixel/replaymod/entities => com/replaymod/replay}/CameraEntity.java (91%) create mode 100755 src/main/java/com/replaymod/replay/ReplayHandler.java create mode 100644 src/main/java/com/replaymod/replay/ReplayModReplay.java rename src/main/java/{eu/crushedpixel => com}/replaymod/replay/ReplaySender.java (95%) create mode 100644 src/main/java/com/replaymod/replay/Setting.java create mode 100644 src/main/java/com/replaymod/replay/events/ReplayCloseEvent.java create mode 100644 src/main/java/com/replaymod/replay/events/ReplayOpenEvent.java rename src/main/java/{eu/crushedpixel/replaymod/gui/elements/timelines => com/replaymod/replay/gui/overlay}/GuiMarkerTimeline.java (56%) create mode 100644 src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java rename src/main/java/{eu/crushedpixel/replaymod/gui/replayviewer => com/replaymod/replay/gui/screen}/GuiRenameReplay.java (94%) rename src/main/java/{eu/crushedpixel/replaymod/gui/replayviewer => com/replaymod/replay/gui/screen}/GuiReplayViewer.java (91%) rename src/main/java/{eu/crushedpixel/replaymod/gui/replayviewer => com/replaymod/replay/gui/screen}/ReplayList.java (94%) create mode 100644 src/main/java/com/replaymod/replay/handler/GuiHandler.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/container/AbstractGuiOverlay.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/container/GuiOverlay.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiSlider.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiTexturedButton.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/GuiSlider.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/GuiTexturedButton.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/IGuiSlider.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/IGuiTexturedButton.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/advanced/AbstractGuiTimeline.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/advanced/GuiTimeline.java create mode 100644 src/main/java/de/johni0702/minecraft/gui/element/advanced/IGuiTimeline.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/events/ReplayExitEvent.java delete mode 100755 src/main/java/eu/crushedpixel/replaymod/gui/GuiMouseInput.java delete mode 100755 src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/Marker.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/PlayerVisibility.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/mixin/MixinRenderGlobal.java delete mode 100755 src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java delete mode 100755 src/main/java/eu/crushedpixel/replaymod/recording/ReplayMetaData.java delete mode 100755 src/main/java/eu/crushedpixel/replaymod/registry/PlayerHandler.java delete mode 100755 src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java create mode 100644 src/main/resources/assets/guiapi/gui.png create mode 100644 src/main/resources/mixins.recording.replaymod.json diff --git a/build.gradle b/build.gradle index 6182fa4e..df206de6 100755 --- a/build.gradle +++ b/build.gradle @@ -119,7 +119,7 @@ jar { manifest { attributes 'TweakClass': 'org.spongepowered.asm.launch.MixinTweaker', 'TweakOrder': '0', - 'MixinConfigs': 'mixins.replaymod.json', + 'MixinConfigs': 'mixins.replaymod.json,mixins.recording.replaymod.json', 'FMLCorePlugin': 'eu.crushedpixel.replaymod.coremod.LoadingPlugin', 'FMLAT': 'replaymod_at.cfg' } diff --git a/libs/replaystudio-0.0.1-SNAPSHOT-all.jar b/libs/replaystudio-0.0.1-SNAPSHOT-all.jar index d08f9102932884b15ebf382d906fe6402766b406..9d462c120f34a535aabf1d032697ca9f1114ee57 100644 GIT binary patch delta 248215 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 z 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 diff --git a/src/main/java/com/replaymod/core/KeyBindingRegistry.java b/src/main/java/com/replaymod/core/KeyBindingRegistry.java new file mode 100644 index 00000000..24a3807b --- /dev/null +++ b/src/main/java/com/replaymod/core/KeyBindingRegistry.java @@ -0,0 +1,60 @@ +package com.replaymod.core; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.ReportedException; +import net.minecraftforge.fml.client.registry.ClientRegistry; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.InputEvent; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; + +public class KeyBindingRegistry { + private Map keyBindings = new HashMap(); + private Multimap keyBindingHandlers = ArrayListMultimap.create(); + + public void registerKeyBinding(String name, int keyCode, Runnable whenPressed) { + KeyBinding keyBinding = keyBindings.get(name); + if (keyBinding == null) { + keyBinding = new KeyBinding(name, keyCode, "replaymod.title"); + keyBindings.put(name, keyBinding); + ClientRegistry.registerKeyBinding(keyBinding); + } + keyBindingHandlers.put(keyBinding, whenPressed); + } + + public Map getKeyBindings() { + return Collections.unmodifiableMap(keyBindings); + } + + @SubscribeEvent + public void onKeyInput(InputEvent.KeyInputEvent event) { + for (Map.Entry> entry : keyBindingHandlers.asMap().entrySet()) { + while (entry.getKey().isPressed()) { + for (final Runnable runnable : entry.getValue()) { + try { + runnable.run(); + } catch (Throwable cause) { + CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Key Binding"); + CrashReportCategory category = crashReport.makeCategory("Key Binding"); + category.addCrashSection("Key Binding", entry.getKey()); + category.addCrashSectionCallable("Handler", new Callable() { + @Override + public Object call() throws Exception { + return runnable; + } + }); + throw new ReportedException(crashReport); + } + } + } + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java similarity index 82% rename from src/main/java/eu/crushedpixel/replaymod/ReplayMod.java rename to src/main/java/com/replaymod/core/ReplayMod.java index ffd90e6a..7c97eeee 100755 --- a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -1,34 +1,30 @@ -package eu.crushedpixel.replaymod; +package com.replaymod.core; import com.google.common.util.concurrent.ListenableFutureTask; +import com.replaymod.replay.ReplaySender; import eu.crushedpixel.replaymod.api.ApiClient; import eu.crushedpixel.replaymod.api.replay.holders.FileInfo; import eu.crushedpixel.replaymod.chat.ChatMessageHandler; -import eu.crushedpixel.replaymod.events.handlers.*; +import eu.crushedpixel.replaymod.events.handlers.CrosshairRenderHandler; +import eu.crushedpixel.replaymod.events.handlers.GuiEventHandler; +import eu.crushedpixel.replaymod.events.handlers.MouseInputHandler; +import eu.crushedpixel.replaymod.events.handlers.TickAndRenderListener; import eu.crushedpixel.replaymod.events.handlers.keyboard.KeyInputHandler; import eu.crushedpixel.replaymod.gui.online.GuiReplayDownloading; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; -import eu.crushedpixel.replaymod.holders.KeyframeSet; import eu.crushedpixel.replaymod.localization.LocalizedResourcePack; import eu.crushedpixel.replaymod.online.authentication.ConfigurationAuthData; import eu.crushedpixel.replaymod.online.urischeme.UriScheme; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import eu.crushedpixel.replaymod.registry.*; import eu.crushedpixel.replaymod.renderer.CustomObjectRenderer; import eu.crushedpixel.replaymod.renderer.InvisibilityRender; import eu.crushedpixel.replaymod.renderer.PathPreviewRenderer; import eu.crushedpixel.replaymod.renderer.SpectatorRenderer; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.replay.ReplayProcess; -import eu.crushedpixel.replaymod.replay.ReplaySender; import eu.crushedpixel.replaymod.settings.EncodingPreset; import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.settings.ReplaySettings; import eu.crushedpixel.replaymod.sound.SoundHandler; import eu.crushedpixel.replaymod.timer.ReplayTimer; import eu.crushedpixel.replaymod.utils.OpenGLUtils; -import eu.crushedpixel.replaymod.utils.ReplayFile; -import eu.crushedpixel.replaymod.utils.ReplayFileIO; import eu.crushedpixel.replaymod.utils.TooltipRenderer; import eu.crushedpixel.replaymod.video.rendering.Pipelines; import lombok.Getter; @@ -38,6 +34,7 @@ import net.minecraft.client.resources.IResourcePack; import net.minecraft.client.settings.GameSettings; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.ResourceLocation; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.config.Configuration; import net.minecraftforge.fml.client.FMLClientHandler; @@ -50,6 +47,7 @@ import net.minecraftforge.fml.common.ModContainer; import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import java.io.File; @@ -61,14 +59,11 @@ import java.util.List; import java.util.Map; import java.util.Queue; -import static org.apache.commons.io.FileUtils.forceDelete; -import static org.apache.commons.io.FileUtils.listFiles; - -@Mod(modid = ReplayMod.MODID, useMetadata = true) +@Mod(modid = ReplayMod.MOD_ID, useMetadata = true) public class ReplayMod { public static ModContainer getContainer() { - return Loader.instance().getIndexedModList().get(MODID); + return Loader.instance().getIndexedModList().get(MOD_ID); } @Getter(lazy = true) @@ -85,14 +80,16 @@ public class ReplayMod { return "Unknown"; } - public static final String MODID = "replaymod"; + public static final String MOD_ID = "replaymod"; + + public static final ResourceLocation TEXTURE = new ResourceLocation("replaymod", "replay_gui.png"); + public static final int TEXTURE_SIZE = 128; + private static final Minecraft mc = Minecraft.getMinecraft(); public static GuiEventHandler guiEventHandler; - public static GuiReplayOverlay overlay; public static ReplaySettings replaySettings; public static Configuration config; public static boolean firstMainMenu = true; - public static RecordingHandler recordingHandler; public static ChatMessageHandler chatMessageHandler = new ChatMessageHandler(); public static KeyInputHandler keyInputHandler = new KeyInputHandler(); public static MouseInputHandler mouseInputHandler = new MouseInputHandler(); @@ -111,13 +108,30 @@ public class ReplayMod { public static CrosshairRenderHandler crosshairRenderHandler; public static ApiClient apiClient; + private final KeyBindingRegistry keyBindingRegistry = new KeyBindingRegistry(); + private final SettingsRegistry settingsRegistry = new SettingsRegistry(); + @Getter private static boolean latestModVersion = true; // The instance of your mod that Forge uses. - @Instance(value = "ReplayModID") + @Instance(MOD_ID) public static ReplayMod instance; + public KeyBindingRegistry getKeyBindingRegistry() { + return keyBindingRegistry; + } + + public SettingsRegistry getSettingsRegistry() { + return settingsRegistry; + } + + public File getReplayFolder() throws IOException { + File folder = new File(replaySettings.getRecordingPath()); + FileUtils.forceMkdir(folder); + return folder; + } + @EventHandler public void preInit(FMLPreInitializationEvent event) { try { @@ -137,6 +151,7 @@ public class ReplayMod { config = new Configuration(event.getSuggestedConfigurationFile()); config.load(); + settingsRegistry.setConfiguration(config); ConfigurationAuthData authData = new ConfigurationAuthData(config); apiClient = new ApiClient(authData); authData.load(apiClient); @@ -144,7 +159,6 @@ public class ReplayMod { uploadedFileHandler = new UploadedFileHandler(event.getModConfigurationDirectory()); replaySettings = new ReplaySettings(); - replaySettings.readValues(); downloadedFileHandler = new DownloadedFileHandler(); favoritedFileHandler = new FavoritedFileHandler(); @@ -164,27 +178,22 @@ public class ReplayMod { @EventHandler public void init(FMLInitializationEvent event) { mc.timer = new ReplayTimer(); - overlay = new GuiReplayOverlay(); - FMLCommonHandler.instance().bus().register(new ConnectionEventHandler()); MinecraftForge.EVENT_BUS.register(guiEventHandler = new GuiEventHandler()); FMLCommonHandler.instance().bus().register(keyInputHandler); + FMLCommonHandler.instance().bus().register(keyBindingRegistry); FMLCommonHandler.instance().bus().register(mouseInputHandler); MinecraftForge.EVENT_BUS.register(mouseInputHandler); - - recordingHandler = new RecordingHandler(); - FMLCommonHandler.instance().bus().register(recordingHandler); - MinecraftForge.EVENT_BUS.register(recordingHandler); } @EventHandler public void postInit(FMLPostInitializationEvent event) throws IOException { + settingsRegistry.save(); // Save default values to disk + if(!FMLClientHandler.instance().hasOptifine()) GameSettings.Options.RENDER_DISTANCE.setValueMax(64f); - overlay.register(); - TickAndRenderListener tarl = new TickAndRenderListener(); FMLCommonHandler.instance().bus().register(tarl); MinecraftForge.EVENT_BUS.register(tarl); @@ -212,9 +221,6 @@ public class ReplayMod { skinMap.put("default", new InvisibilityRender(mc.getRenderManager())); skinMap.put("slim", new InvisibilityRender(mc.getRenderManager(), true)); - //clean up replay_recordings folder - removeTmcprFiles(); - Thread localizedResourcePackLoader = new Thread(new Runnable() { @Override public void run() { @@ -330,36 +336,42 @@ public class ReplayMod { } System.out.println("Loading replay..."); - ReplayHandler.startReplay(file, false); - - int index = 0; - if (path != null) { - for (KeyframeSet set : ReplayHandler.getKeyframeRepository()) { - if (set.getName().equals(path)) { - break; - } - index++; - } - if (index >= ReplayHandler.getKeyframeRepository().length) { - throw new IllegalArgumentException("No path named \"" + path + "\"."); - } - } else if (ReplayHandler.getKeyframeRepository().length == 0) { - throw new IllegalArgumentException("Replay file has no paths defined."); - } - ReplayHandler.useKeyframePresetFromRepository(index); - - System.out.println("Rendering started..."); - try { - ReplayProcess.startReplayProcess(options, true); - } catch (Throwable t) { - t.printStackTrace(); - FMLCommonHandler.instance().exitJava(1, false); - } - if (mc.hasCrashed) { - System.out.println(mc.crashReporter.getCompleteReport()); - } - System.out.println("Rendering done. Shutting down..."); - mc.shutdown(); + // TODO +// try { +// ReplayHandler.startReplay(file, false); +// } catch (Throwable t) { +// t.printStackTrace(); +// FMLCommonHandler.instance().exitJava(1, false); +// } +// +// int index = 0; +// if (path != null) { +// for (KeyframeSet set : ReplayHandler.getKeyframeRepository()) { +// if (set.getName().equals(path)) { +// break; +// } +// index++; +// } +// if (index >= ReplayHandler.getKeyframeRepository().length) { +// throw new IllegalArgumentException("No path named \"" + path + "\"."); +// } +// } else if (ReplayHandler.getKeyframeRepository().length == 0) { +// throw new IllegalArgumentException("Replay file has no paths defined."); +// } +// ReplayHandler.useKeyframePresetFromRepository(index); +// +// System.out.println("Rendering started..."); +// try { +// ReplayProcess.startReplayProcess(options, true); +// } catch (Throwable t) { +// t.printStackTrace(); +// FMLCommonHandler.instance().exitJava(1, false); +// } +// if (mc.hasCrashed) { +// System.out.println(mc.crashReporter.getCompleteReport()); +// } +// System.out.println("Rendering done. Shutting down..."); +// mc.shutdown(); } }, null)); } @@ -422,7 +434,8 @@ public class ReplayMod { new GuiReplayDownloading(info).display(); } else { try { - ReplayHandler.startReplay(file); + // TODO +// ReplayHandler.startReplay(file); } catch (Exception e) { e.printStackTrace(); } @@ -434,16 +447,4 @@ public class ReplayMod { System.exit(-1); } } - - private void removeTmcprFiles() { - try { - File folder = ReplayFileIO.getReplayFolder(); - - for (File file : listFiles(folder, new String[]{ReplayFile.TEMP_FILE_EXTENSION.substring(1)}, false)) { - forceDelete(file); - } - } catch (IOException e) { - e.printStackTrace(); - } - } } diff --git a/src/main/java/com/replaymod/core/SettingsRegistry.java b/src/main/java/com/replaymod/core/SettingsRegistry.java new file mode 100644 index 00000000..6cb0f234 --- /dev/null +++ b/src/main/java/com/replaymod/core/SettingsRegistry.java @@ -0,0 +1,133 @@ +package com.replaymod.core; + +import net.minecraft.client.resources.I18n; +import net.minecraftforge.common.config.Configuration; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class SettingsRegistry { + private static final Object NULL_OBJECT = new Object(); + private Map, Object> settings = new ConcurrentHashMap<>(); + private Configuration configuration; + + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + + List> keys = new ArrayList<>(settings.keySet()); + settings.clear(); + for (SettingKey key : keys) { + register(key); + } + } + + public void register(Class settingsClass) { + for (Field field : settingsClass.getDeclaredFields()) { + if ((field.getModifiers() & (Modifier.STATIC | Modifier.PUBLIC)) != 0 + && SettingKey.class.isAssignableFrom(field.getType())) { + try { + register((SettingKey) field.get(null)); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + } + + public void register(SettingKey key) { + Object value; + if (configuration != null) { + if (key.getDefault() instanceof Boolean) { + value = configuration.get(key.getCategory(), key.getKey(), (Boolean) key.getDefault()).getBoolean(); + } else if (key.getDefault() instanceof Integer) { + value = configuration.get(key.getCategory(), key.getKey(), (Integer) key.getDefault()).getInt(); + } else if (key.getDefault() instanceof Double) { + value = configuration.get(key.getCategory(), key.getKey(), (Double) key.getDefault()).getDouble(); + } else if (key.getDefault() instanceof String) { + value = configuration.get(key.getCategory(), key.getKey(), (String) key.getDefault()).getString(); + } else { + throw new IllegalArgumentException("Default type " + key.getDefault().getClass() + " not supported."); + } + } else { + value = NULL_OBJECT; + } + settings.put(key, value); + } + + public Set> getSettings() { + return settings.keySet(); + } + + @SuppressWarnings("unchecked") + public T get(SettingKey key) { + if (!settings.containsKey(key)) { + throw new IllegalArgumentException("Setting " + key + " unknown."); + } + return (T) settings.get(key); + } + + public void set(SettingKey key, T value) { + if (key.getDefault() instanceof Boolean) { + configuration.get(key.getCategory(), key.getKey(), (Boolean) key.getDefault()).set((Boolean) value); + } else if (key.getDefault() instanceof Integer) { + configuration.get(key.getCategory(), key.getKey(), (Integer) key.getDefault()).set((Integer) value); + } else if (key.getDefault() instanceof Double) { + configuration.get(key.getCategory(), key.getKey(), (Double) key.getDefault()).set((Double) value); + } else if (key.getDefault() instanceof String) { + configuration.get(key.getCategory(), key.getKey(), (String) key.getDefault()).set((String) value); + } else { + throw new IllegalArgumentException("Default type " + key.getDefault().getClass() + " not supported."); + } + settings.put(key, value); + } + + public void save() { + configuration.save(); + } + + public interface SettingKey { + String getCategory(); + String getKey(); + String getDisplayString(); + T getDefault(); + } + + public static class SettingKeys implements SettingKey { + private final String category; + private final String key; + private final String displayString; + private final T defaultValue; + + public SettingKeys(String category, String key, String displayString, T defaultValue) { + this.category = category; + this.key = key; + this.displayString = displayString; + this.defaultValue = defaultValue; + } + + @Override + public String getCategory() { + return category; + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getDisplayString() { + return I18n.format(displayString); + } + + @Override + public T getDefault() { + return defaultValue; + } + } +} diff --git a/src/main/java/com/replaymod/core/gui/GuiReplaySettings.java b/src/main/java/com/replaymod/core/gui/GuiReplaySettings.java new file mode 100644 index 00000000..90e3d150 --- /dev/null +++ b/src/main/java/com/replaymod/core/gui/GuiReplaySettings.java @@ -0,0 +1,75 @@ +package com.replaymod.core.gui; + +import de.johni0702.minecraft.gui.container.AbstractGuiScreen; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiElement; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.element.GuiToggleButton; +import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; +import com.replaymod.core.SettingsRegistry; +import net.minecraft.client.resources.I18n; + +public class GuiReplaySettings extends AbstractGuiScreen { + + public GuiReplaySettings(final net.minecraft.client.gui.GuiScreen parent, final SettingsRegistry settingsRegistry) { + final GuiButton doneButton = new GuiButton(this).setI18nLabel("gui.done").setSize(200, 20).onClick(new Runnable() { + @Override + public void run() { + getMinecraft().displayGuiScreen(parent); + } + }); + + final GuiPanel allElements = new GuiPanel(this).setLayout(new HorizontalLayout().setSpacing(10)); + GuiPanel leftColumn = new GuiPanel().setLayout(new VerticalLayout().setSpacing(4)); + GuiPanel rightColumn = new GuiPanel().setLayout(new VerticalLayout().setSpacing(4)); + allElements.addElements(new VerticalLayout.Data(0), leftColumn, rightColumn); + HorizontalLayout.Data leftHorizontalData = new HorizontalLayout.Data(1); + HorizontalLayout.Data rightHorizontalData = new HorizontalLayout.Data(0); + int i = 0; + for (final SettingsRegistry.SettingKey key : settingsRegistry.getSettings()) { + if (key.getDisplayString() != null) { + GuiElement element; + if (key.getDefault() instanceof Boolean) { + @SuppressWarnings("unchecked") + final SettingsRegistry.SettingKey booleanKey = (SettingsRegistry.SettingKey) key; + final GuiToggleButton button = new GuiToggleButton<>().setSize(150, 20) + .setLabel(key.getDisplayString()).setSelected(settingsRegistry.get(booleanKey) ? 0 : 1) + .setValues(I18n.format("options.on"), I18n.format("options.off")); + element = button.onClick(new Runnable() { + @Override + public void run() { + settingsRegistry.set(booleanKey, button.getSelected() == 0); + settingsRegistry.save(); + } + }); + } else { + throw new IllegalArgumentException("Type " + key.getDefault().getClass() + " not supported."); + } + + if (i++ % 2 == 0) { + leftColumn.addElements(leftHorizontalData, element); + } else { + rightColumn.addElements(rightHorizontalData, element); + } + } + } + + setLayout(new CustomLayout() { + @Override + protected void layout(GuiReplaySettings container, int width, int height) { + pos(allElements, width / 2 - 155, height / 6); + pos(doneButton, width / 2 - 100, height - 27); + } + }); + + setTitle(new GuiLabel().setI18nText("replaymod.gui.settings.title")); + } + + @Override + protected GuiReplaySettings getThis() { + return this; + } +} diff --git a/src/main/java/com/replaymod/extras/Extra.java b/src/main/java/com/replaymod/extras/Extra.java new file mode 100644 index 00000000..93f3acd6 --- /dev/null +++ b/src/main/java/com/replaymod/extras/Extra.java @@ -0,0 +1,7 @@ +package com.replaymod.extras; + +import com.replaymod.core.ReplayMod; + +public interface Extra { + void register(ReplayMod mod) throws Exception; +} diff --git a/src/main/java/com/replaymod/extras/HotkeyButtons.java b/src/main/java/com/replaymod/extras/HotkeyButtons.java new file mode 100644 index 00000000..1aea2bfa --- /dev/null +++ b/src/main/java/com/replaymod/extras/HotkeyButtons.java @@ -0,0 +1,101 @@ +package com.replaymod.extras; + +import com.replaymod.core.ReplayMod; +import com.replaymod.replay.events.ReplayOpenEvent; +import com.replaymod.replay.gui.overlay.GuiReplayOverlay; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiElement; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.element.GuiTexturedButton; +import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.GridLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.LayoutData; +import net.minecraft.client.settings.KeyBinding; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import org.lwjgl.input.Keyboard; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +public class HotkeyButtons implements Extra { + private ReplayMod mod; + + @Override + public void register(ReplayMod mod) throws Exception { + this.mod = mod; + + FMLCommonHandler.instance().bus().register(this); + } + + @SubscribeEvent + public void postReplayOpen(ReplayOpenEvent.Post event) { + GuiReplayOverlay overlay = event.getReplayHandler().getOverlay(); + new Gui(mod, overlay); + } + + public static final class Gui { + private final GuiTexturedButton toggleButton; + private final GridLayout panelLayout; + private final GuiPanel panel; + + private boolean open; + + public Gui(ReplayMod mod, GuiReplayOverlay overlay) { + toggleButton = new GuiTexturedButton(overlay).setSize(20, 20) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(0, 0) + .onClick(new Runnable() { + @Override + public void run() { + open = !open; + } + }); + + panel = new GuiPanel(overlay) { + @Override + public Collection getChildren() { + return open ? super.getChildren() : Collections.emptyList(); + } + + @Override + public Map getElements() { + return open ? super.getElements() : Collections.emptyMap(); + } + }.setLayout(panelLayout = new GridLayout().setSpacingX(5).setSpacingY(5).setColumns(1)); + + for (final KeyBinding keyBinding : mod.getKeyBindingRegistry().getKeyBindings().values()) { + String keyName = "???"; + try { + keyName = Keyboard.getKeyName(keyBinding.getKeyCode()); + } catch (ArrayIndexOutOfBoundsException e) { + // Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55 + } + panel.addElements(null, new GuiPanel().setSize(150, 20).setLayout(new HorizontalLayout().setSpacing(2)) + .addElements(new HorizontalLayout.Data(0.5), + new GuiButton().setSize(20, 20).setLabel(keyName).onClick(new Runnable() { + @Override + public void run() { + keyBinding.pressTime++; + FMLCommonHandler.instance().fireKeyInput(); + } + }), + new GuiLabel().setI18nText(keyBinding.getKeyDescription()) + )); + } + + overlay.setLayout(new CustomLayout(overlay.getLayout()) { + @Override + protected void layout(GuiReplayOverlay container, int width, int height) { + panelLayout.setColumns((width - 10) / 105); + size(panel, panelLayout.calcMinSize(container)); + + pos(toggleButton, 5, height - 25); + pos(panel, 5, y(toggleButton) - 5 - height(panel)); + } + }); + } + } +} diff --git a/src/main/java/com/replaymod/extras/ReplayModExtras.java b/src/main/java/com/replaymod/extras/ReplayModExtras.java new file mode 100644 index 00000000..623bbaa1 --- /dev/null +++ b/src/main/java/com/replaymod/extras/ReplayModExtras.java @@ -0,0 +1,44 @@ +package com.replaymod.extras; + +import com.replaymod.core.ReplayMod; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import org.apache.logging.log4j.Logger; + +import java.util.Arrays; +import java.util.List; + +@Mod(modid = ReplayModExtras.MOD_ID, useMetadata = true) +public class ReplayModExtras { + public static final String MOD_ID = "replaymod-extras"; + + @Mod.Instance(MOD_ID) + public static ReplayModExtras instance; + + @Mod.Instance(ReplayMod.MOD_ID) + private static ReplayMod core; + + private static final List> builtin = Arrays.>asList( + HotkeyButtons.class + ); + + private Logger logger; + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + logger = event.getModLog(); + } + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + for (Class cls : builtin) { + try { + Extra extra = cls.newInstance(); + extra.register(core); + } catch (Throwable t) { + logger.warn("Failed to load extra " + cls.getName() + ": ", t); + } + } + } +} diff --git a/src/main/java/com/replaymod/recording/ReplayModRecording.java b/src/main/java/com/replaymod/recording/ReplayModRecording.java new file mode 100644 index 00000000..66d3ce10 --- /dev/null +++ b/src/main/java/com/replaymod/recording/ReplayModRecording.java @@ -0,0 +1,47 @@ +package com.replaymod.recording; + +import com.replaymod.recording.handler.ConnectionEventHandler; +import com.replaymod.recording.packet.PacketListener; +import com.replaymod.core.ReplayMod; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.EventBus; +import org.apache.logging.log4j.Logger; +import org.lwjgl.input.Keyboard; + +@Mod(modid = ReplayModRecording.MOD_ID, useMetadata = true) +public class ReplayModRecording { + public static final String MOD_ID = "replaymod-recording"; + + @Mod.Instance(ReplayMod.MOD_ID) + private static ReplayMod core; + + private Logger logger; + + private ConnectionEventHandler connectionEventHandler; + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + logger = event.getModLog(); + + core.getSettingsRegistry().register(Setting.class); + + core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() { + @Override + public void run() { + PacketListener packetListener = connectionEventHandler.getPacketListener(); + if (packetListener != null) { + packetListener.addMarker(); + } + } + }); + } + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + EventBus bus = FMLCommonHandler.instance().bus(); + bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core)); + } +} diff --git a/src/main/java/com/replaymod/recording/Setting.java b/src/main/java/com/replaymod/recording/Setting.java new file mode 100644 index 00000000..e3742d8b --- /dev/null +++ b/src/main/java/com/replaymod/recording/Setting.java @@ -0,0 +1,17 @@ +package com.replaymod.recording; + +import com.replaymod.core.SettingsRegistry; + +public final class Setting extends SettingsRegistry.SettingKeys { + public static final Setting RECORD_SINGLEPLAYER = make("recordSingleplayer", "recordsingleplayer", true); + public static final Setting RECORD_SERVER = make("recordServer", "recordserver", true); + public static final Setting INDICATOR = make("indicator", "indicator", true); + + private static Setting make(String key, String displayName, T defaultValue) { + return new Setting<>(key, displayName, defaultValue); + } + + public Setting(String key, String displayString, T defaultValue) { + super("recording", key, "replaymod.gui.settings." + displayString, defaultValue); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiRecordingOverlay.java b/src/main/java/com/replaymod/recording/gui/GuiRecordingOverlay.java similarity index 74% rename from src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiRecordingOverlay.java rename to src/main/java/com/replaymod/recording/gui/GuiRecordingOverlay.java index 30722889..68b8d9b1 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiRecordingOverlay.java +++ b/src/main/java/com/replaymod/recording/gui/GuiRecordingOverlay.java @@ -1,16 +1,17 @@ -package eu.crushedpixel.replaymod.gui.overlay; +package com.replaymod.recording.gui; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.resources.I18n; import net.minecraftforge.client.event.RenderGameOverlayEvent; +import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import static eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay.TEXTURE_SIZE; -import static eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay.replay_gui; +import static com.replaymod.core.ReplayMod.TEXTURE; +import static com.replaymod.core.ReplayMod.TEXTURE_SIZE; /** * Renders overlay during recording. @@ -22,6 +23,14 @@ public class GuiRecordingOverlay { this.mc = mc; } + public void register() { + MinecraftForge.EVENT_BUS.register(this); + } + + public void unregister() { + MinecraftForge.EVENT_BUS.unregister(this); + } + /** * Render the recording icon and text in the top left corner of the screen. * @param event Rendered post game overlay @@ -32,7 +41,7 @@ public class GuiRecordingOverlay { if(ReplayMod.replaySettings.showRecordingIndicator()) { FontRenderer fontRenderer = mc.fontRendererObj; fontRenderer.drawString(I18n.format("replaymod.gui.recording").toUpperCase(), 30, 18 - (fontRenderer.FONT_HEIGHT / 2), 0xffffffff); - mc.renderEngine.bindTexture(replay_gui); + mc.renderEngine.bindTexture(TEXTURE); GlStateManager.resetColor(); GlStateManager.enableAlpha(); Gui.drawModalRectWithCustomSizedTexture(10, 10, 58, 20, 16, 16, TEXTURE_SIZE, TEXTURE_SIZE); diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java similarity index 52% rename from src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java rename to src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 1785721a..5cd214b0 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -1,79 +1,53 @@ -package eu.crushedpixel.replaymod.recording; +package com.replaymod.recording.handler; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.recording.Setting; +import com.replaymod.recording.gui.GuiRecordingOverlay; +import com.replaymod.recording.packet.PacketListener; +import de.johni0702.replaystudio.replay.ReplayFile; +import de.johni0702.replaystudio.replay.ZipReplayFile; +import de.johni0702.replaystudio.studio.ReplayStudio; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.chat.ChatMessageHandler.ChatMessageType; -import eu.crushedpixel.replaymod.gui.overlay.GuiRecordingOverlay; -import eu.crushedpixel.replaymod.utils.ReplayFile; -import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import net.minecraft.client.Minecraft; import net.minecraft.network.NetworkManager; -import net.minecraft.network.Packet; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldType; -import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.network.FMLNetworkEvent.ClientConnectedToServerEvent; import net.minecraftforge.fml.common.network.FMLNetworkEvent.ClientDisconnectionFromServerEvent; -import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.text.SimpleDateFormat; import java.util.Calendar; +/** + * Handles connection events and initiates recording if enabled. + */ public class ConnectionEventHandler { private static final String packetHandlerKey = "packet_handler"; private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss"; private static final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); - private static PacketListener packetListener = null; - private static boolean isRecording = false; - private final GuiRecordingOverlay guiOverlay = new GuiRecordingOverlay(Minecraft.getMinecraft()); + private static final Minecraft mc = Minecraft.getMinecraft(); - private static final Logger logger = LogManager.getLogger(); + private final Logger logger; + private final ReplayMod core; - public static boolean isRecording() { - return isRecording; - } + private RecordingEventHandler recordingEventHandler; + private PacketListener packetListener; + private GuiRecordingOverlay guiOverlay; - public static void notifyServerPaused() { - if (packetListener != null) { - packetListener.serverWasPaused = true; - } - } - - public static void insertPacket(Packet packet) { - if(!isRecording || packetListener == null) { - String reason = isRecording ? " (recording)" : " (null)"; - logger.error("Invalid attempt to insert Packet!" + reason); - return; - } - try { - packetListener.saveOnly(packet); - } catch(Exception e) { - e.printStackTrace(); - } - } - - public static void addMarker() { - if(!isRecording || packetListener == null) { - String reason = isRecording ? " (recording)" : " (null)"; - logger.error("Invalid attempt to insert MarkerKeyframe!" + reason); - return; - } - try { - packetListener.addMarker(); - } catch(Exception e) { - e.printStackTrace(); - } + public ConnectionEventHandler(Logger logger, ReplayMod core) { + this.logger = logger; + this.core = core; } @SubscribeEvent public void onConnectedToServerEvent(ClientConnectedToServerEvent event) { ReplayMod.chatMessageHandler.initialize(); - ReplayMod.recordingHandler.resetVars(); try { if(event.isLocal) { @@ -81,16 +55,17 @@ public class ConnectionEventHandler { logger.info("Debug World recording is not supported."); return; } - if(!ReplayMod.replaySettings.isEnableRecordingSingleplayer()) { + if(!core.getSettingsRegistry().get(Setting.RECORD_SINGLEPLAYER)) { logger.info("Singleplayer Recording is disabled"); return; } } else { - if(!ReplayMod.replaySettings.isEnableRecordingServer()) { + if(!core.getSettingsRegistry().get(Setting.RECORD_SERVER)) { logger.info("Multiplayer Recording is disabled"); return; } } + NetworkManager nm = event.manager; String worldName; if(event.isLocal) { @@ -101,17 +76,22 @@ public class ConnectionEventHandler { Channel channel = nm.channel(); ChannelPipeline pipeline = channel.pipeline(); - File folder = ReplayFileIO.getReplayFolder(); + File folder = core.getReplayFolder(); - String fileName = sdf.format(Calendar.getInstance().getTime()); - File currentFile = new File(folder, fileName + ReplayFile.TEMP_FILE_EXTENSION); + String name = sdf.format(Calendar.getInstance().getTime()); + File currentFile = new File(folder, name + ".mcpr"); + ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), currentFile); + + packetListener = new PacketListener(replayFile, name, worldName, System.currentTimeMillis(), event.isLocal); + pipeline.addBefore(packetHandlerKey, "replay_recorder", packetListener); + + recordingEventHandler = new RecordingEventHandler(packetListener); + recordingEventHandler.register(); + + guiOverlay = new GuiRecordingOverlay(mc); + guiOverlay.register(); - pipeline.addBefore(packetHandlerKey, "replay_recorder", packetListener = new PacketListener - (currentFile, fileName, worldName, System.currentTimeMillis(), event.isLocal)); ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.recordingstarted", ChatMessageType.INFORMATION); - isRecording = true; - - MinecraftForge.EVENT_BUS.register(guiOverlay); } catch(Exception e) { ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.recordingfailed", ChatMessageType.WARNING); e.printStackTrace(); @@ -120,11 +100,18 @@ public class ConnectionEventHandler { @SubscribeEvent public void onDisconnectedFromServerEvent(ClientDisconnectionFromServerEvent event) { - if (isRecording) { - isRecording = false; - MinecraftForge.EVENT_BUS.unregister(guiOverlay); + if (packetListener != null) { + guiOverlay.unregister(); + guiOverlay = null; + recordingEventHandler.unregister(); + recordingEventHandler = null; packetListener = null; + ReplayMod.chatMessageHandler.stop(); } } + + public PacketListener getPacketListener() { + return packetListener; + } } diff --git a/src/main/java/eu/crushedpixel/replaymod/events/handlers/RecordingHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java similarity index 81% rename from src/main/java/eu/crushedpixel/replaymod/events/handlers/RecordingHandler.java rename to src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index 6bd7ad8f..4aafa8ee 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/handlers/RecordingHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -1,6 +1,6 @@ -package eu.crushedpixel.replaymod.events.handlers; +package com.replaymod.recording.handler; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; +import com.replaymod.recording.packet.PacketListener; import eu.crushedpixel.replaymod.utils.Objects; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -13,39 +13,60 @@ import net.minecraft.network.PacketBuffer; import net.minecraft.network.play.server.*; import net.minecraft.network.play.server.S14PacketEntity.S17PacketEntityLookMove; import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.util.BlockPos; import net.minecraft.util.MathHelper; +import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.minecart.MinecartInteractEvent; import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent; import net.minecraftforge.event.entity.player.PlayerUseItemEvent; +import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.PlayerEvent.ItemPickupEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; import net.minecraftforge.fml.common.gameevent.TickEvent.PlayerTickEvent; -public class RecordingHandler { +public class RecordingEventHandler { private final Minecraft mc = Minecraft.getMinecraft(); - private Double lastX = null, lastY = null, lastZ = null; + private final PacketListener packetListener; + + private Double lastX, lastY, lastZ; private ItemStack[] playerItems = new ItemStack[5]; - private int ticksSinceLastCorrection = 0; - private boolean wasSleeping = false; + private int ticksSinceLastCorrection; + private boolean wasSleeping; private int lastRiding = -1; - private Integer rotationYawHeadBefore = null; + private Integer rotationYawHeadBefore; + + public RecordingEventHandler(PacketListener packetListener) { + this.packetListener = packetListener; + } + + public void register() { + FMLCommonHandler.instance().bus().register(this); + MinecraftForge.EVENT_BUS.register(this); + ((RecordingEventSender) mc.renderGlobal).setRecordingEventHandler(this); + } + + public void unregister() { + FMLCommonHandler.instance().bus().unregister(this); + MinecraftForge.EVENT_BUS.unregister(this); + RecordingEventSender recordingEventSender = ((RecordingEventSender) mc.renderGlobal); + if (recordingEventSender.getRecordingEventHandler() == this) { + recordingEventSender.setRecordingEventHandler(null); + } + } public void onPlayerJoin() { try { - if(!ConnectionEventHandler.isRecording()) return; - - ConnectionEventHandler.insertPacket(spawnPlayer(mc.thePlayer)); + packetListener.save(spawnPlayer(mc.thePlayer)); } catch(Exception e1) { e1.printStackTrace(); } } public void onPlayerRespawn() { - if(!ConnectionEventHandler.isRecording()) return; try { - ConnectionEventHandler.insertPacket(spawnPlayer(mc.thePlayer)); + packetListener.save(spawnPlayer(mc.thePlayer)); } catch(Exception e) { e.printStackTrace(); } @@ -81,18 +102,10 @@ public class RecordingHandler { } } - public void resetVars() { - lastX = lastY = lastZ = null; - rotationYawHeadBefore = null; - playerItems = new ItemStack[5]; - } - @SubscribeEvent public void onPlayerTick(PlayerTickEvent e) { - if(!ConnectionEventHandler.isRecording()) return; try { if(e.player != mc.thePlayer) return; - if(!ConnectionEventHandler.isRecording()) return; boolean force = false; if(lastX == null || lastY == null || lastZ == null) { @@ -133,7 +146,7 @@ public class RecordingHandler { newYaw, newPitch, e.player.onGround); } - ConnectionEventHandler.insertPacket(packet); + packetListener.save(packet); //HEAD POS int rotationYawHead = ((int)(e.player.rotationYawHead * 256.0F / 360.0F)); @@ -148,13 +161,13 @@ public class RecordingHandler { head.readPacketData(pb1); - ConnectionEventHandler.insertPacket(head); + packetListener.save(head); rotationYawHeadBefore = rotationYawHead; } S12PacketEntityVelocity vel = new S12PacketEntityVelocity(e.player.getEntityId(), e.player.motionX, e.player.motionY, e.player.motionZ); - ConnectionEventHandler.insertPacket(vel); + packetListener.save(vel); //Animation Packets //Swing Animation @@ -169,7 +182,7 @@ public class RecordingHandler { pac.readPacketData(pb); - ConnectionEventHandler.insertPacket(pac); + packetListener.save(pac); } /* @@ -179,13 +192,13 @@ public class RecordingHandler { found.add(pe.getPotionID()); if(lastEffects.contains(found)) continue; S1DPacketEntityEffect pee = new S1DPacketEntityEffect(entityID, pe); - ConnectionEventHandler.insertPacket(pee); + packetListener.save(pee); } for(int id : lastEffects) { if(!found.contains(id)) { S1EPacketRemoveEntityEffect pre = new S1EPacketRemoveEntityEffect(entityID, new PotionEffect(id, 0)); - ConnectionEventHandler.insertPacket(pre); + packetListener.save(pre); } } @@ -196,31 +209,31 @@ public class RecordingHandler { if(playerItems[0] != mc.thePlayer.getHeldItem()) { playerItems[0] = mc.thePlayer.getHeldItem(); S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 0, playerItems[0]); - ConnectionEventHandler.insertPacket(pee); + packetListener.save(pee); } if(playerItems[1] != mc.thePlayer.inventory.armorInventory[0]) { playerItems[1] = mc.thePlayer.inventory.armorInventory[0]; S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 1, playerItems[1]); - ConnectionEventHandler.insertPacket(pee); + packetListener.save(pee); } if(playerItems[2] != mc.thePlayer.inventory.armorInventory[1]) { playerItems[2] = mc.thePlayer.inventory.armorInventory[1]; S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 2, playerItems[2]); - ConnectionEventHandler.insertPacket(pee); + packetListener.save(pee); } if(playerItems[3] != mc.thePlayer.inventory.armorInventory[2]) { playerItems[3] = mc.thePlayer.inventory.armorInventory[2]; S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 3, playerItems[3]); - ConnectionEventHandler.insertPacket(pee); + packetListener.save(pee); } if(playerItems[4] != mc.thePlayer.inventory.armorInventory[3]) { playerItems[4] = mc.thePlayer.inventory.armorInventory[3]; S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 4, playerItems[4]); - ConnectionEventHandler.insertPacket(pee); + packetListener.save(pee); } //Leaving Ride @@ -244,7 +257,7 @@ public class RecordingHandler { pea.readPacketData(pbuf); - ConnectionEventHandler.insertPacket(pea); + packetListener.save(pea); } //Sleeping @@ -259,7 +272,7 @@ public class RecordingHandler { pac.readPacketData(pb); - ConnectionEventHandler.insertPacket(pac); + packetListener.save(pac); wasSleeping = false; } @@ -271,9 +284,8 @@ public class RecordingHandler { @SubscribeEvent public void onPickupItem(ItemPickupEvent event) { - if(!ConnectionEventHandler.isRecording()) return; try { - ConnectionEventHandler.insertPacket(new S0DPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId())); + packetListener.save(new S0DPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId())); } catch(Exception e) { e.printStackTrace(); } @@ -281,7 +293,6 @@ public class RecordingHandler { @SubscribeEvent public void onStartEating(PlayerUseItemEvent.Start event) { - if(!ConnectionEventHandler.isRecording()) return; try { if(!event.entityPlayer.isEating()) return; S0BPacketAnimation packet = new S0BPacketAnimation(); @@ -294,7 +305,7 @@ public class RecordingHandler { packet.readPacketData(pb); - ConnectionEventHandler.insertPacket(packet); + packetListener.save(packet); } catch(Exception e) { e.printStackTrace(); } @@ -302,13 +313,11 @@ public class RecordingHandler { @SubscribeEvent public void onSleep(PlayerSleepInBedEvent event) { - if(!ConnectionEventHandler.isRecording()) return; try { if(event.entityPlayer != mc.thePlayer) { return; } - System.out.println(event.getResult()); S0APacketUseBed pub = new S0APacketUseBed(); ByteBuf buf = Unpooled.buffer(); @@ -319,7 +328,7 @@ public class RecordingHandler { pub.readPacketData(pbuf); - ConnectionEventHandler.insertPacket(pub); + packetListener.save(pub); wasSleeping = true; @@ -330,7 +339,6 @@ public class RecordingHandler { @SubscribeEvent public void enterMinecart(MinecartInteractEvent event) { - if(!ConnectionEventHandler.isRecording()) return; try { if(event.player != mc.thePlayer) { return; @@ -347,7 +355,7 @@ public class RecordingHandler { pea.readPacketData(pbuf); - ConnectionEventHandler.insertPacket(pea); + packetListener.save(pea); lastRiding = event.minecart.getEntityId(); } catch(Exception e) { @@ -355,15 +363,26 @@ public class RecordingHandler { } } + public void onBlockBreakAnim(int breakerId, BlockPos pos, int progress) { + EntityPlayer thePlayer = mc.thePlayer; + if (thePlayer != null && breakerId == thePlayer.getEntityId()) { + packetListener.save(new S25PacketBlockBreakAnim(breakerId, pos, progress)); + } + } + @SubscribeEvent public void checkForGamePaused(TickEvent.RenderTickEvent event) { if (event.phase != TickEvent.Phase.START) return; - if (!ConnectionEventHandler.isRecording()) return; if (mc.isIntegratedServerRunning()) { IntegratedServer server = mc.getIntegratedServer(); if (server != null && server.isGamePaused) { - ConnectionEventHandler.notifyServerPaused(); + packetListener.serverWasPaused = true; } } } + + public interface RecordingEventSender { + void setRecordingEventHandler(RecordingEventHandler recordingEventHandler); + RecordingEventHandler getRecordingEventHandler(); + } } diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinNetHandlerPlayClient.java b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java similarity index 72% rename from src/main/java/eu/crushedpixel/replaymod/mixin/MixinNetHandlerPlayClient.java rename to src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java index 5f0e73dd..baf316ed 100644 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinNetHandlerPlayClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java @@ -1,12 +1,12 @@ -package eu.crushedpixel.replaymod.mixin; +package com.replaymod.recording.mixin; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; +import com.replaymod.recording.handler.RecordingEventHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.network.play.server.S07PacketRespawn; import net.minecraft.network.play.server.S38PacketPlayerListItem; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -16,6 +16,13 @@ import java.util.List; @Mixin(NetHandlerPlayClient.class) public abstract class MixinNetHandlerPlayClient { + @Shadow + private Minecraft gameController; + + public RecordingEventHandler getRecordingEventHandler() { + return ((RecordingEventHandler.RecordingEventSender) gameController.renderGlobal).getRecordingEventHandler(); + } + /** * Record the own player entity joining the world. * We cannot use the {@link net.minecraftforge.event.entity.EntityJoinWorldEvent} because the entity id @@ -25,12 +32,13 @@ public abstract class MixinNetHandlerPlayClient { */ @Inject(method = "handlePlayerListItem", at=@At("RETURN")) public void recordOwnJoin(S38PacketPlayerListItem packet, CallbackInfo ci) { - if (ConnectionEventHandler.isRecording() && packet.func_179768_b() == S38PacketPlayerListItem.Action.ADD_PLAYER) { + RecordingEventHandler handler = getRecordingEventHandler(); + if (handler != null && packet.func_179768_b() == S38PacketPlayerListItem.Action.ADD_PLAYER) { @SuppressWarnings("unchecked") List dataList = packet.func_179767_a(); for (S38PacketPlayerListItem.AddPlayerData data : dataList) { if (data.func_179962_a().getId().equals(Minecraft.getMinecraft().thePlayer.getGameProfile().getId())) { - ReplayMod.recordingHandler.onPlayerJoin(); + handler.onPlayerJoin(); } } } @@ -45,8 +53,9 @@ public abstract class MixinNetHandlerPlayClient { */ @Inject(method = "handleRespawn", at=@At("RETURN")) public void recordOwnRespawn(S07PacketRespawn packet, CallbackInfo ci) { - if (ConnectionEventHandler.isRecording()) { - ReplayMod.recordingHandler.onPlayerRespawn(); + RecordingEventHandler handler = getRecordingEventHandler(); + if (handler != null) { + handler.onPlayerRespawn(); } } } diff --git a/src/main/java/com/replaymod/recording/mixin/MixinRenderGlobal.java b/src/main/java/com/replaymod/recording/mixin/MixinRenderGlobal.java new file mode 100644 index 00000000..d9e11de1 --- /dev/null +++ b/src/main/java/com/replaymod/recording/mixin/MixinRenderGlobal.java @@ -0,0 +1,32 @@ +package com.replaymod.recording.mixin; + +import com.replaymod.recording.handler.RecordingEventHandler; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.util.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(RenderGlobal.class) +public abstract class MixinRenderGlobal implements RecordingEventHandler.RecordingEventSender { + + private RecordingEventHandler recordingEventHandler; + + @Override + public void setRecordingEventHandler(RecordingEventHandler recordingEventHandler) { + this.recordingEventHandler = recordingEventHandler; + } + + @Override + public RecordingEventHandler getRecordingEventHandler() { + return recordingEventHandler; + } + + @Inject(method = "sendBlockBreakProgress", at = @At("HEAD")) + public void saveBlockBreakProgressPacket(int breakerId, BlockPos pos, int progress, CallbackInfo info) { + if (recordingEventHandler != null) { + recordingEventHandler.onBlockBreakAnim(breakerId, pos, progress); + } + } +} diff --git a/src/main/java/com/replaymod/recording/packet/DataListener.java b/src/main/java/com/replaymod/recording/packet/DataListener.java new file mode 100755 index 00000000..01ce0e27 --- /dev/null +++ b/src/main/java/com/replaymod/recording/packet/DataListener.java @@ -0,0 +1,182 @@ +package com.replaymod.recording.packet; + +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import de.johni0702.replaystudio.data.Marker; +import de.johni0702.replaystudio.replay.ReplayFile; +import de.johni0702.replaystudio.replay.ReplayMetaData; +import com.replaymod.core.ReplayMod; +import eu.crushedpixel.replaymod.chat.ChatMessageHandler; +import eu.crushedpixel.replaymod.holders.AdvancedPosition; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import net.minecraft.client.Minecraft; +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public abstract class DataListener extends ChannelInboundHandlerAdapter { + + private static final Logger logger = LogManager.getLogger(); + + private final ReplayFile replayFile; + protected final DataWriter dataWriter; + + protected Long startTime = null; + protected String name; + protected String worldName; + public boolean serverWasPaused; + protected long lastSentPacket = 0; + protected boolean alive = true; + protected Set players = new HashSet<>(); + private boolean singleplayer; + + private final Set markers = new HashSet<>(); + private final Map requestToHash = new ConcurrentHashMap<>(); + + public DataListener(ReplayFile replayFile, String name, String worldName, long startTime, boolean singleplayer) throws IOException { + this.replayFile = replayFile; + this.startTime = startTime; + this.name = name; + this.worldName = worldName; + this.singleplayer = singleplayer; + + dataWriter = new DataWriter(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + dataWriter.requestFinish(); + } + + protected void recordResourcePack(File file, int requestId) { + try { + String hash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); + synchronized (dataWriter) { + if (!requestToHash.containsValue(hash)) { + try (OutputStream out = replayFile.writeResourcePack(hash)) { + FileUtils.copyFile(file, out); + } + } + } + requestToHash.put(requestId, hash); + } catch (IOException e) { + logger.warn("Failed to save resource pack.", e); + } + } + + public void addMarker() { + AdvancedPosition pos = new AdvancedPosition(Minecraft.getMinecraft().getRenderViewEntity()); + int timestamp = (int) (System.currentTimeMillis() - startTime); + + Marker marker = new Marker(); + marker.setTime(timestamp); + marker.setX(pos.getX()); + marker.setY(pos.getY()); + marker.setZ(pos.getZ()); + marker.setYaw((float) pos.getYaw()); + marker.setPitch((float) pos.getPitch()); + marker.setRoll((float) pos.getRoll()); + markers.add(marker); + + ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.addedmarker", ChatMessageHandler.ChatMessageType.INFORMATION); + } + + public class DataWriter { + private long paused; + + private DataOutputStream stream; + private ExecutorService saveService = Executors.newSingleThreadExecutor(); + + private final Thread shutdownHook = new Thread(new Runnable() { + @Override + public void run() { + if (!saveService.isShutdown()) { + requestFinish(); + } + } + }, "shutdown-hook-data-listener"); + + public DataWriter() throws IOException { + stream = new DataOutputStream(replayFile.writePacketData()); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + public synchronized void writePacket(final byte[] bytes) { + long now = System.currentTimeMillis(); + if(startTime == null) { + startTime = now; + } + + if (serverWasPaused) { + paused = now - startTime - lastSentPacket; + serverWasPaused = false; + } + final int timestamp = (int) (now - startTime - paused); + lastSentPacket = timestamp; + saveService.submit(new Runnable() { + @Override + public void run() { + try { + stream.writeInt(timestamp); + stream.writeInt(bytes.length); + stream.write(bytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + } + + public synchronized void requestFinish() { + if (saveService.isShutdown()) return; + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + saveService.shutdown(); + saveService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + stream.close(); + + // TODO: Get rid of replay file appender + ReplayMod.replayFileAppender.startNewReplayFileWriting(); + + String mcversion = ReplayMod.getMinecraftVersion(); + + String[] pl = players.toArray(new String[players.size()]); + + String generator = "ReplayMod v" + ReplayMod.getContainer().getVersion(); + + ReplayMetaData metaData = new ReplayMetaData(); + metaData.setSingleplayer(singleplayer); + metaData.setServerName(worldName); + metaData.setGenerator(generator); + metaData.setDuration((int) lastSentPacket); + metaData.setDate(startTime); + metaData.setPlayers(pl); + metaData.setMcVersion(mcversion); + + replayFile.writeMetaData(metaData); + replayFile.writeMarkers(markers); + replayFile.writeResourcePackIndex(requestToHash); + replayFile.save(); + } catch(Exception e) { + e.printStackTrace(); + } finally { + ReplayMod.replayFileAppender.replayFileWritingFinished(); + } + } + + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java similarity index 92% rename from src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java rename to src/main/java/com/replaymod/recording/packet/PacketListener.java index 90cc3e9b..daae000b 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -1,15 +1,11 @@ -package eu.crushedpixel.replaymod.recording; +package com.replaymod.recording.packet; import com.google.common.hash.Hashing; import com.google.common.io.Files; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.chat.ChatMessageHandler; -import eu.crushedpixel.replaymod.holders.AdvancedPosition; -import eu.crushedpixel.replaymod.holders.Keyframe; -import eu.crushedpixel.replaymod.holders.Marker; +import de.johni0702.replaystudio.replay.ReplayFile; import eu.crushedpixel.replaymod.replay.Restrictions; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.ChannelHandlerContext; @@ -35,7 +31,6 @@ import org.apache.logging.log4j.Logger; import javax.annotation.Nonnull; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; import java.util.Map; @@ -50,11 +45,11 @@ public class PacketListener extends DataListener { private int nextRequestId; - public PacketListener(File file, String name, String worldName, long startTime, boolean singleplayer) throws FileNotFoundException { + public PacketListener(ReplayFile file, String name, String worldName, long startTime, boolean singleplayer) throws IOException { super(file, name, worldName, startTime, singleplayer); } - public void saveOnly(Packet packet) { + public void save(Packet packet) { try { if(packet instanceof S0CPacketSpawnPlayer) { UUID uuid = ((S0CPacketSpawnPlayer) packet).func_179819_c(); @@ -101,7 +96,7 @@ public class PacketListener extends DataListener { if (packet instanceof S48PacketResourcePackSend) { S48PacketResourcePackSend p = (S48PacketResourcePackSend) packet; int requestId = handleResourcePack(p); - saveOnly(new S48PacketResourcePackSend("replay://" + requestId, "")); + save(new S48PacketResourcePackSend("replay://" + requestId, "")); return; } @@ -216,17 +211,6 @@ public class PacketListener extends DataListener { return requestId; } - public void addMarker() { - AdvancedPosition pos = new AdvancedPosition(Minecraft.getMinecraft().getRenderViewEntity()); - int timestamp = (int) (System.currentTimeMillis() - startTime); - - Keyframe marker = new Keyframe(timestamp, new Marker(null, pos)); - - markers.add(marker); - - ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.addedmarker", ChatMessageHandler.ChatMessageType.INFORMATION); - } - private void downloadResourcePackFuture(int requestId, String url, final String hash) { Futures.addCallback(downloadResourcePack(requestId, url, hash), new FutureCallback() { @Override diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/PacketSerializer.java b/src/main/java/com/replaymod/recording/packet/PacketSerializer.java similarity index 96% rename from src/main/java/eu/crushedpixel/replaymod/recording/PacketSerializer.java rename to src/main/java/com/replaymod/recording/packet/PacketSerializer.java index e8379a7a..12b3f214 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/PacketSerializer.java +++ b/src/main/java/com/replaymod/recording/packet/PacketSerializer.java @@ -1,4 +1,4 @@ -package eu.crushedpixel.replaymod.recording; +package com.replaymod.recording.packet; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; diff --git a/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java b/src/main/java/com/replaymod/replay/CameraEntity.java similarity index 91% rename from src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java rename to src/main/java/com/replaymod/replay/CameraEntity.java index 5cff2b60..94f4dcfa 100755 --- a/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/CameraEntity.java @@ -1,14 +1,12 @@ -package eu.crushedpixel.replaymod.entities; +package com.replaymod.replay; import eu.crushedpixel.replaymod.holders.AdvancedPosition; import eu.crushedpixel.replaymod.replay.LesserDataWatcher; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import net.minecraft.block.material.Material; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayer; import net.minecraft.stats.StatFileWriter; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MathHelper; @@ -48,12 +46,12 @@ public class CameraEntity extends EntityPlayerSP { private Vec3 dirBefore; private double motion; + public float roll; + private long lastCall = 0; private boolean speedup = false; - public static UUID spectating; - public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatFileWriter statFileWriter) { super(mcIn, worldIn, netHandlerPlayClient, statFileWriter); } @@ -133,11 +131,11 @@ public class CameraEntity extends EntityPlayerSP { this.moveAbsolute(pos.getX(), pos.getY(), pos.getZ()); rotationPitch = prevRotationPitch = (float)pos.getPitch(); rotationYaw = prevRotationYaw = (float)pos.getYaw(); + roll = (float) pos.getRoll(); updateBoundingBox(); } public void moveAbsolute(double x, double y, double z) { - if(ReplayHandler.isInPath()) return; this.lastTickPosX = this.prevPosX = this.posX = x; this.lastTickPosY = this.prevPosY = this.posY = y; this.lastTickPosZ = this.prevPosZ = this.posZ = z; @@ -145,7 +143,6 @@ public class CameraEntity extends EntityPlayerSP { } public void moveRelative(double x, double y, double z) { - if(ReplayHandler.isInPath()) return; this.lastTickPosX = this.prevPosX = this.posX = this.posX + x; this.lastTickPosY = this.prevPosY = this.posY = this.posY + y; this.lastTickPosZ = this.prevPosZ = this.posZ = this.posZ + z; @@ -155,6 +152,7 @@ public class CameraEntity extends EntityPlayerSP { public void movePath(AdvancedPosition pos) { this.prevRotationPitch = this.rotationPitch = (float)pos.getPitch(); this.prevRotationYaw = this.rotationYaw = (float)pos.getYaw(); + this.roll = (float) pos.getRoll(); this.lastTickPosX = this.prevPosX = this.posX = pos.getX(); this.lastTickPosY = this.prevPosY = this.posY = pos.getY(); this.lastTickPosZ = this.prevPosZ = this.posZ = pos.getZ(); @@ -166,7 +164,7 @@ public class CameraEntity extends EntityPlayerSP { posX + width / 2, posY + height, posZ + width / 2)); } - private void updatePos(Entity to) { + protected void updatePos(Entity to) { prevPosX = to.prevPosX; prevPosY = to.prevPosY; prevPosZ = to.prevPosZ; @@ -188,9 +186,6 @@ public class CameraEntity extends EntityPlayerSP { @Override public void setAngles(float yaw, float pitch) { - if (ReplayHandler.isInPath()) { - return; // Disallow camera movement by mouse during path preview - } this.rotationYaw = (float) ((double) this.rotationYaw + (double) yaw * 0.15D); this.rotationPitch = (float) ((double) this.rotationPitch - (double) pitch * 0.15D); this.rotationPitch = MathHelper.clamp_float(this.rotationPitch, -90.0F, 90.0F); @@ -202,6 +197,7 @@ public class CameraEntity extends EntityPlayerSP { public void onUpdate() { Entity view = mc.getRenderViewEntity(); if (view != null) { + UUID spectating = ReplayModReplay.instance.getReplayHandler().spectating; if (spectating != null && (view.getUniqueID() != spectating || view.worldObj != worldObj)) { view = worldObj.getPlayerEntityByUUID(spectating); if (view != null) { @@ -263,20 +259,6 @@ public class CameraEntity extends EntityPlayerSP { return true; } - public void spectate(Entity e) { - if (e == null || e == this) { - spectating = null; - e = this; - } else if (e instanceof EntityPlayer) { - spectating = e.getUniqueID(); - } - - if (mc.getRenderViewEntity() != e) { - mc.setRenderViewEntity(e); - updatePos(e); - } - } - public enum MoveDirection { UP, DOWN, LEFT, RIGHT, FORWARD, BACKWARD } diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java new file mode 100755 index 00000000..09fa3484 --- /dev/null +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -0,0 +1,349 @@ +package com.replaymod.replay; + +import com.google.common.base.Preconditions; +import com.mojang.authlib.GameProfile; +import com.replaymod.core.ReplayMod; +import com.replaymod.replay.events.ReplayCloseEvent; +import com.replaymod.replay.events.ReplayOpenEvent; +import com.replaymod.replay.gui.overlay.GuiReplayOverlay; +import de.johni0702.replaystudio.data.Marker; +import de.johni0702.replaystudio.replay.ReplayFile; +import eu.crushedpixel.replaymod.holders.AdvancedPosition; +import eu.crushedpixel.replaymod.registry.ReplayGuiRegistry; +import eu.crushedpixel.replaymod.replay.Restrictions; +import eu.crushedpixel.replaymod.utils.MouseUtils; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityOtherPlayerMP; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.client.resources.I18n; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.NetworkManager; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; +import org.lwjgl.opengl.Display; +import org.lwjgl.util.Point; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static net.minecraft.client.renderer.GlStateManager.*; +import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; + +public class ReplayHandler { + + private static Minecraft mc = Minecraft.getMinecraft(); + + /** + * The file currently being played. + */ + private final ReplayFile replayFile; + + /** + * Decodes and sends packets into channel. + */ + private final ReplaySender replaySender; + + /** + * Currently active replay restrictions. + */ + private Restrictions restrictions = new Restrictions(); + + /** + * Whether camera movements by user input and/or server packets should be suppressed. + */ + private boolean suppressCameraMovements; + + private final Set markers; + + private final GuiReplayOverlay overlay; + + private EmbeddedChannel channel; + + /** + * The position at which the camera should be located after the next jump. + */ + private AdvancedPosition targetCameraPosition; + + protected UUID spectating; + + public ReplayHandler(ReplayFile replayFile, boolean asyncMode) throws IOException { + Preconditions.checkState(mc.isCallingFromMinecraftThread(), "Must be called from Minecraft thread."); + this.replayFile = replayFile; + + FMLCommonHandler.instance().bus().post(new ReplayOpenEvent.Pre(this)); + + markers = replayFile.getMarkers().or(Collections.emptySet()); + + // TODO: get rid of chat message handler + ReplayMod.chatMessageHandler.initialize(); + + replaySender = new ReplaySender(this, replayFile, asyncMode); + + setup(); + + overlay = new GuiReplayOverlay(this); + overlay.setVisible(true); + + FMLCommonHandler.instance().bus().post(new ReplayOpenEvent.Post(this)); + } + + void restartedReplay() { + channel.close(); + + restrictions = new Restrictions(); + + setup(); + } + + public void endReplay() throws IOException { + Preconditions.checkState(mc.isCallingFromMinecraftThread(), "Must be called from Minecraft thread."); + + FMLCommonHandler.instance().bus().post(new ReplayCloseEvent.Pre(this)); + + replaySender.terminateReplay(); + + replayFile.save(); + replayFile.close(); + + channel.close().awaitUninterruptibly(); + + if (mc.theWorld != null) { + mc.theWorld.sendQuittingDisconnectingPacket(); + mc.loadWorld(null); + } + + overlay.setVisible(false); + + // These might have been hidden by the overlay, so we have to make sure they're visible again + ReplayGuiRegistry.show(); + + FMLCommonHandler.instance().bus().post(new ReplayCloseEvent.Post(this)); + } + + private void setup() { + mc.ingameGUI.getChatGUI().clearChatMessages(); + + NetworkManager networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) { + t.printStackTrace(); + } + }; + networkManager.setNetHandler(new NetHandlerPlayClient( + mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player"))); + + channel = new EmbeddedChannel(networkManager); + channel.attr(NetworkDispatcher.FML_DISPATCHER).set(new NetworkDispatcher(networkManager)); + + channel.pipeline().addFirst(replaySender); + channel.pipeline().fireChannelActive(); + } + + public ReplayFile getReplayFile() { + return replayFile; + } + + public Restrictions getRestrictions() { + return restrictions; + } + + public ReplaySender getReplaySender() { + return replaySender; + } + + public GuiReplayOverlay getOverlay() { + return overlay; + } + + /** + * Return whether camera movement by user inputs and/or server packets should be suppressed. + * @return {@code true} if these kinds of movement should be suppressed + */ + public boolean shouldSuppressCameraMovements() { + return suppressCameraMovements; + } + + /** + * Set whether camera movement by user inputs and/or server packets should be suppressed. + * @param suppressCameraMovements {@code true} to suppress these kinds of movement, {@code false} to allow them + */ + public void setSuppressCameraMovements(boolean suppressCameraMovements) { + this.suppressCameraMovements = suppressCameraMovements; + } + + /** + * Returns all markers. + * When changed, {@link #saveMarkers()} should be called to save the changes. + * @return Set of markers + */ + public Set getMarkers() { + return markers; + } + + /** + * Saves all markers. + */ + public void saveMarkers() { + try { + replayFile.writeMarkers(markers); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Spectate the specified entity. + * When the entity is {@code null} or the camera entity, the camera becomes the view entity. + * @param e The entity to spectate + */ + public void spectateEntity(Entity e) { + CameraEntity cameraEntity = getCameraEntity(); + if (e == null || e == cameraEntity) { + spectating = null; + e = cameraEntity; + } else if (e instanceof EntityPlayer) { + spectating = e.getUniqueID(); + } + + if (mc.getRenderViewEntity() != e) { + mc.setRenderViewEntity(e); + cameraEntity.updatePos(e); + } + } + + /** + * Set the camera as the view entity. + * This is equivalent to {@link #spectateEntity(Entity) spectateEntity(null)}. + */ + public void spectateCamera() { + spectateEntity(null); + } + + /** + * Returns whether the current view entity is the camera entity. + * @return {@code true} if the camera is the view entity, {@code false} otherwise + */ + public boolean isCameraView() { + return mc.thePlayer instanceof CameraEntity && mc.thePlayer == mc.getRenderViewEntity(); + } + + /** + * Returns the camera entity. + * @return The camera entity or {@code null} if it does not yet exist + */ + public CameraEntity getCameraEntity() { + return mc.thePlayer instanceof CameraEntity ? (CameraEntity) mc.thePlayer : null; + } + + public void setTargetPosition(AdvancedPosition pos) { + targetCameraPosition = pos; + } + + public void moveCameraToTargetPosition() { + CameraEntity cam = getCameraEntity(); + if (cam != null && targetCameraPosition != null) { + cam.moveAbsolute(targetCameraPosition); + } + } + + public void doJump(int targetTime, boolean retainCameraPosition) { + if (replaySender.isHurrying()) { + return; // When hurrying, no Timeline jumping etc. is possible + } + + if (targetTime < replaySender.currentTimeStamp()) { + mc.displayGuiScreen(null); + } + + if (retainCameraPosition) { + CameraEntity cam = getCameraEntity(); + if (cam != null) { + targetCameraPosition = new AdvancedPosition(cam.posX, cam.posY, cam.posZ, + cam.rotationPitch, cam.rotationYaw, cam.roll); + } else { + targetCameraPosition = null; + } + } + + long diff = targetTime - replaySender.getDesiredTimestamp(); + if (diff != 0) { + if (diff > 0 && diff < 5000) { // Small difference and no time travel + replaySender.jumpToTime(targetTime); + } else { // We either have to restart the replay or send a significant amount of packets + // Render our please-wait-screen + GuiScreen guiScreen = new GuiScreen() { + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + drawBackground(0); + drawCenteredString(fontRendererObj, I18n.format("replaymod.gui.pleasewait"), + width / 2, height / 2, 0xffffffff); + } + }; + + // Make sure that the replaysender changes into sync mode + replaySender.setSyncModeAndWait(); + + // Perform the rendering using OpenGL + pushMatrix(); + clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + enableTexture2D(); + mc.getFramebuffer().bindFramebuffer(true); + mc.entityRenderer.setupOverlayRendering(); + + Point point = MouseUtils.getScaledDimensions(); + guiScreen.setWorldAndResolution(mc, point.getX(), point.getY()); + guiScreen.drawScreen(0, 0, 0); + + mc.getFramebuffer().unbindFramebuffer(); + popMatrix(); + pushMatrix(); + mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); + popMatrix(); + + Display.update(); + + // Send the packets + ReplayMod.replaySender.sendPacketsTill(targetTime); + ReplayMod.replaySender.setAsyncMode(true); + ReplayMod.replaySender.setReplaySpeed(0); + + mc.getNetHandler().getNetworkManager().processReceivedPackets(); + @SuppressWarnings("unchecked") + List entities = (List) mc.theWorld.loadedEntityList; + for (Entity entity : entities) { + if (entity instanceof EntityOtherPlayerMP) { + EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity; + e.setPosition(e.otherPlayerMPX, e.otherPlayerMPY, e.otherPlayerMPZ); + e.rotationYaw = (float) e.otherPlayerMPYaw; + e.rotationPitch = (float) e.otherPlayerMPPitch; + } + entity.lastTickPosX = entity.prevPosX = entity.posX; + entity.lastTickPosY = entity.prevPosY = entity.posY; + entity.lastTickPosZ = entity.prevPosZ = entity.posZ; + entity.prevRotationYaw = entity.rotationYaw; + entity.prevRotationPitch = entity.rotationPitch; + } + try { + mc.runTick(); + } catch (IOException e) { + e.printStackTrace(); // This should never be thrown but whatever + } + + //finally, updating the camera's position (which is not done by the sync jumping) + moveCameraToTargetPosition(); + + // No need to remove our please-wait-screen. It'll vanish with the next + // render pass as it's never been a real GuiScreen in the first place. + } + } + } +} diff --git a/src/main/java/com/replaymod/replay/ReplayModReplay.java b/src/main/java/com/replaymod/replay/ReplayModReplay.java new file mode 100644 index 00000000..599a9c28 --- /dev/null +++ b/src/main/java/com/replaymod/replay/ReplayModReplay.java @@ -0,0 +1,58 @@ +package com.replaymod.replay; + +import com.replaymod.core.ReplayMod; +import com.replaymod.replay.handler.GuiHandler; +import de.johni0702.replaystudio.replay.ReplayFile; +import de.johni0702.replaystudio.replay.ZipReplayFile; +import de.johni0702.replaystudio.studio.ReplayStudio; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import org.apache.logging.log4j.Logger; +import org.lwjgl.input.Keyboard; + +import java.io.File; +import java.io.IOException; + +@Mod(modid = ReplayModReplay.MOD_ID, useMetadata = true) +public class ReplayModReplay { + public static final String MOD_ID = "replaymod-replay"; + + @Mod.Instance(MOD_ID) + public static ReplayModReplay instance; + + @Mod.Instance(ReplayMod.MOD_ID) + private static ReplayMod core; + + private Logger logger; + + private ReplayHandler replayHandler; + + public ReplayHandler getReplayHandler() { + return replayHandler; + } + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + logger = event.getModLog(); + + core.getSettingsRegistry().register(Setting.class); + + core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() { + @Override + public void run() { + + } + }); + } + + @Mod.EventHandler + public void init(FMLInitializationEvent event) { + new GuiHandler(this).register(); + } + + public void startReplay(File file) throws IOException { + ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file); + replayHandler = new ReplayHandler(replayFile, true); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java b/src/main/java/com/replaymod/replay/ReplaySender.java similarity index 95% rename from src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java rename to src/main/java/com/replaymod/replay/ReplaySender.java index a001ba51..321b2aec 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java +++ b/src/main/java/com/replaymod/replay/ReplaySender.java @@ -1,13 +1,13 @@ -package eu.crushedpixel.replaymod.replay; +package com.replaymod.replay; import com.google.common.base.Preconditions; import com.google.common.io.Files; import com.google.common.util.concurrent.ListenableFutureTask; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.entities.CameraEntity; +import de.johni0702.replaystudio.replay.ReplayFile; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.holders.PacketData; +import eu.crushedpixel.replaymod.replay.Restrictions; import eu.crushedpixel.replaymod.settings.ReplaySettings; -import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; @@ -67,6 +67,11 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { S39PacketPlayerAbilities.class, S45PacketTitle.class); + /** + * The replay handler responsible for the current replay. + */ + private final ReplayHandler replayHandler; + /** * Whether to work in async mode. * @@ -156,10 +161,11 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { * @param asyncMode {@code true} for async mode, {@code false} otherwise * @see #asyncMode */ - public ReplaySender(ReplayFile file, boolean asyncMode) { + public ReplaySender(ReplayHandler replayHandler, ReplayFile file, boolean asyncMode) throws IOException { + this.replayHandler = replayHandler; this.replayFile = file; this.asyncMode = asyncMode; - this.replayLength = file.metadata().get().getDuration(); + this.replayLength = file.getMetaData().getDuration(); if (asyncMode) { new Thread(asyncSender, "replaymod-async-sender").start(); @@ -296,7 +302,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { if (p instanceof S3FPacketCustomPayload) { S3FPacketCustomPayload packet = (S3FPacketCustomPayload) p; if (Restrictions.PLUGIN_CHANNEL.equals(packet.getChannelName())) { - final String unknown = ReplayHandler.getRestrictions().handle(packet); + final String unknown = replayHandler.getRestrictions().handle(packet); if (unknown == null) { return null; } else { @@ -306,7 +312,11 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { mc.addScheduledTask(new Runnable() { @Override public void run() { - ReplayHandler.endReplay(); + try { + replayHandler.endReplay(); + } catch (IOException e) { + e.printStackTrace(); + } mc.displayGuiScreen(new GuiErrorScreen( I18n.format("replaymod.error.unknownrestriction1"), I18n.format("replaymod.error.unknownrestriction2", unknown) @@ -327,12 +337,6 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { if(BAD_PACKETS.contains(p.getClass())) return null; - if(ReplayProcess.isVideoRecording() && ReplayHandler.isInPath()) { - if(p instanceof S29PacketSoundEffect) { - return null; - } - } - convertLegacyEntityIds(p); if(p instanceof S48PacketResourcePackSend) { @@ -340,13 +344,13 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { String url = packet.func_179783_a(); if (url.startsWith("replay://")) { int id = Integer.parseInt(url.substring("replay://".length())); - Map index = replayFile.resourcePackIndex().get(); + Map index = replayFile.getResourcePackIndex(); if (index != null) { String hash = index.get(id); if (hash != null) { File file = new File(tempResourcePackFolder, hash + ".zip"); if (!file.exists()) { - IOUtils.copy(replayFile.resourcePack(hash).get(), new FileOutputStream(file)); + IOUtils.copy(replayFile.getResourcePack(hash).get(), new FileOutputStream(file)); } mc.getResourcePackRepository().func_177319_a(file); } @@ -387,9 +391,9 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { mc.displayGuiScreen(null); } - if(ReplayHandler.isInPath()) return null; + if(replayHandler.shouldSuppressCameraMovements()) return null; - CameraEntity cent = ReplayHandler.getCameraEntity(); + CameraEntity cent = replayHandler.getCameraEntity(); for (Object relative : ppl.func_179834_f()) { if (relative == S08PacketPlayerPosLook.EnumFlags.X @@ -419,7 +423,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { return null; } - CameraEntity cent = ReplayHandler.getCameraEntity(); + CameraEntity cent = replayHandler.getCameraEntity(); cent.moveAbsolute(ppl.func_148932_c(), ppl.func_148928_d(), ppl.func_148933_e()); return null; } @@ -461,7 +465,6 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - replayFile.close(); FileUtils.deleteDirectory(tempResourcePackFolder); super.channelInactive(ctx); } @@ -522,7 +525,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { while (!terminate) { synchronized (ReplaySender.this) { if (dis == null) { - dis = new DataInputStream(replayFile.recording().get()); + dis = new DataInputStream(replayFile.getPacketData()); } // Packet loop while (true) { @@ -580,7 +583,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { if (isHurrying() && lastTimeStamp > desiredTimeStamp && !startFromBeginning) { desiredTimeStamp = -1; - ReplayHandler.moveCameraToLastPosition(); + replayHandler.moveCameraToTargetPosition(); // Pause after jumping setReplaySpeed(0); @@ -605,7 +608,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { startFromBeginning = false; nextPacket = null; lastPacketSent = System.currentTimeMillis(); - ReplayHandler.restartReplay(); + replayHandler.restartedReplay(); if (dis != null) { dis.close(); dis = null; @@ -659,8 +662,8 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { } protected Packet processPacketAsync(Packet p) { - //If hurrying, ignore some packets, unless during Replay Path and *not* in short hurries - if(!ReplayHandler.isInPath() && desiredTimeStamp - lastTimeStamp > 1000) { + //If hurrying, ignore some packets, except for short durations + if(desiredTimeStamp - lastTimeStamp > 1000) { if(p instanceof S2APacketParticles) return null; if(p instanceof S0EPacketSpawnObject) { @@ -700,11 +703,11 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { } startFromBeginning = false; nextPacket = null; - ReplayHandler.restartReplay(); + replayHandler.restartedReplay(); } if (dis == null) { - dis = new DataInputStream(replayFile.recording().get()); + dis = new DataInputStream(replayFile.getPacketData()); } while (true) { // Send packets diff --git a/src/main/java/com/replaymod/replay/Setting.java b/src/main/java/com/replaymod/replay/Setting.java new file mode 100644 index 00000000..7adf2527 --- /dev/null +++ b/src/main/java/com/replaymod/replay/Setting.java @@ -0,0 +1,17 @@ +package com.replaymod.replay; + +import com.replaymod.core.SettingsRegistry; + +public final class Setting extends SettingsRegistry.SettingKeys { + public static final Setting RECORD_SINGLEPLAYER = make("recordSingleplayer", "recordsingleplayer", true); + public static final Setting RECORD_SERVER = make("recordServer", "recordserver", true); + public static final Setting INDICATOR = make("indicator", "indicator", true); + + private static Setting make(String key, String displayName, T defaultValue) { + return new Setting<>(key, displayName, defaultValue); + } + + public Setting(String key, String displayString, T defaultValue) { + super("recording", key, "replaymod.gui.settings." + displayString, defaultValue); + } +} diff --git a/src/main/java/com/replaymod/replay/events/ReplayCloseEvent.java b/src/main/java/com/replaymod/replay/events/ReplayCloseEvent.java new file mode 100644 index 00000000..f01720e6 --- /dev/null +++ b/src/main/java/com/replaymod/replay/events/ReplayCloseEvent.java @@ -0,0 +1,24 @@ +package com.replaymod.replay.events; + +import com.replaymod.replay.ReplayHandler; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.minecraftforge.fml.common.eventhandler.Event; + +@RequiredArgsConstructor +public abstract class ReplayCloseEvent extends Event { + @Getter + private final ReplayHandler replayHandler; + + public static class Pre extends ReplayCloseEvent { + public Pre(ReplayHandler replayHandler) { + super(replayHandler); + } + } + + public static class Post extends ReplayCloseEvent { + public Post(ReplayHandler replayHandler) { + super(replayHandler); + } + } +} diff --git a/src/main/java/com/replaymod/replay/events/ReplayOpenEvent.java b/src/main/java/com/replaymod/replay/events/ReplayOpenEvent.java new file mode 100644 index 00000000..ad970a37 --- /dev/null +++ b/src/main/java/com/replaymod/replay/events/ReplayOpenEvent.java @@ -0,0 +1,24 @@ +package com.replaymod.replay.events; + +import com.replaymod.replay.ReplayHandler; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.minecraftforge.fml.common.eventhandler.Event; + +@RequiredArgsConstructor +public abstract class ReplayOpenEvent extends Event { + @Getter + private final ReplayHandler replayHandler; + + public static class Pre extends ReplayOpenEvent { + public Pre(ReplayHandler replayHandler) { + super(replayHandler); + } + } + + public static class Post extends ReplayOpenEvent { + public Post(ReplayHandler replayHandler) { + super(replayHandler); + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiMarkerTimeline.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java similarity index 56% rename from src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiMarkerTimeline.java rename to src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java index 6ae4e025..d12ecc3c 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiMarkerTimeline.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java @@ -1,10 +1,10 @@ -package eu.crushedpixel.replaymod.gui.elements.timelines; +package com.replaymod.replay.gui.overlay; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.gui.GuiEditKeyframe; -import eu.crushedpixel.replaymod.holders.Keyframe; -import eu.crushedpixel.replaymod.holders.Marker; -import eu.crushedpixel.replaymod.replay.ReplayHandler; +import de.johni0702.replaystudio.data.Marker; +import com.replaymod.core.ReplayMod; +import eu.crushedpixel.replaymod.gui.elements.timelines.GuiTimeline; +import eu.crushedpixel.replaymod.holders.AdvancedPosition; +import com.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.MouseUtils; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.I18n; @@ -16,13 +16,16 @@ public class GuiMarkerTimeline extends GuiTimeline { private static final int KEYFRAME_MARKER_X = 109; private static final int KEYFRAME_MARKER_Y = 20; - private Keyframe clickedKeyFrame; + private final ReplayHandler replayHandler; + private Marker selectedMarker; + private Marker clickedMarker; private long clickTime; private boolean dragging; - public GuiMarkerTimeline(int positionX, int positionY, int width, int height, boolean showMarkers) { + public GuiMarkerTimeline(ReplayHandler replayHandler, int positionX, int positionY, int width, int height) { super(positionX, positionY, width, height); - this.showMarkers = showMarkers; + this.replayHandler = replayHandler; + this.showMarkers = false; } @Override @@ -36,30 +39,33 @@ public class GuiMarkerTimeline extends GuiTimeline { int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); - Keyframe closest = null; + Marker closest = null; if(mouseY >= positionY + BORDER_TOP + 10) { - closest = ReplayHandler.getMarkerKeyframes().getClosestKeyframeForTimestamp((int) time, tolerance); + long distance = tolerance; + for (Marker m : replayHandler.getMarkers()) { + long d = Math.abs(m.getTime() - time); + if (d <= distance) { + closest = m; + distance = d; + } + } } //left mouse button if(button == 0) { - ReplayHandler.selectKeyframe(closest); - if(closest == null) { //if no keyframe clicked, jump in time - ReplayMod.overlay.performJump(getTimeAt(mouseX, mouseY)); + replayHandler.doJump((int) getTimeAt(mouseX, mouseY), true); } else { + selectedMarker = closest; // If we clicked on a key frame, then continue monitoring the mouse for movements long currentTime = System.currentTimeMillis(); - if(closest != null) { - if(currentTime - clickTime < 500) { // if double clicked then open GUI instead - mc.displayGuiScreen(GuiEditKeyframe.create(closest)); - this.clickedKeyFrame = null; - } else { - this.clickedKeyFrame = closest; - this.dragging = false; - } - } else { // If we didn't then just update the cursor - this.dragging = true; + if(currentTime - clickTime < 500) { // if double clicked then open GUI instead +// mc.displayGuiScreen(GuiEditKeyframe.create(closest)); + // TODO Edit Gui + this.clickedMarker = null; + } else { + this.clickedMarker = closest; + this.dragging = false; } this.clickTime = currentTime; } @@ -67,10 +73,13 @@ public class GuiMarkerTimeline extends GuiTimeline { } else if(button == 1) { if(closest != null) { //Jump to clicked Marker Keyframe (explicitly force to jump to this position) - ReplayHandler.setLastPosition(closest.getValue().getPosition(), true); + replayHandler.setTargetPosition(new AdvancedPosition( + closest.getX(), closest.getY(), closest.getZ(), + closest.getPitch(), closest.getYaw(), closest.getRoll() + )); //perform the jump, telling the Overlay not to override the last position value - ReplayMod.overlay.performJump(closest.getRealTimestamp(), false); + replayHandler.doJump(closest.getTime(), false); } } @@ -82,11 +91,11 @@ public class GuiMarkerTimeline extends GuiTimeline { if(!enabled) return; long time = getTimeAt(mouseX, mouseY); if (time != -1) { - if (clickedKeyFrame != null) { + if (clickedMarker != null) { int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); - if (dragging || Math.abs(clickedKeyFrame.getRealTimestamp() - time) > tolerance) { - clickedKeyFrame.setRealTimestamp((int) time); + if (dragging || Math.abs(clickedMarker.getTime() - time) > tolerance) { + clickedMarker.setTime((int) time); dragging = true; } } @@ -96,8 +105,11 @@ public class GuiMarkerTimeline extends GuiTimeline { @Override public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) { mouseDrag(mc, mouseX, mouseY, button); - clickedKeyFrame = null; - dragging = false; + clickedMarker = null; + if (dragging) { + replayHandler.saveMarkers(); + dragging = false; + } } @Override @@ -114,17 +126,12 @@ public class GuiMarkerTimeline extends GuiTimeline { drawTimelineCursor(leftTime, rightTime, bodyWidth); //Draw Keyframe logos - for(Keyframe kf : ReplayHandler.getMarkerKeyframes()) { - if (kf != null && !kf.equals(ReplayHandler.getSelectedKeyframe())) - drawKeyframe(kf, bodyWidth, leftTime, rightTime, segmentLength); - } - - if(ReplayHandler.getSelectedKeyframe() != null && ReplayHandler.getSelectedKeyframe().getValue() instanceof Marker) { - drawKeyframe(ReplayHandler.getSelectedKeyframe(), bodyWidth, leftTime, rightTime, segmentLength); + for (Marker marker : replayHandler.getMarkers()) { + drawMarker(marker, bodyWidth, leftTime, rightTime, segmentLength); } } - private int getKeyframeX(int timestamp, long leftTime, int bodyWidth, double segmentLength) { + private int getMarkerX(int timestamp, long leftTime, int bodyWidth, double segmentLength) { long positionInSegment = timestamp - leftTime; double fractionOfSegment = positionInSegment / segmentLength; return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth); @@ -139,12 +146,12 @@ public class GuiMarkerTimeline extends GuiTimeline { long leftTime = Math.round(timeStart * timelineLength); double segmentLength = timelineLength * zoom; - for(Keyframe marker : ReplayHandler.getMarkerKeyframes()) { - int keyframeX = getKeyframeX(marker.getRealTimestamp(), leftTime, bodyWidth, segmentLength); + for (Marker marker : replayHandler.getMarkers()) { + int markerX = getMarkerX(marker.getTime(), leftTime, bodyWidth, segmentLength); - if(MouseUtils.isMouseWithinBounds(keyframeX - 2, this.positionY + BORDER_TOP + 10 + 1, 5, 5)) { + if(MouseUtils.isMouseWithinBounds(markerX - 2, this.positionY + BORDER_TOP + 10 + 1, 5, 5)) { Point mouse = MouseUtils.getMousePos(); - String markerName = marker.getValue().getName(); + String markerName = marker.getName(); if(markerName == null || markerName.isEmpty()) markerName = I18n.format("replaymod.gui.ingame.unnamedmarker"); ReplayMod.tooltipRenderer.drawTooltip(mouse.getX(), mouse.getY(), markerName, null, Color.WHITE); @@ -157,15 +164,15 @@ public class GuiMarkerTimeline extends GuiTimeline { } } - private void drawKeyframe(Keyframe kf, int bodyWidth, long leftTime, long rightTime, double segmentLength) { - if (kf.getRealTimestamp() <= rightTime && kf.getRealTimestamp() >= leftTime) { + private void drawMarker(Marker marker, int bodyWidth, long leftTime, long rightTime, double segmentLength) { + if (marker.getTime() <= rightTime && marker.getTime() >= leftTime) { int textureX = KEYFRAME_MARKER_X; int textureY = KEYFRAME_MARKER_Y; int y = positionY+10; - int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); + int keyframeX = getMarkerX(marker.getTime(), leftTime, bodyWidth, segmentLength); - if (ReplayHandler.isSelected(kf)) { + if (selectedMarker == marker) { textureX += 5; } diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java new file mode 100644 index 00000000..a6bfaddf --- /dev/null +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java @@ -0,0 +1,140 @@ +package com.replaymod.replay.gui.overlay; + +import com.replaymod.core.ReplayMod; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplaySender; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.AbstractGuiOverlay; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiSlider; +import de.johni0702.minecraft.gui.element.GuiTexturedButton; +import de.johni0702.minecraft.gui.element.advanced.GuiTimeline; +import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline; +import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import net.minecraft.client.resources.I18n; +import net.minecraft.client.settings.GameSettings; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.InputEvent; +import org.lwjgl.util.ReadableDimension; +import org.lwjgl.util.ReadablePoint; +import org.lwjgl.util.WritablePoint; + +import static com.replaymod.core.ReplayMod.TEXTURE_SIZE; + +public class GuiReplayOverlay extends AbstractGuiOverlay { + + private final ReplayHandler replayHandler; + public final GuiPanel topPanel = new GuiPanel(this) + .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.LEFT).setSpacing(5)); + public final GuiTexturedButton playPauseButton = new GuiTexturedButton().setSize(20, 20) + .setTexture(ReplayMod.TEXTURE, TEXTURE_SIZE); + public final GuiSlider speedSlider = new GuiSlider().setSize(100, 20).setSteps(37); // 0.0 is not included + public final GuiTimeline timeline = new GuiTimeline(){ + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + setCursorPosition(replayHandler.getReplaySender().currentTimeStamp()); + super.draw(renderer, size, renderInfo); + } + }.setSize(Integer.MAX_VALUE, 20); + + public GuiReplayOverlay(final ReplayHandler replayHandler) { + this.replayHandler = replayHandler; + + topPanel.addElements(null, playPauseButton, speedSlider, timeline); + setLayout(new CustomLayout() { + @Override + protected void layout(GuiReplayOverlay container, int width, int height) { + pos(topPanel, 10, 10); + size(topPanel, width - 20, 20); + } + }); + + playPauseButton.setTexturePosH(new ReadablePoint() { + @Override + public int getX() { + return 0; + } + + @Override + public int getY() { + return replayHandler.getReplaySender().paused() ? 0 : 20; + } + + @Override + public void getLocation(WritablePoint dest) { + dest.setLocation(getX(), getY()); + } + }).onClick(new Runnable() { + @Override + public void run() { + ReplaySender replaySender = replayHandler.getReplaySender(); + // If currently paused + if (replaySender.paused()) { + // then play + replaySender.setReplaySpeed(getSpeed()); + } else { + // else pause + replaySender.setReplaySpeed(0); + } + } + }); + + speedSlider.onValueChanged(new Runnable() { + @Override + public void run() { + double speed = getSpeed(); + speedSlider.setText(I18n.format("replaymod.gui.speed") + ": " + speed + "x"); + ReplaySender replaySender = replayHandler.getReplaySender(); + if (!replaySender.paused()) { + replaySender.setReplaySpeed(speed); + } + } + }).setValue(9); + + timeline.onClick(new IGuiTimeline.OnClick() { + @Override + public void run(int time) { + replayHandler.doJump(time, true); + } + }).setLength(replayHandler.getReplaySender().replayLength()); + } + + private double getSpeed() { + int value = speedSlider.getValue() + 1; + if (value <= 9) { + return value / 10d; + } else { + return 1 + (0.25d * (value - 10)); + } + } + + @Override + public void setVisible(boolean visible) { + if (isVisible() != visible) { + if (visible) { + FMLCommonHandler.instance().bus().register(this); + } else { + FMLCommonHandler.instance().bus().unregister(this); + } + } + super.setVisible(visible); + } + + @SubscribeEvent + public void onKeyInput(InputEvent.KeyInputEvent event) { + GameSettings gameSettings = getMinecraft().gameSettings; + while (gameSettings.keyBindChat.isPressed() || gameSettings.keyBindCommand.isPressed()) { + if (!isMouseVisible()) { + setMouseVisible(true); + } + } + } + + @Override + protected GuiReplayOverlay getThis() { + return this; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiRenameReplay.java b/src/main/java/com/replaymod/replay/gui/screen/GuiRenameReplay.java similarity index 94% rename from src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiRenameReplay.java rename to src/main/java/com/replaymod/replay/gui/screen/GuiRenameReplay.java index 787605fd..c580200f 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiRenameReplay.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiRenameReplay.java @@ -1,6 +1,5 @@ -package eu.crushedpixel.replaymod.gui.replayviewer; +package com.replaymod.replay.gui.screen; -import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiErrorScreen; @@ -57,7 +56,7 @@ public class GuiRenameReplay extends GuiScreen { } else if(button.id == 0) { File folder = ReplayFileIO.getReplayFolder(); - File initRenamed = new File(folder, (this.replayNameInput.getText().trim() + ReplayFile.ZIP_FILE_EXTENSION).replaceAll("[^a-zA-Z0-9\\.\\- ]", "_")); + File initRenamed = new File(folder, (this.replayNameInput.getText().trim() + ".zip").replaceAll("[^a-zA-Z0-9\\.\\- ]", "_")); File renamed = ReplayFileIO.getNextFreeFile(initRenamed); try { FileUtils.moveFile(file, renamed); diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiReplayViewer.java b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java similarity index 91% rename from src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiReplayViewer.java rename to src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java index 6be4fd91..e41770d3 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -1,18 +1,21 @@ -package eu.crushedpixel.replaymod.gui.replayviewer; +package com.replaymod.replay.gui.screen; +import com.google.common.base.Optional; import com.mojang.realmsclient.util.Pair; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; +import com.replaymod.core.gui.GuiReplaySettings; +import com.replaymod.replay.ReplayModReplay; +import de.johni0702.replaystudio.replay.ReplayFile; +import de.johni0702.replaystudio.replay.ReplayMetaData; +import de.johni0702.replaystudio.replay.ZipReplayFile; +import de.johni0702.replaystudio.studio.ReplayStudio; import eu.crushedpixel.replaymod.api.replay.holders.FileInfo; -import eu.crushedpixel.replaymod.gui.GuiReplaySettings; import eu.crushedpixel.replaymod.gui.elements.GuiLoadingListEntry; import eu.crushedpixel.replaymod.gui.elements.GuiReplayListEntry; import eu.crushedpixel.replaymod.gui.elements.GuiReplayListExtended; import eu.crushedpixel.replaymod.gui.online.GuiUploadFile; -import eu.crushedpixel.replaymod.recording.ReplayMetaData; import eu.crushedpixel.replaymod.registry.ResourceHelper; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.ImageUtils; -import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; @@ -34,6 +37,7 @@ import java.io.File; import java.io.IOException; import java.util.*; import java.util.List; +import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback { @@ -45,6 +49,7 @@ public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback { private static final int DELETE_BUTTON_ID = 9005; private static final int SETTINGS_BUTTON_ID = 9006; private static final int CANCEL_BUTTON_ID = 9007; + private final ReplayModReplay mod; private boolean initialized; private GuiReplayListExtended replayGuiList; private List, File>> replayFileList = new ArrayList, File>>(); @@ -58,6 +63,10 @@ public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback { private Thread fileReloader; + public GuiReplayViewer(ReplayModReplay mod) { + this.mod = mod; + } + private class FileReloaderThread extends Thread { private final GuiLoadingListEntry loadingListEntry = new GuiLoadingListEntry(); @@ -74,26 +83,26 @@ public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback { for(final File file : ReplayFileIO.getAllReplayFiles()) { if(interrupted()) break; try { - ReplayFile replayFile = new ReplayFile(file); - final ReplayMetaData metaData = replayFile.metadata().get(); - BufferedImage img = replayFile.thumb().get(); + ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file); + final ReplayMetaData metaData = replayFile.getMetaData(); + Optional thumb = replayFile.getThumb(); replayFile.close(); File tmp = null; - if(img != null) { - img = ImageUtils.scaleImage(img, new Dimension(1280, 720)); + if(thumb.isPresent()) { + BufferedImage img = ImageUtils.scaleImage(thumb.get(), new Dimension(1280, 720)); tmp = File.createTempFile(FilenameUtils.getBaseName(file.getAbsolutePath())+"_THUMBNAIL", "jpg"); tmp.deleteOnExit(); ImageIO.write(img, "jpg", tmp); } - final File thumb = tmp; + final File thumbFile = tmp; loadedReplaysQueue.offer(new Runnable() { @Override public void run() { - addEntry(file, metaData, thumb); + addEntry(file, metaData, thumbFile); } }); } catch(Exception e) { @@ -213,7 +222,7 @@ public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback { this.mc.displayGuiScreen(guiyesno); } } else if(button.id == SETTINGS_BUTTON_ID) { - this.mc.displayGuiScreen(new GuiReplaySettings(this)); + new GuiReplaySettings(this, ReplayMod.instance.getSettingsRegistry()).display(); } else if(button.id == RENAME_BUTTON_ID) { File file = replayFileList.get(replayGuiList.selected).first().first(); this.mc.displayGuiScreen(new GuiRenameReplay(this, file)); @@ -306,7 +315,7 @@ public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback { mc.displayGuiScreen(null); try { - ReplayHandler.startReplay(replayFileList.get(id).first().first()); + mod.startReplay(replayFileList.get(id).first().first()); } catch(Exception e) { e.printStackTrace(); } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/ReplayList.java b/src/main/java/com/replaymod/replay/gui/screen/ReplayList.java similarity index 94% rename from src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/ReplayList.java rename to src/main/java/com/replaymod/replay/gui/screen/ReplayList.java index 29912d7a..c08f517b 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/ReplayList.java +++ b/src/main/java/com/replaymod/replay/gui/screen/ReplayList.java @@ -1,4 +1,4 @@ -package eu.crushedpixel.replaymod.gui.replayviewer; +package com.replaymod.replay.gui.screen; import eu.crushedpixel.replaymod.gui.elements.GuiReplayListExtended; import net.minecraft.client.Minecraft; diff --git a/src/main/java/com/replaymod/replay/handler/GuiHandler.java b/src/main/java/com/replaymod/replay/handler/GuiHandler.java new file mode 100644 index 00000000..8ad74d1f --- /dev/null +++ b/src/main/java/com/replaymod/replay/handler/GuiHandler.java @@ -0,0 +1,111 @@ +package com.replaymod.replay.handler; + +import com.replaymod.replay.ReplayModReplay; +import com.replaymod.replay.gui.screen.GuiReplayViewer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiIngameMenu; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.resources.I18n; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GuiHandler { + private static final int BUTTON_EXIT_SERVER = 1; + private static final int BUTTON_RETURN_TO_GAME = 4; + private static final int BUTTON_ACHIEVEMENTS = 5; + private static final int BUTTON_STATS = 6; + private static final int BUTTON_OPEN_TO_LAN = 7; + + private static final int BUTTON_REPLAY_VIEWER = 17890234; + private static final int BUTTON_EXIT_REPLAY = 17890235; + + private static final Minecraft mc = Minecraft.getMinecraft(); + + private final ReplayModReplay mod; + + public GuiHandler(ReplayModReplay mod) { + this.mod = mod; + } + + public void register() { + FMLCommonHandler.instance().bus().register(this); + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void injectIntoIngameMenu(GuiScreenEvent.InitGuiEvent.Post event) { + if (!(event.gui instanceof GuiIngameMenu)) { + return; + } + + if (mod.getReplayHandler() != null) { + // Pause replay when menu is opened + mod.getReplayHandler().getReplaySender().setReplaySpeed(0); + + @SuppressWarnings("unchecked") + List buttonList = event.buttonList; + for(GuiButton b : new ArrayList<>(buttonList)) { + switch (b.id) { + // Replace "Exit Server" button with "Exit Replay" button + case BUTTON_EXIT_SERVER: + b.displayString = I18n.format("replaymod.gui.exit"); + b.id = BUTTON_EXIT_REPLAY; + break; + // Remove "Achievements", "Stats" and "Open to LAN" buttons + case BUTTON_ACHIEVEMENTS: + case BUTTON_STATS: + case BUTTON_OPEN_TO_LAN: + buttonList.remove(b); + } + // Move all buttons except the "Return to game" button upwards + if (b.id != BUTTON_RETURN_TO_GAME) { + b.yPosition -= 48; + } + } + } + } + + @SubscribeEvent + public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) { + if (!(event.gui instanceof GuiMainMenu)) { + return; + } + + @SuppressWarnings("unchecked") + List buttonList = event.buttonList; + GuiButton button = new GuiButton(BUTTON_REPLAY_VIEWER, event.gui.width / 2 - 100, + event.gui.height / 4 + 10 + 3 * 24, I18n.format("replaymod.gui.replayviewer")); + button.width = button.width / 2 - 2; + buttonList.add(button); + } + + @SubscribeEvent + public void onButton(GuiScreenEvent.ActionPerformedEvent event) { + if(!event.button.enabled) return; + + if (event.gui instanceof GuiMainMenu) { + if (event.button.id == BUTTON_REPLAY_VIEWER) { + mc.displayGuiScreen(new GuiReplayViewer(mod)); + } + } + + if (event.gui instanceof GuiIngameMenu && mod.getReplayHandler() != null) { + if (event.button.id == BUTTON_EXIT_REPLAY) { + event.button.enabled = false; + try { + mod.getReplayHandler().endReplay(); + } catch (IOException e) { + e.printStackTrace(); + } + mc.displayGuiScreen(new GuiMainMenu()); + } + } + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/container/AbstractGuiOverlay.java b/src/main/java/de/johni0702/minecraft/gui/container/AbstractGuiOverlay.java new file mode 100644 index 00000000..36130eff --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/container/AbstractGuiOverlay.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.container; + +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.MinecraftGuiRenderer; +import de.johni0702.minecraft.gui.OffsetGuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.element.GuiElement; +import de.johni0702.minecraft.gui.function.*; +import eu.crushedpixel.replaymod.utils.MouseUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.ReportedException; +import net.minecraftforge.client.event.RenderGameOverlayEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.Point; +import org.lwjgl.util.ReadableDimension; +import org.lwjgl.util.ReadablePoint; + +import java.io.IOException; +import java.util.concurrent.Callable; + +public abstract class AbstractGuiOverlay> extends AbstractGuiContainer { + + private final UserInputGuiScreen userInputGuiScreen = new UserInputGuiScreen(); + private final EventHandler eventHandler = new EventHandler(); + private boolean visible; + private Dimension screenSize; + private boolean mouseVisible; + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + if (this.visible != visible) { + if (visible) { + MinecraftForge.EVENT_BUS.register(eventHandler); + } else { + forEach(Closeable.class).close(); + MinecraftForge.EVENT_BUS.unregister(eventHandler); + } + updateUserInputGui(); + } + this.visible = visible; + } + + public boolean isMouseVisible() { + return mouseVisible; + } + + public void setMouseVisible(boolean mouseVisible) { + this.mouseVisible = mouseVisible; + updateUserInputGui(); + } + + private void updateUserInputGui() { + Minecraft mc = getMinecraft(); + if (visible) { + if (mouseVisible) { + if (mc.currentScreen != userInputGuiScreen) { + mc.displayGuiScreen(userInputGuiScreen); + } + } else { + if (mc.currentScreen == userInputGuiScreen) { + mc.displayGuiScreen(null); + } + } + } + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + super.draw(renderer, size, renderInfo); + if (mouseVisible && renderInfo.layer == getMaxLayer()) { + final GuiElement tooltip = forEach(GuiElement.class).getTooltip(renderInfo); + if (tooltip != null) { + final ReadableDimension tooltipSize = tooltip.getMinSize(); + int x, y; + if (renderInfo.mouseX + 8 + tooltipSize.getWidth() < screenSize.getWidth()) { + x = renderInfo.mouseX + 8; + } else { + x = screenSize.getWidth() - tooltipSize.getWidth() - 1; + } + if (renderInfo.mouseY + 8 + tooltipSize.getHeight() < screenSize.getHeight()) { + y = renderInfo.mouseY + 8; + } else { + y = screenSize.getHeight() - tooltipSize.getHeight() - 1; + } + final ReadablePoint position = new Point(x, y); + try { + OffsetGuiRenderer eRenderer = new OffsetGuiRenderer(renderer, position, tooltipSize); + tooltip.draw(eRenderer, tooltipSize, renderInfo); + } catch (Exception ex) { + CrashReport crashReport = CrashReport.makeCrashReport(ex, "Rendering Gui Tooltip"); + renderInfo.addTo(crashReport); + CrashReportCategory category = crashReport.makeCategory("Gui container details"); + category.addCrashSectionCallable("Container", new Callable() { + @Override + public Object call() throws Exception { + return this; + } + }); + category.addCrashSection("Width", size.getWidth()); + category.addCrashSection("Height", size.getHeight()); + category = crashReport.makeCategory("Tooltip details"); + category.addCrashSectionCallable("Element", new Callable() { + @Override + public Object call() throws Exception { + return tooltip; + } + }); + category.addCrashSectionCallable("Position", new Callable() { + @Override + public Object call() throws Exception { + return position; + } + }); + category.addCrashSectionCallable("Size", new Callable() { + @Override + public Object call() throws Exception { + return tooltipSize; + } + }); + throw new ReportedException(crashReport); + } + } + } + } + + @Override + public ReadableDimension getMinSize() { + return screenSize; + } + + @Override + public ReadableDimension getMaxSize() { + return screenSize; + } + + private class EventHandler { + private MinecraftGuiRenderer renderer; + + @SubscribeEvent + public void renderOverlay(RenderGameOverlayEvent.Post event) { + if (event.type == RenderGameOverlayEvent.ElementType.ALL) { + updateRenderer(); + int layers = getMaxLayer(); + int mouseX = -1, mouseY = -1; + if (mouseVisible) { + Point mouse = MouseUtils.getMousePos(); + mouseX = mouse.getX(); + mouseY = mouse.getY(); + } + for (int layer = 0; layer <= layers; layer++) { + draw(renderer, screenSize, new RenderInfo(event.partialTicks, mouseX, mouseY, layer)); + } + } + } + + @SubscribeEvent + public void tickOverlay(TickEvent.ClientTickEvent event) { + if (event.phase == TickEvent.Phase.START) { + forEach(Tickable.class).tick(); + } + } + + private void updateRenderer() { + Minecraft mc = getMinecraft(); + ScaledResolution res = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + if (screenSize == null + || screenSize.getWidth() != res.getScaledWidth() + || screenSize.getHeight() != res.getScaledHeight()) { + screenSize = new Dimension(res.getScaledWidth(), res.getScaledHeight()); + renderer = new MinecraftGuiRenderer(screenSize); + } + } + } + + protected class UserInputGuiScreen extends net.minecraft.client.gui.GuiScreen { + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + forEach(Typeable.class).typeKey(MouseUtils.getMousePos(), keyCode, typedChar, isCtrlKeyDown(), isShiftKeyDown()); + super.keyTyped(typedChar, keyCode); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + forEach(Clickable.class).mouseClick(new Point(mouseX, mouseY), mouseButton); + } + + @Override + protected void mouseReleased(int mouseX, int mouseY, int mouseButton) { + forEach(Draggable.class).mouseClick(new Point(mouseX, mouseY), mouseButton); + } + + @Override + protected void mouseClickMove(int mouseX, int mouseY, int mouseButton, long timeSinceLastClick) { + forEach(Draggable.class).mouseDrag(new Point(mouseX, mouseY), mouseButton, timeSinceLastClick); + } + + @Override + public void updateScreen() { + forEach(Tickable.class).tick(); + } + + @Override + public void onGuiClosed() { + mouseVisible = false; + } + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/container/AbstractGuiScreen.java b/src/main/java/de/johni0702/minecraft/gui/container/AbstractGuiScreen.java index 520d214e..4558c9ea 100644 --- a/src/main/java/de/johni0702/minecraft/gui/container/AbstractGuiScreen.java +++ b/src/main/java/de/johni0702/minecraft/gui/container/AbstractGuiScreen.java @@ -83,21 +83,21 @@ public abstract class AbstractGuiScreen> extends final GuiElement tooltip = forEach(GuiElement.class).getTooltip(renderInfo); if (tooltip != null) { final ReadableDimension tooltipSize = tooltip.getMinSize(); - int dx, dy; + int x, y; if (renderInfo.mouseX + 8 + tooltipSize.getWidth() < screenSize.getWidth()) { - dx = 8; + x = renderInfo.mouseX + 8; } else { - dx = -8; + x = screenSize.getWidth() - tooltipSize.getWidth() - 1; } if (renderInfo.mouseY + 8 + tooltipSize.getHeight() < screenSize.getHeight()) { - dy = 8; + y = renderInfo.mouseY + 8; } else { - dy = -8; + y = screenSize.getHeight() - tooltipSize.getHeight() - 1; } - final ReadablePoint position = new Point(renderInfo.mouseX + dx, renderInfo.mouseY + dy); + final ReadablePoint position = new Point(x, y); try { OffsetGuiRenderer eRenderer = new OffsetGuiRenderer(renderer, position, tooltipSize); - tooltip.draw(eRenderer, tooltipSize, renderInfo.offsetMouse(position.getX(), position.getY())); + tooltip.draw(eRenderer, tooltipSize, renderInfo); } catch (Exception ex) { CrashReport crashReport = CrashReport.makeCrashReport(ex, "Rendering Gui Tooltip"); renderInfo.addTo(crashReport); diff --git a/src/main/java/de/johni0702/minecraft/gui/container/GuiOverlay.java b/src/main/java/de/johni0702/minecraft/gui/container/GuiOverlay.java new file mode 100644 index 00000000..acfe39b6 --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/container/GuiOverlay.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.container; + +public class GuiOverlay extends AbstractGuiOverlay { + @Override + protected GuiOverlay getThis() { + return this; + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiElement.java b/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiElement.java index a72bfda2..cdc66592 100644 --- a/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiElement.java +++ b/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiElement.java @@ -26,11 +26,14 @@ import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.GuiContainer; import lombok.Getter; import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; import org.lwjgl.util.Dimension; import org.lwjgl.util.Point; import org.lwjgl.util.ReadableDimension; public abstract class AbstractGuiElement> implements GuiElement { + protected static final ResourceLocation TEXTURE = new ResourceLocation("guiapi", "gui.png"); + @Getter private final Minecraft minecraft = Minecraft.getMinecraft(); diff --git a/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiSlider.java b/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiSlider.java new file mode 100644 index 00000000..2a3c58b5 --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiSlider.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.element; + +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.GuiContainer; +import de.johni0702.minecraft.gui.function.Clickable; +import de.johni0702.minecraft.gui.function.Draggable; +import net.minecraft.client.resources.I18n; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.Point; +import org.lwjgl.util.ReadableDimension; +import org.lwjgl.util.ReadablePoint; + +// TODO: Currently assumes a height of 20 +public abstract class AbstractGuiSlider> extends AbstractGuiElement implements Clickable, Draggable, IGuiSlider { + private Runnable onValueChanged; + private ReadableDimension size; + + private int value; + private int steps; + + private String text = ""; + + private boolean dragging; + + public AbstractGuiSlider() { + } + + public AbstractGuiSlider(GuiContainer container) { + super(container); + } + + @Override + protected ReadableDimension calcMinSize() { + return new Dimension(0, 0); + } + + @Override + public boolean mouseClick(ReadablePoint position, int button) { + Point pos = new Point(position); + if (getContainer() != null) { + getContainer().convertFor(this, pos); + } + + if (isMouseHovering(pos) && isEnabled()) { + updateValue(pos); + dragging = true; + return true; + } + return false; + } + + @Override + public boolean mouseDrag(ReadablePoint position, int button, long timeSinceLastCall) { + if (dragging) { + Point pos = new Point(position); + if (getContainer() != null) { + getContainer().convertFor(this, pos); + } + updateValue(pos); + } + return dragging; + } + + @Override + public boolean mouseRelease(ReadablePoint position, int button) { + if (dragging) { + dragging = false; + Point pos = new Point(position); + if (getContainer() != null) { + getContainer().convertFor(this, pos); + } + updateValue(pos); + return true; + } else { + return false; + } + } + + protected boolean isMouseHovering(ReadablePoint pos) { + return pos.getX() > 0 && pos.getY() > 0 + && pos.getX() < size.getWidth() && pos.getY() < size.getHeight(); + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + this.size = size; + + int width = size.getWidth(); + int height = size.getHeight(); + + renderer.bindTexture(GuiButton.WIDGETS_TEXTURE); + + // Draw background + renderer.drawTexturedRect(0, 0, 0, 46, width / 2, height); + renderer.drawTexturedRect(width / 2, 0, 200 - width / 2, 46, width / 2, height); + + // Draw slider + int sliderX = (width - 8) * value / steps; + renderer.drawTexturedRect(sliderX, 0, 0, 66, 4, 20); + renderer.drawTexturedRect(sliderX + 4, 0, 196, 66, 4, 20); + + // Draw text + int color = 0xe0e0e0; + if (!isEnabled()) { + color = 0xa0a0a0; + } else if (isMouseHovering(new Point(renderInfo.mouseX, renderInfo.mouseY))) { + color = 0xffffa0; + } + renderer.drawCenteredString(width / 2, height / 2 - 4, color, text); + } + + protected void updateValue(ReadablePoint position) { + if (size == null) { + return; + } + int width = size.getWidth() - 8; + int pos = Math.max(0, Math.min(width, position.getX() - 4)); + setValue(steps * pos / width); + } + + public void onValueChanged() { + if (onValueChanged != null) { + onValueChanged.run(); + } + } + + @Override + public T setText(String text) { + this.text = text; + return getThis(); + } + + @Override + public T setI18nText(String text, Object... args) { + return setText(I18n.format(text, args)); + } + + @Override + public T setValue(int value) { + this.value = value; + onValueChanged(); + return getThis(); + } + + @Override + public int getValue() { + return value; + } + + @Override + public int getSteps() { + return steps; + } + + @Override + public T setSteps(int steps) { + this.steps = steps; + return getThis(); + } + + @Override + public T onValueChanged(Runnable runnable) { + this.onValueChanged = runnable; + return getThis(); + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiTexturedButton.java b/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiTexturedButton.java new file mode 100644 index 00000000..02b77f6f --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiTexturedButton.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.element; + +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.GuiContainer; +import de.johni0702.minecraft.gui.function.Clickable; +import lombok.Getter; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.util.*; + +import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA; +import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA; + +public abstract class AbstractGuiTexturedButton> extends AbstractGuiClickable implements Clickable, IGuiTexturedButton { + protected static final ResourceLocation BUTTON_SOUND = new ResourceLocation("gui.button.press"); + + @Getter + private ResourceLocation texture; + + @Getter + private ReadableDimension textureSize = new ReadableDimension() { + @Override + public int getWidth() { + return getMaxSize().getWidth(); + } + + @Override + public int getHeight() { + return getMaxSize().getHeight(); + } + + @Override + public void getSize(WritableDimension dest) { + getMaxSize().getSize(dest); + } + }; + + @Getter + private ReadableDimension textureTotalSize; + + @Getter + private ReadablePoint textureNormal; + + @Getter + private ReadablePoint textureHover; + + @Getter + private ReadablePoint textureDisabled; + + public AbstractGuiTexturedButton() { + } + + public AbstractGuiTexturedButton(GuiContainer container) { + super(container); + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + super.draw(renderer, size, renderInfo); + + renderer.bindTexture(texture); + + ReadablePoint texture = textureNormal; + if (!isEnabled()) { + texture = textureDisabled; + } else if (isMouseHovering(new Point(renderInfo.mouseX, renderInfo.mouseY))) { + texture = textureHover; + } + + if (texture == null) { // Button is disabled but we have no texture for that + GlStateManager.color(0.5f, 0.5f, 0.5f, 1); + texture = textureNormal; + } + + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, 1, 0); + GlStateManager.blendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + renderer.drawTexturedRect(0, 0, texture.getX(), texture.getY(), size.getWidth(), size.getHeight(), + textureSize.getWidth(), textureSize.getHeight(), + textureTotalSize.getWidth(), textureTotalSize.getHeight()); + } + + @Override + public ReadableDimension calcMinSize() { + return new Dimension(0, 0); + } + + @Override + public void onClick() { + getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F)); + super.onClick(); + } + + @Override + public T setTexture(ResourceLocation resourceLocation, int size) { + return setTexture(resourceLocation, size, size); + } + + @Override + public T setTexture(ResourceLocation resourceLocation, int width, int height) { + this.texture = resourceLocation; + this.textureTotalSize = new Dimension(width, height); + return getThis(); + } + + @Override + public T setTextureSize(int size) { + return setTextureSize(size, size); + } + + @Override + public T setTextureSize(int width, int height) { + this.textureSize = new Dimension(width, height); + return getThis(); + } + + @Override + public T setTexturePosH(final int x, final int y) { + return setTexturePosH(new Point(x, y)); + } + + @Override + public T setTexturePosV(final int x, final int y) { + return setTexturePosV(new Point(x, y)); + } + + @Override + public T setTexturePosH(final ReadablePoint pos) { + this.textureNormal = pos; + this.textureHover = new ReadablePoint() { + @Override + public int getX() { + return pos.getX() + textureSize.getWidth(); + } + + @Override + public int getY() { + return pos.getY(); + } + + @Override + public void getLocation(WritablePoint dest) { + dest.setLocation(getX(), getY()); + } + }; + return getThis(); + } + + @Override + public T setTexturePosV(final ReadablePoint pos) { + this.textureNormal = pos; + this.textureHover = new ReadablePoint() { + @Override + public int getX() { + return pos.getX(); + } + + @Override + public int getY() { + return pos.getY() + textureSize.getHeight(); + } + + @Override + public void getLocation(WritablePoint dest) { + dest.setLocation(getX(), getY()); + } + }; + return getThis(); + } + + @Override + public T setTexturePos(int normalX, int normalY, int hoverX, int hoverY) { + return setTexturePos(new Point(normalX, normalY), new Point(hoverX, hoverY)); + } + + @Override + public T setTexturePos(ReadablePoint normal, ReadablePoint hover) { + this.textureNormal = normal; + this.textureHover = hover; + return getThis(); + } + + @Override + public T setTexturePos(int normalX, int normalY, int hoverX, int hoverY, int disabledX, int disabledY) { + return setTexturePos(new Point(normalX, normalY), new Point(hoverX, hoverY), new Point(disabledX, disabledY)); + } + + @Override + public T setTexturePos(ReadablePoint normal, ReadablePoint hover, ReadablePoint disabled) { + this.textureDisabled = disabled; + return setTexturePos(normal, hover); + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiToggleButton.java b/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiToggleButton.java index bcbcbbf8..64bed00e 100644 --- a/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiToggleButton.java +++ b/src/main/java/de/johni0702/minecraft/gui/element/AbstractGuiToggleButton.java @@ -47,7 +47,7 @@ public abstract class AbstractGuiToggleButton { + public GuiSlider() { + } + + public GuiSlider(GuiContainer container) { + super(container); + } + + @Override + protected GuiSlider getThis() { + return this; + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/GuiTexturedButton.java b/src/main/java/de/johni0702/minecraft/gui/element/GuiTexturedButton.java new file mode 100644 index 00000000..dfe9ca67 --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/element/GuiTexturedButton.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.element; + +import de.johni0702.minecraft.gui.container.GuiContainer; + +public class GuiTexturedButton extends AbstractGuiTexturedButton { + public GuiTexturedButton() { + } + + public GuiTexturedButton(GuiContainer container) { + super(container); + } + + @Override + protected GuiTexturedButton getThis() { + return this; + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/IGuiSlider.java b/src/main/java/de/johni0702/minecraft/gui/element/IGuiSlider.java new file mode 100644 index 00000000..2a947c55 --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/element/IGuiSlider.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.element; + +public interface IGuiSlider> extends GuiElement { + T setText(String text); + T setI18nText(String text, Object... args); + + T setValue(int value); + int getValue(); + + int getSteps(); + T setSteps(int steps); + + T onValueChanged(Runnable runnable); +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/IGuiTexturedButton.java b/src/main/java/de/johni0702/minecraft/gui/element/IGuiTexturedButton.java new file mode 100644 index 00000000..88637530 --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/element/IGuiTexturedButton.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.element; + +import net.minecraft.util.ResourceLocation; +import org.lwjgl.util.ReadableDimension; +import org.lwjgl.util.ReadablePoint; + +public interface IGuiTexturedButton> extends IGuiClickable { + ResourceLocation getTexture(); + ReadableDimension getTextureTotalSize(); + T setTexture(ResourceLocation resourceLocation, int size); + T setTexture(ResourceLocation resourceLocation, int width, int height); + + ReadableDimension getTextureSize(); + T setTextureSize(int size); + T setTextureSize(int width, int height); + + ReadablePoint getTextureNormal(); + ReadablePoint getTextureHover(); + ReadablePoint getTextureDisabled(); + T setTexturePosH(int x, int y); + T setTexturePosV(int x, int y); + T setTexturePosH(ReadablePoint pos); + T setTexturePosV(ReadablePoint pos); + T setTexturePos(int normalX, int normalY, int hoverX, int hoverY); + T setTexturePos(ReadablePoint normal, ReadablePoint hover); + T setTexturePos(int normalX, int normalY, int hoverX, int hoverY, int disabledX, int disabledY); + T setTexturePos(ReadablePoint normal, ReadablePoint hover, ReadablePoint disabled); +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/advanced/AbstractGuiTimeline.java b/src/main/java/de/johni0702/minecraft/gui/element/advanced/AbstractGuiTimeline.java new file mode 100644 index 00000000..98d11e88 --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/element/advanced/AbstractGuiTimeline.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.element.advanced; + +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.GuiContainer; +import de.johni0702.minecraft.gui.element.AbstractGuiElement; +import de.johni0702.minecraft.gui.element.GuiTooltip; +import de.johni0702.minecraft.gui.function.Clickable; +import net.minecraft.util.MathHelper; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.Point; +import org.lwjgl.util.ReadableDimension; +import org.lwjgl.util.ReadablePoint; + +public abstract class AbstractGuiTimeline> extends AbstractGuiElement implements IGuiTimeline, Clickable { + protected static final int TEXTURE_WIDTH = 64; + protected static final int TEXTURE_HEIGHT = 22; + + protected static final int TEXTURE_X = 0; + protected static final int TEXTURE_Y = 16; + + protected static final int BORDER_LEFT = 4; + protected static final int BORDER_RIGHT = 4; + protected static final int BORDER_TOP = 4; + protected static final int BORDER_BOTTOM = 3; + protected static final int BODY_WIDTH = TEXTURE_WIDTH - BORDER_LEFT - BORDER_RIGHT; + protected static final int BODY_HEIGHT = TEXTURE_HEIGHT - BORDER_TOP - BORDER_BOTTOM; + + private OnClick onClick; + + private int length; + private int cursorPosition; + private double zoom = 1; + private int offset; + + private ReadableDimension size; + + public AbstractGuiTimeline() { + } + + public AbstractGuiTimeline(GuiContainer container) { + super(container); + } + + { + setTooltip(new GuiTooltip(){ + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + int ms = getTimeAt(renderInfo.mouseX, renderInfo.mouseY); + int s = ms / 1000 % 60; + int m = ms / 1000 / 60; + setText(String.format("%02d:%02d", m, s)); + super.draw(renderer, size, renderInfo); + } + }.setText("00:00")); + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + this.size = size; + + int width = size.getWidth(); + int height = size.getHeight(); + + // We have to increase the border size as there is one pixel row which is part of the border while drawing + // but isn't during position calculations due to shadows + int BORDER_LEFT = GuiTimeline.BORDER_LEFT + 1; + int BODY_WIDTH = GuiTimeline.BODY_WIDTH - 1; + + renderer.bindTexture(TEXTURE); + + // Left and right borders + for (int pass = 0; pass < 2; pass++) { + int x = pass == 0 ? 0 : width - BORDER_RIGHT; + int textureX = pass == 0 ? TEXTURE_X : TEXTURE_X + TEXTURE_WIDTH - BORDER_RIGHT; + // Border + for (int y = BORDER_TOP; y < height - BORDER_BOTTOM; y += BODY_HEIGHT) { + int segmentHeight = Math.min(BODY_HEIGHT, height - BORDER_BOTTOM - y); + renderer.drawTexturedRect(x, y, textureX, TEXTURE_Y + BORDER_TOP, BORDER_LEFT, segmentHeight); + } + // Top corner + renderer.drawTexturedRect(x, 0, textureX, TEXTURE_Y, BORDER_LEFT, BORDER_TOP); + // Bottom corner + renderer.drawTexturedRect(x, height - BORDER_BOTTOM, textureX, TEXTURE_Y + TEXTURE_HEIGHT - BORDER_BOTTOM, + BORDER_LEFT, BORDER_BOTTOM); + } + + for (int x = BORDER_LEFT; x < width - BORDER_RIGHT; x += BODY_WIDTH) { + int segmentWidth = Math.min(BODY_WIDTH, width - BORDER_RIGHT - x); + int textureX = TEXTURE_X + BORDER_LEFT; + + // Content + for (int y = BORDER_TOP; y < height - BORDER_BOTTOM; y += BODY_HEIGHT) { + int segmentHeight = Math.min(BODY_HEIGHT, height - BORDER_BOTTOM - y); + renderer.drawTexturedRect(x, y, textureX, TEXTURE_Y + BORDER_TOP, segmentWidth, segmentHeight); + } + + // Top border + renderer.drawTexturedRect(x, 0, textureX, TEXTURE_Y, segmentWidth, BORDER_TOP); + // Bottom border + renderer.drawTexturedRect(x, height - BORDER_BOTTOM, textureX, TEXTURE_Y + TEXTURE_HEIGHT - BORDER_BOTTOM, + segmentWidth, BORDER_BOTTOM); + } + + drawTimelineCursor(renderer, size); + } + + /** + * Draws the timeline cursor. + * This is separate from the main draw method so subclasses can repaint the cursor + * in case it got drawn over by other elements. + * @param renderer Gui renderer used to draw the cursor + * @param size Size of the drawable area + */ + protected void drawTimelineCursor(GuiRenderer renderer, ReadableDimension size) { + int height = size.getHeight(); + renderer.bindTexture(TEXTURE); + + int visibleLength = (int) (length * zoom); + int cursor = MathHelper.clamp_int(cursorPosition, offset, offset + visibleLength); + double positionInVisible = cursor - offset; + double fractionOfVisible = positionInVisible / visibleLength; + int cursorX = (int) (BORDER_LEFT + fractionOfVisible * (size.getWidth() - BORDER_LEFT - BORDER_RIGHT)); + + // Pin + renderer.drawTexturedRect(cursorX - 2, BORDER_TOP - 1, 64, 0, 5, 4); + // Needle + for (int y = BORDER_TOP - 1; y < height - BORDER_BOTTOM; y += 11) { + int segmentHeight = Math.min(11, height - BORDER_BOTTOM - y); + renderer.drawTexturedRect(cursorX - 2, y, 64, 4, 5, segmentHeight); + } + } + + /** + * Returns the time which the mouse is at. + * @param mouseX X coordinate of the mouse + * @param mouseY Y coordinate of the mouse + * @return The time or -1 if the mouse isn't on the timeline + */ + protected int getTimeAt(int mouseX, int mouseY) { + if (size == null) { + return -1; + } + Point mouse = new Point(mouseX, mouseY); + getContainer().convertFor(this, mouse); + mouseX = mouse.getX(); + mouseY = mouse.getY(); + + if (mouseX < 0 || mouseY < 0 + || mouseX > size.getWidth() || mouseY > size.getHeight()) { + return -1; + } + + int width = size.getWidth(); + int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; + double segmentLength = length * zoom; + double segmentTime = segmentLength * (mouseX - BORDER_LEFT) / bodyWidth; + return Math.min(Math.max((int) Math.round(offset + segmentTime), 0), length); + } + + public void onClick(int time) { + if (onClick != null) { + onClick.run(time); + } + } + + @Override + public ReadableDimension calcMinSize() { + return new Dimension(0, 0); + } + + @Override + public T setLength(int length) { + this.length = length; + return getThis(); + } + + @Override + public int getLength() { + return length; + } + + @Override + public T setCursorPosition(int position) { + this.cursorPosition = position; + return getThis(); + } + + @Override + public int getCursorPosition() { + return cursorPosition; + } + + @Override + public T setZoom(double zoom) { + this.zoom = zoom; + return getThis(); + } + + @Override + public double getZoom() { + return zoom; + } + + @Override + public T setOffset(int offset) { + this.offset = offset; + return getThis(); + } + + @Override + public int getOffset() { + return offset; + } + + @Override + public T onClick(OnClick onClick) { + this.onClick = onClick; + return getThis(); + } + + @Override + public boolean mouseClick(ReadablePoint position, int button) { + int time = getTimeAt(position.getX(), position.getY()); + if (time != -1) { + onClick(time); + return true; + } + return false; + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/advanced/GuiTimeline.java b/src/main/java/de/johni0702/minecraft/gui/element/advanced/GuiTimeline.java new file mode 100644 index 00000000..d21068f8 --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/element/advanced/GuiTimeline.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.element.advanced; + +import de.johni0702.minecraft.gui.container.GuiContainer; + +public class GuiTimeline extends AbstractGuiTimeline { + public GuiTimeline() { + } + + public GuiTimeline(GuiContainer container) { + super(container); + } + + @Override + protected GuiTimeline getThis() { + return this; + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/element/advanced/IGuiTimeline.java b/src/main/java/de/johni0702/minecraft/gui/element/advanced/IGuiTimeline.java new file mode 100644 index 00000000..a82f9d28 --- /dev/null +++ b/src/main/java/de/johni0702/minecraft/gui/element/advanced/IGuiTimeline.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2015 johni0702 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.johni0702.minecraft.gui.element.advanced; + +import de.johni0702.minecraft.gui.element.GuiElement; + +public interface IGuiTimeline> extends GuiElement { + + /** + * Set the total length of the timeline. + * @param length length in milliseconds, must be > 0 + * @return {@code this}, for chaining + */ + T setLength(int length); + + /** + * Returns the total length of the timeline. + * @return The total length in millisconds + */ + int getLength(); + + /** + * Set the current position of the cursor. Should be between 0 and {@link #getLength()}. + * @param position Position of the cursor in milliseconds + * @return {@code this}, for chaining + */ + T setCursorPosition(int position); + + /** + * Returns the current position of the cursor. Should be between 0 and {@link #getLength()}. + * @return cursor position in milliseconds + */ + int getCursorPosition(); + + /** + * Set the zoom of this timeline. 1/10 allows the user to see 1/10 of the total length. + * @param zoom The zoom factor. Must be between 1 (inclusive) and 0 (exclusive) + * @return {@code this}, for chaining + */ + T setZoom(double zoom); + + /** + * Returns the zoom of this timeline. 1/10 allows the user to see 1/10 of the total length. + * @return The zoom factor. Must be between 1 (inclusive) and 0 (exclusive) + */ + double getZoom(); + + /** + * Set the position of the timeline which should be shown. + * The left side of the timeline will start at this offset. + * @param offset The offset in milliseconds + * @return {@code this}, for chaining + */ + T setOffset(int offset); + + /** + * Returns the position of the timeline which should be shown. + * The left side of the timeline will start at this offset. + * @return The offset in milliseconds + */ + int getOffset(); + + T onClick(OnClick onClick); + + interface OnClick { + void run(int time); + } +} diff --git a/src/main/java/de/johni0702/minecraft/gui/layout/CustomLayout.java b/src/main/java/de/johni0702/minecraft/gui/layout/CustomLayout.java index 624b96be..020ada36 100644 --- a/src/main/java/de/johni0702/minecraft/gui/layout/CustomLayout.java +++ b/src/main/java/de/johni0702/minecraft/gui/layout/CustomLayout.java @@ -35,15 +35,32 @@ import java.util.Collection; import java.util.Map; public abstract class CustomLayout> implements Layout { + private final Layout parent; private Map> result = Maps.newHashMap(); + public CustomLayout() { + this(null); + } + + public CustomLayout(Layout parent) { + this.parent = parent; + } + @Override @SuppressWarnings("unchecked") public Map> layOut(GuiContainer container, ReadableDimension size) { result.clear(); - Collection elements = container.getChildren(); - for (GuiElement element : elements) { - result.put(element, Pair.of(new Point(0, 0), new Dimension(element.getMinSize()))); + if (parent == null) { + Collection elements = container.getChildren(); + for (GuiElement element : elements) { + result.put(element, Pair.of(new Point(0, 0), new Dimension(element.getMinSize()))); + } + } else { + Map> elements = parent.layOut(container, size); + for (Map.Entry> entry : elements.entrySet()) { + Pair pair = entry.getValue(); + result.put(entry.getKey(), Pair.of(new Point(pair.getLeft()), new Dimension(pair.getRight()))); + } } layout((T) container, size.getWidth(), size.getHeight()); diff --git a/src/main/java/de/johni0702/minecraft/gui/layout/GridLayout.java b/src/main/java/de/johni0702/minecraft/gui/layout/GridLayout.java index ca523885..1f077dc1 100644 --- a/src/main/java/de/johni0702/minecraft/gui/layout/GridLayout.java +++ b/src/main/java/de/johni0702/minecraft/gui/layout/GridLayout.java @@ -27,7 +27,6 @@ import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.element.GuiElement; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; import org.apache.commons.lang3.tuple.Pair; @@ -40,11 +39,13 @@ import java.util.Collections; import java.util.Iterator; import java.util.Map; -@RequiredArgsConstructor public class GridLayout implements Layout { private static final Data DEFAULT_DATA = new Data(); - private final int columns; + @Accessors(chain = true) + @Getter + @Setter + private int columns; @Accessors(chain = true) @Getter diff --git a/src/main/java/de/johni0702/minecraft/gui/layout/HorizontalLayout.java b/src/main/java/de/johni0702/minecraft/gui/layout/HorizontalLayout.java index 53cf4987..1f2a80d0 100644 --- a/src/main/java/de/johni0702/minecraft/gui/layout/HorizontalLayout.java +++ b/src/main/java/de/johni0702/minecraft/gui/layout/HorizontalLayout.java @@ -67,7 +67,9 @@ public class HorizontalLayout implements Layout { GuiElement element = entry.getKey(); Data data = entry.getValue() instanceof Data ? (Data) entry.getValue() : DEFAULT_DATA; Dimension elementSize = new Dimension(element.getMinSize()); - elementSize.setHeight(Math.min(size.getHeight(), element.getMaxSize().getHeight())); + ReadableDimension elementMaxSize = element.getMaxSize(); + elementSize.setWidth(Math.min(size.getWidth() - x, Math.min(elementSize.getWidth(), elementMaxSize.getWidth()))); + elementSize.setHeight(Math.min(size.getHeight(), elementMaxSize.getHeight())); int remainingHeight = size.getHeight() - elementSize.getHeight(); int y = (int) (data.alignment * remainingHeight); map.put(element, Pair.of(new Point(x, y), elementSize)); diff --git a/src/main/java/de/johni0702/minecraft/gui/layout/VerticalLayout.java b/src/main/java/de/johni0702/minecraft/gui/layout/VerticalLayout.java index 48554d52..5a333201 100644 --- a/src/main/java/de/johni0702/minecraft/gui/layout/VerticalLayout.java +++ b/src/main/java/de/johni0702/minecraft/gui/layout/VerticalLayout.java @@ -67,6 +67,8 @@ public class VerticalLayout implements Layout { GuiElement element = entry.getKey(); Data data = entry.getValue() instanceof Data ? (Data) entry.getValue() : DEFAULT_DATA; Dimension elementSize = new Dimension(element.getMinSize()); + ReadableDimension elementMaxSize = element.getMaxSize(); + elementSize.setHeight(Math.min(size.getHeight() - y, Math.min(elementSize.getHeight(), elementMaxSize.getHeight()))); elementSize.setWidth(Math.min(size.getWidth(), element.getMaxSize().getWidth())); int remainingWidth = size.getWidth() - elementSize.getWidth(); int x = (int) (data.alignment * remainingWidth); diff --git a/src/main/java/eu/crushedpixel/replaymod/api/ApiClient.java b/src/main/java/eu/crushedpixel/replaymod/api/ApiClient.java index 91b12774..3418da84 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/ApiClient.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/ApiClient.java @@ -5,7 +5,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.mojang.authlib.exceptions.AuthenticationException; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.replay.ReplayModApiMethods; import eu.crushedpixel.replaymod.api.replay.SearchQuery; import eu.crushedpixel.replaymod.api.replay.holders.*; diff --git a/src/main/java/eu/crushedpixel/replaymod/api/GsonApiClient.java b/src/main/java/eu/crushedpixel/replaymod/api/GsonApiClient.java index 2e0de1f9..07ae5200 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/GsonApiClient.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/GsonApiClient.java @@ -1,26 +1,26 @@ -package eu.crushedpixel.replaymod.api; - -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; -import org.apache.commons.lang3.StringEscapeUtils; - -import java.io.IOException; - -public class GsonApiClient { - - private static final JsonParser parser = new JsonParser(); - - public static JsonElement invoke(QueryBuilder query) throws IOException, ApiException { - String apiResult = SimpleApiClient.invoke(query); - return wrapWithJson(apiResult); - } - - public static JsonElement invokeJson(String url) throws IOException, ApiException { - String apiResult = StringEscapeUtils.unescapeHtml4(SimpleApiClient.invokeUrl(url).replace(""", "\\\"")); - return wrapWithJson(apiResult); - } - - private static JsonElement wrapWithJson(String apiResult) { - return parser.parse(apiResult); - } -} +package eu.crushedpixel.replaymod.api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.apache.commons.lang3.StringEscapeUtils; + +import java.io.IOException; + +public class GsonApiClient { + + private static final JsonParser parser = new JsonParser(); + + public static JsonElement invoke(QueryBuilder query) throws IOException, ApiException { + String apiResult = SimpleApiClient.invoke(query); + return wrapWithJson(apiResult); + } + + public static JsonElement invokeJson(String url) throws IOException, ApiException { + String apiResult = StringEscapeUtils.unescapeHtml4(SimpleApiClient.invokeUrl(url).replace(""", "\\\"")); + return wrapWithJson(apiResult); + } + + private static JsonElement wrapWithJson(String apiResult) { + return parser.parse(apiResult); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/api/SimpleApiClient.java b/src/main/java/eu/crushedpixel/replaymod/api/SimpleApiClient.java index a3f23c43..3e79dadc 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/SimpleApiClient.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/SimpleApiClient.java @@ -1,123 +1,123 @@ -package eu.crushedpixel.replaymod.api; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; -import eu.crushedpixel.replaymod.api.replay.holders.ApiError; -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Map; - -public class SimpleApiClient { - - private static final JsonParser jsonParser = new JsonParser(); - private static final Gson gson = new Gson(); - - /** - * Returns a Json String from the given QueryBuilder - * - * @param query The QueryBuilder to use - * @return A Json String from the API - * @throws IOException - * @throws ApiException - */ - public static String invoke(QueryBuilder query) throws IOException, ApiException { - return invokeImpl(query.toString()); - } - - /** - * Returns a Json String from the given URL - * - * @param url The URL to parse the Json from - * @return A Json String from the API - * @throws IOException - * @throws ApiException - */ - public static String invokeUrl(String url) throws IOException, ApiException { - return invokeImpl(url); - } - - /** - * Returns a Json String from the API - * - * @param method The apiMethod to be called - * @param paramMap The parameters to apply - * @return A Json String from the API - * @throws IOException - * @throws ApiException - */ - public static String invoke(String method, Map paramMap) throws IOException, ApiException { - return invokeImpl(method, paramMap); - } - - /** - * Returns a Json String from the API - * - * @param method The apiMethod to be called - * @return A Json String from the API - * @throws IOException - * @throws ApiException - */ - public static String invoke(String method) throws IOException, ApiException { - return invokeImpl(method, null); - } - - private static String invokeImpl(String urlString) throws IOException, ApiException { - - // read response - String responseContent = null; - InputStream is = null; - HttpURLConnection httpUrlConnection = null; - try { - URL url = new URL(urlString); - httpUrlConnection = (HttpURLConnection) url.openConnection(); - - httpUrlConnection.setRequestMethod("GET"); - - // give it 15 seconds to respond - httpUrlConnection.setReadTimeout(15 * 1000); - httpUrlConnection.connect(); - - int responseCode = httpUrlConnection.getResponseCode(); - - if(responseCode != 200) { - is = httpUrlConnection.getErrorStream(); - if(is != null) { - responseContent = IOUtils.toString(is, "UTF-8"); - } else { - responseContent = ""; - } - try { - JsonObject response = jsonParser.parse(responseContent).getAsJsonObject(); - throw new ApiException(gson.fromJson(response, ApiError.class)); - } catch(JsonParseException e) { - throw new ApiException(responseContent); - } - } - - is = httpUrlConnection.getInputStream(); - - responseContent = IOUtils.toString(is, "UTF-8"); - - } finally { - if(is != null) { - is.close(); - } - if(httpUrlConnection != null) { - httpUrlConnection.disconnect(); - } - } - return responseContent; - } - - private static String invokeImpl(String method, Map paramMap) throws IOException, ApiException { - QueryBuilder queryBuilder = new QueryBuilder(method); - queryBuilder.put(paramMap); - return invokeImpl(queryBuilder.toString()); - } -} +package eu.crushedpixel.replaymod.api; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import eu.crushedpixel.replaymod.api.replay.holders.ApiError; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; + +public class SimpleApiClient { + + private static final JsonParser jsonParser = new JsonParser(); + private static final Gson gson = new Gson(); + + /** + * Returns a Json String from the given QueryBuilder + * + * @param query The QueryBuilder to use + * @return A Json String from the API + * @throws IOException + * @throws ApiException + */ + public static String invoke(QueryBuilder query) throws IOException, ApiException { + return invokeImpl(query.toString()); + } + + /** + * Returns a Json String from the given URL + * + * @param url The URL to parse the Json from + * @return A Json String from the API + * @throws IOException + * @throws ApiException + */ + public static String invokeUrl(String url) throws IOException, ApiException { + return invokeImpl(url); + } + + /** + * Returns a Json String from the API + * + * @param method The apiMethod to be called + * @param paramMap The parameters to apply + * @return A Json String from the API + * @throws IOException + * @throws ApiException + */ + public static String invoke(String method, Map paramMap) throws IOException, ApiException { + return invokeImpl(method, paramMap); + } + + /** + * Returns a Json String from the API + * + * @param method The apiMethod to be called + * @return A Json String from the API + * @throws IOException + * @throws ApiException + */ + public static String invoke(String method) throws IOException, ApiException { + return invokeImpl(method, null); + } + + private static String invokeImpl(String urlString) throws IOException, ApiException { + + // read response + String responseContent = null; + InputStream is = null; + HttpURLConnection httpUrlConnection = null; + try { + URL url = new URL(urlString); + httpUrlConnection = (HttpURLConnection) url.openConnection(); + + httpUrlConnection.setRequestMethod("GET"); + + // give it 15 seconds to respond + httpUrlConnection.setReadTimeout(15 * 1000); + httpUrlConnection.connect(); + + int responseCode = httpUrlConnection.getResponseCode(); + + if(responseCode != 200) { + is = httpUrlConnection.getErrorStream(); + if(is != null) { + responseContent = IOUtils.toString(is, "UTF-8"); + } else { + responseContent = ""; + } + try { + JsonObject response = jsonParser.parse(responseContent).getAsJsonObject(); + throw new ApiException(gson.fromJson(response, ApiError.class)); + } catch(JsonParseException e) { + throw new ApiException(responseContent); + } + } + + is = httpUrlConnection.getInputStream(); + + responseContent = IOUtils.toString(is, "UTF-8"); + + } finally { + if(is != null) { + is.close(); + } + if(httpUrlConnection != null) { + httpUrlConnection.disconnect(); + } + } + return responseContent; + } + + private static String invokeImpl(String method, Map paramMap) throws IOException, ApiException { + QueryBuilder queryBuilder = new QueryBuilder(method); + queryBuilder.put(paramMap); + return invokeImpl(queryBuilder.toString()); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/api/replay/FileUploader.java b/src/main/java/eu/crushedpixel/replaymod/api/replay/FileUploader.java index 93d2df3f..0dcb2e89 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/replay/FileUploader.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/replay/FileUploader.java @@ -1,7 +1,7 @@ package eu.crushedpixel.replaymod.api.replay; import com.google.gson.Gson; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.replay.holders.ApiError; import eu.crushedpixel.replaymod.api.replay.holders.Category; import eu.crushedpixel.replaymod.gui.online.GuiUploadFile; diff --git a/src/main/java/eu/crushedpixel/replaymod/api/replay/ReplayModApiMethods.java b/src/main/java/eu/crushedpixel/replaymod/api/replay/ReplayModApiMethods.java index 4e47f049..26afd9c0 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/replay/ReplayModApiMethods.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/replay/ReplayModApiMethods.java @@ -1,24 +1,24 @@ -package eu.crushedpixel.replaymod.api.replay; - -public class ReplayModApiMethods { - - public static final String REPLAYMOD_BASE_URL = "http://ReplayMod.com/api/"; - - public static final String register = REPLAYMOD_BASE_URL+"register"; - public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey"; - public static final String login = REPLAYMOD_BASE_URL+"login"; - public static final String logout = REPLAYMOD_BASE_URL+"logout"; - public static final String file_details = REPLAYMOD_BASE_URL+"file_details"; - public static final String upload_file = REPLAYMOD_BASE_URL+"upload_file"; - public static final String download_file = REPLAYMOD_BASE_URL+"download_file"; - public static final String get_thumbnail = REPLAYMOD_BASE_URL+"get_thumbnail"; - public static final String remove_file = REPLAYMOD_BASE_URL+"remove_file"; - public static final String rate_file = REPLAYMOD_BASE_URL+"rate_file"; - public static final String get_ratings = REPLAYMOD_BASE_URL+"get_ratings"; - public static final String fav_file = REPLAYMOD_BASE_URL+"fav_file"; - public static final String get_favorites = REPLAYMOD_BASE_URL+"get_favorites"; - public static final String check_auth = REPLAYMOD_BASE_URL+"check_auth"; - public static final String get_language = REPLAYMOD_BASE_URL+"get_language"; - public static final String search = REPLAYMOD_BASE_URL+"search"; - public static final String up_to_date = REPLAYMOD_BASE_URL+"up_to_date"; -} +package eu.crushedpixel.replaymod.api.replay; + +public class ReplayModApiMethods { + + public static final String REPLAYMOD_BASE_URL = "http://ReplayMod.com/api/"; + + public static final String register = REPLAYMOD_BASE_URL+"register"; + public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey"; + public static final String login = REPLAYMOD_BASE_URL+"login"; + public static final String logout = REPLAYMOD_BASE_URL+"logout"; + public static final String file_details = REPLAYMOD_BASE_URL+"file_details"; + public static final String upload_file = REPLAYMOD_BASE_URL+"upload_file"; + public static final String download_file = REPLAYMOD_BASE_URL+"download_file"; + public static final String get_thumbnail = REPLAYMOD_BASE_URL+"get_thumbnail"; + public static final String remove_file = REPLAYMOD_BASE_URL+"remove_file"; + public static final String rate_file = REPLAYMOD_BASE_URL+"rate_file"; + public static final String get_ratings = REPLAYMOD_BASE_URL+"get_ratings"; + public static final String fav_file = REPLAYMOD_BASE_URL+"fav_file"; + public static final String get_favorites = REPLAYMOD_BASE_URL+"get_favorites"; + public static final String check_auth = REPLAYMOD_BASE_URL+"check_auth"; + public static final String get_language = REPLAYMOD_BASE_URL+"get_language"; + public static final String search = REPLAYMOD_BASE_URL+"search"; + public static final String up_to_date = REPLAYMOD_BASE_URL+"up_to_date"; +} diff --git a/src/main/java/eu/crushedpixel/replaymod/api/replay/holders/FileInfo.java b/src/main/java/eu/crushedpixel/replaymod/api/replay/holders/FileInfo.java index 87b65807..d3c5d41a 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/replay/holders/FileInfo.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/replay/holders/FileInfo.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.api.replay.holders; -import eu.crushedpixel.replaymod.recording.ReplayMetaData; +import de.johni0702.replaystudio.replay.ReplayMetaData; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/DownloadedFilePagination.java b/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/DownloadedFilePagination.java index ff43a607..857d23d3 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/DownloadedFilePagination.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/DownloadedFilePagination.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.api.replay.pagination; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.replay.holders.FileInfo; import java.io.File; diff --git a/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/FavoritedFilePagination.java b/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/FavoritedFilePagination.java index 36a86e1e..63224821 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/FavoritedFilePagination.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/FavoritedFilePagination.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.api.replay.pagination; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.replay.holders.FileInfo; import java.util.ArrayList; diff --git a/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/SearchPagination.java b/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/SearchPagination.java index 90b682c8..64ea66b0 100755 --- a/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/SearchPagination.java +++ b/src/main/java/eu/crushedpixel/replaymod/api/replay/pagination/SearchPagination.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.api.replay.pagination; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.replay.SearchQuery; import eu.crushedpixel.replaymod.api.replay.holders.FileInfo; diff --git a/src/main/java/eu/crushedpixel/replaymod/assets/AssetRepository.java b/src/main/java/eu/crushedpixel/replaymod/assets/AssetRepository.java index da9898cc..d96a7cee 100644 --- a/src/main/java/eu/crushedpixel/replaymod/assets/AssetRepository.java +++ b/src/main/java/eu/crushedpixel/replaymod/assets/AssetRepository.java @@ -1,18 +1,12 @@ package eu.crushedpixel.replaymod.assets; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.utils.ReplayFile; import lombok.EqualsAndHashCode; 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.*; -import java.util.zip.ZipEntry; @EqualsAndHashCode public class AssetRepository { @@ -62,40 +56,29 @@ public class AssetRepository { } public void saveAssets() { - try { - ReplayFile replayFile = new ReplayFile(ReplayHandler.getReplayFile()); - - Enumeration entries = replayFile.entries(); - while(entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if(entry.getName().startsWith(ReplayFile.ENTRY_ASSET_FOLDER)) { - ReplayMod.replayFileAppender.registerModifiedFile(null, entry.getName(), ReplayHandler.getReplayFile()); - } - } - - replayFile.close(); - - ReplayMod.replayFileAppender.deleteAllFilesByFolder(ReplayFile.ENTRY_ASSET_FOLDER, ReplayHandler.getReplayFile()); - - for(Map.Entry e : replayAssets.entrySet()) { - UUID uuid = e.getKey(); - ReplayAsset asset = e.getValue(); - try { - String filepath = ReplayFile.ENTRY_ASSET_FOLDER + uuid.toString()+ "_" + asset.getDisplayString() + "." + asset.getSavedFileExtension(); - - File toAdd = File.createTempFile("ASSET_"+asset.getDisplayString(), asset.getSavedFileExtension()); - FileOutputStream fos = new FileOutputStream(toAdd); - asset.writeToStream(fos); - - - ReplayMod.replayFileAppender.registerModifiedFile(toAdd, filepath, ReplayHandler.getReplayFile()); - } catch(IOException io) { - io.printStackTrace(); - } - } - } catch(IOException e) { - e.printStackTrace(); - } + // TODO +// try { +// ReplayFile replayFile = ReplayHandler.getReplayFile(); +// +// for (ReplayAssetEntry entry : replayFile.getAssets()) { +// if (!replayAssets.containsKey(entry.getUuid())) { +// replayFile.removeAsset(entry.getUuid()); +// } +// } +// +// for(Map.Entry e : replayAssets.entrySet()) { +// UUID uuid = e.getKey(); +// ReplayAsset asset = e.getValue(); +// String extension = asset.getSavedFileExtension(); +// String name = asset.getAssetName(); +// +// try (OutputStream out = replayFile.writeAsset(new ReplayAssetEntry(uuid, extension, name))) { +// asset.writeToStream(out); +// } +// } +// } catch(IOException e) { +// e.printStackTrace(); +// } } public static ReplayAsset assetFromFileName(String filename) { diff --git a/src/main/java/eu/crushedpixel/replaymod/assets/CustomImageObject.java b/src/main/java/eu/crushedpixel/replaymod/assets/CustomImageObject.java index 2307eed6..5f3b17b0 100644 --- a/src/main/java/eu/crushedpixel/replaymod/assets/CustomImageObject.java +++ b/src/main/java/eu/crushedpixel/replaymod/assets/CustomImageObject.java @@ -3,7 +3,6 @@ package eu.crushedpixel.replaymod.assets; import eu.crushedpixel.replaymod.holders.GuiEntryListEntry; import eu.crushedpixel.replaymod.holders.Transformations; import eu.crushedpixel.replaymod.registry.ResourceHelper; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -55,19 +54,20 @@ public class CustomImageObject implements GuiEntryListEntry { //if no asset repository available, simply accept the UUID and it will load the image later //when calling getResourceLocation for the first time - if(ReplayHandler.getAssetRepository() == null) { - this.linkedAsset = assetUUID; - return; - } - - ReplayAsset asset = ReplayHandler.getAssetRepository().getAssetByUUID(assetUUID); - - if(asset instanceof ReplayImageAsset) { - this.linkedAsset = assetUUID; - setImage(((ReplayImageAsset)asset).getObject()); - } else if(asset != null) { - throw new UnsupportedOperationException("A CustomImageObject requires a ReplayImageAsset"); - } + // TODO +// if(ReplayHandler.getAssetRepository() == null) { +// this.linkedAsset = assetUUID; +// return; +// } +// +// ReplayAsset asset = ReplayHandler.getAssetRepository().getAssetByUUID(assetUUID); +// +// if(asset instanceof ReplayImageAsset) { +// this.linkedAsset = assetUUID; +// setImage(((ReplayImageAsset)asset).getObject()); +// } else if(asset != null) { +// throw new UnsupportedOperationException("A CustomImageObject requires a ReplayImageAsset"); +// } } public void setImage(final BufferedImage bufferedImage) throws IOException { @@ -103,14 +103,15 @@ public class CustomImageObject implements GuiEntryListEntry { public ResourceLocation getResourceLocation() { if(resourceLocation == null) { - ReplayAsset asset = ReplayHandler.getAssetRepository().getAssetByUUID(linkedAsset); - if(asset instanceof ReplayImageAsset) { - try { - setImage(((ReplayImageAsset) asset).getObject()); - } catch(Exception e) { - e.printStackTrace(); - } - } + // TODO +// ReplayAsset asset = ReplayHandler.getAssetRepository().getAssetByUUID(linkedAsset); +// if(asset instanceof ReplayImageAsset) { +// try { +// setImage(((ReplayImageAsset) asset).getObject()); +// } catch(Exception e) { +// e.printStackTrace(); +// } +// } return null; } diff --git a/src/main/java/eu/crushedpixel/replaymod/assets/ReplayImageAsset.java b/src/main/java/eu/crushedpixel/replaymod/assets/ReplayImageAsset.java index 70d719d8..3ddd568c 100644 --- a/src/main/java/eu/crushedpixel/replaymod/assets/ReplayImageAsset.java +++ b/src/main/java/eu/crushedpixel/replaymod/assets/ReplayImageAsset.java @@ -1,7 +1,6 @@ package eu.crushedpixel.replaymod.assets; import eu.crushedpixel.replaymod.registry.ResourceHelper; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.BoundingUtils; import eu.crushedpixel.replaymod.utils.BufferedImageUtils; import lombok.EqualsAndHashCode; @@ -67,11 +66,12 @@ public class ReplayImageAsset implements ReplayAsset { this.bufferedImageHashCode = BufferedImageUtils.hashCode(object); ResourceHelper.freeResource(previewResource); - for(CustomImageObject object : ReplayHandler.getCustomImageObjects()) { - if(object.getLinkedAsset() != null && object.getLinkedAsset().equals(ReplayHandler.getAssetRepository().getUUIDForAsset(this))) { - object.setImage(this.object); - } - } + // TODO +// for(CustomImageObject object : ReplayHandler.getCustomImageObjects()) { +// if(object.getLinkedAsset() != null && object.getLinkedAsset().equals(ReplayHandler.getAssetRepository().getUUIDForAsset(this))) { +// object.setImage(this.object); +// } +// } } @Override diff --git a/src/main/java/eu/crushedpixel/replaymod/chat/ChatMessageHandler.java b/src/main/java/eu/crushedpixel/replaymod/chat/ChatMessageHandler.java index 78508337..24fe1b12 100755 --- a/src/main/java/eu/crushedpixel/replaymod/chat/ChatMessageHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/chat/ChatMessageHandler.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.chat; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.resources.I18n; diff --git a/src/main/java/eu/crushedpixel/replaymod/coremod/LoadingPlugin.java b/src/main/java/eu/crushedpixel/replaymod/coremod/LoadingPlugin.java index 04314bac..02536d38 100755 --- a/src/main/java/eu/crushedpixel/replaymod/coremod/LoadingPlugin.java +++ b/src/main/java/eu/crushedpixel/replaymod/coremod/LoadingPlugin.java @@ -17,6 +17,7 @@ public class LoadingPlugin implements IFMLLoadingPlugin { public LoadingPlugin() { MixinBootstrap.init(); MixinEnvironment.getDefaultEnvironment().addConfiguration("mixins.replaymod.json"); + MixinEnvironment.getDefaultEnvironment().addConfiguration("mixins.recording.replaymod.json"); CodeSource codeSource = getClass().getProtectionDomain().getCodeSource(); if (codeSource != null) { diff --git a/src/main/java/eu/crushedpixel/replaymod/events/ReplayExitEvent.java b/src/main/java/eu/crushedpixel/replaymod/events/ReplayExitEvent.java deleted file mode 100644 index 948c3ff2..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/events/ReplayExitEvent.java +++ /dev/null @@ -1,6 +0,0 @@ -package eu.crushedpixel.replaymod.events; - -import net.minecraftforge.fml.common.eventhandler.Event; - -public class ReplayExitEvent extends Event { -} diff --git a/src/main/java/eu/crushedpixel/replaymod/events/handlers/CrosshairRenderHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/CrosshairRenderHandler.java index 1a052043..597e3241 100644 --- a/src/main/java/eu/crushedpixel/replaymod/events/handlers/CrosshairRenderHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/CrosshairRenderHandler.java @@ -1,8 +1,6 @@ package eu.crushedpixel.replaymod.events.handlers; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.GlStateManager; import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; @@ -13,18 +11,20 @@ public class CrosshairRenderHandler { @SubscribeEvent public void preCrosshairRender(RenderGameOverlayEvent.Pre event) { //Crosshair should only render if hovered Entity can actually be spectated - if(ReplayHandler.isInReplay() && ReplayHandler.isCamera() && event.type == RenderGameOverlayEvent.ElementType.CROSSHAIRS) { - boolean cancel = !SpectatingHandler.canSpectate(mc.pointedEntity); - event.setCanceled(cancel); - } + // TODO +// if(ReplayHandler.isInReplay() && ReplayHandler.isCameraView() && event.type == RenderGameOverlayEvent.ElementType.CROSSHAIRS) { +// boolean cancel = !SpectatingHandler.canSpectate(mc.pointedEntity); +// event.setCanceled(cancel); +// } } @SubscribeEvent public void preChatRender(RenderGameOverlayEvent.Pre event) { - if(ReplayHandler.isInReplay() && ReplayHandler.isCamera() && event.type == RenderGameOverlayEvent.ElementType.CHAT) { - //when a crosshair was displayed, the background of the lowest line of chat would be opaque - GlStateManager.enableTexture2D(); - GlStateManager.disableBlend(); - } + // TODO +// if(ReplayHandler.isInReplay() && ReplayHandler.isCameraView() && event.type == RenderGameOverlayEvent.ElementType.CHAT) { +// //when a crosshair was displayed, the background of the lowest line of chat would be opaque +// GlStateManager.enableTexture2D(); +// GlStateManager.disableBlend(); +// } } } diff --git a/src/main/java/eu/crushedpixel/replaymod/events/handlers/GuiEventHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/GuiEventHandler.java index 44f0291a..4315f473 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/handlers/GuiEventHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/GuiEventHandler.java @@ -1,14 +1,11 @@ package eu.crushedpixel.replaymod.events.handlers; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; +import com.replaymod.core.gui.GuiReplaySettings; import eu.crushedpixel.replaymod.gui.GuiConstants; -import eu.crushedpixel.replaymod.gui.GuiReplaySettings; import eu.crushedpixel.replaymod.gui.online.GuiLoginPrompt; import eu.crushedpixel.replaymod.gui.online.GuiReplayCenter; import eu.crushedpixel.replaymod.gui.replayeditor.GuiReplayEditor; -import eu.crushedpixel.replaymod.gui.replayviewer.GuiReplayViewer; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.replay.ReplayProcess; import eu.crushedpixel.replaymod.settings.ReplaySettings; import eu.crushedpixel.replaymod.studio.VersionValidator; import eu.crushedpixel.replaymod.utils.MouseUtils; @@ -26,7 +23,6 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.lwjgl.util.Point; import java.awt.*; -import java.util.ArrayList; import java.util.List; public class GuiEventHandler { @@ -54,19 +50,20 @@ public class GuiEventHandler { e.printStackTrace(); } } - if(ReplayHandler.isInReplay()) ReplayHandler.setInReplay(false); } if(!ReplayMod.apiClient.isLoggedIn()) return; if(event.gui instanceof GuiChat || event.gui instanceof GuiInventory) { - if(ReplayHandler.isInReplay()) { - event.setCanceled(true); - } + // TODO +// if(ReplayHandler.isInReplay()) { +// event.setCanceled(true); +// } } else if(event.gui instanceof GuiDisconnected) { - if(!ReplayHandler.isInReplay() && System.currentTimeMillis() - ReplayHandler.lastExit < 5000) { - event.setCanceled(true); - } + // TODO +// if(!ReplayHandler.isInReplay() && System.currentTimeMillis() - ReplayHandler.lastExit < 5000) { +// event.setCanceled(true); +// } } } @@ -121,20 +118,7 @@ public class GuiEventHandler { public void onInit(InitGuiEvent event) { @SuppressWarnings("unchecked") List buttonList = event.buttonList; - if(event.gui instanceof GuiIngameMenu && ReplayHandler.isInReplay()) { - ReplayMod.replaySender.setReplaySpeed(0); - for(GuiButton b : new ArrayList(buttonList)) { - if(b.id == 1) { - b.displayString = I18n.format("replaymod.gui.exit"); - b.yPosition -= 24 * 2; - b.id = GuiConstants.EXIT_REPLAY_BUTTON; - } else if(b.id >= 5 && b.id <= 7) { - buttonList.remove(b); - } else if(b.id != 4) { - b.yPosition -= 24 * 2; - } - } - } else if(event.gui instanceof GuiMainMenu) { + if(event.gui instanceof GuiMainMenu) { int i1 = event.gui.height / 4 + 24 + 10; for(GuiButton b : buttonList) { @@ -171,9 +155,7 @@ public class GuiEventHandler { public void onButton(ActionPerformedEvent event) { if(!event.button.enabled) return; if(event.gui instanceof GuiMainMenu) { - if(event.button.id == GuiConstants.REPLAY_MANAGER_BUTTON_ID) { - mc.displayGuiScreen(new GuiReplayViewer()); - } else if(event.button.id == GuiConstants.REPLAY_CENTER_BUTTON_ID) { + if(event.button.id == GuiConstants.REPLAY_CENTER_BUTTON_ID) { if(ReplayMod.apiClient.isLoggedIn()) { mc.displayGuiScreen(new GuiReplayCenter()); } else { @@ -183,17 +165,7 @@ public class GuiEventHandler { mc.displayGuiScreen(new GuiReplayEditor()); } } else if(event.gui instanceof GuiOptions && event.button.id == GuiConstants.REPLAY_OPTIONS_BUTTON_ID) { - mc.displayGuiScreen(new GuiReplaySettings(event.gui)); - } - - if(ReplayHandler.isInReplay() && event.gui instanceof GuiIngameMenu && event.button.id == GuiConstants.EXIT_REPLAY_BUTTON) { - if(ReplayHandler.isInPath()) ReplayProcess.stopReplayProcess(false); - - event.button.enabled = false; - - mc.displayGuiScreen(new GuiMainMenu()); - - ReplayHandler.endReplay(); + new GuiReplaySettings(event.gui, ReplayMod.instance.getSettingsRegistry()).display(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/events/handlers/MinecraftTicker.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/MinecraftTicker.java index 469ccd19..3ceb9e81 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/handlers/MinecraftTicker.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/MinecraftTicker.java @@ -1,20 +1,19 @@ package eu.crushedpixel.replaymod.events.handlers; -import eu.crushedpixel.replaymod.ReplayMod; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.settings.GameSettings; import net.minecraft.client.settings.KeyBinding; import net.minecraft.crash.CrashReport; import net.minecraft.util.ReportedException; -import net.minecraftforge.client.event.MouseEvent; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; public class MinecraftTicker { public static void runMouseKeyboardTick(Minecraft mc) { - ReplayMod.mouseInputHandler.mouseEvent(new MouseEvent()); + // TODO +// ReplayMod.mouseInputHandler.mouseEvent(new MouseEvent()); if(mc.thePlayer == null) return; try { mc.mcProfiler.endStartSection("mouse"); diff --git a/src/main/java/eu/crushedpixel/replaymod/events/handlers/MouseInputHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/MouseInputHandler.java index 79d63f2f..bd9cbcc9 100644 --- a/src/main/java/eu/crushedpixel/replaymod/events/handlers/MouseInputHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/MouseInputHandler.java @@ -1,51 +1,45 @@ package eu.crushedpixel.replaymod.events.handlers; -import eu.crushedpixel.replaymod.entities.CameraEntity; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import net.minecraft.client.Minecraft; -import net.minecraftforge.client.event.MouseEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import org.lwjgl.input.Mouse; - public class MouseInputHandler { - private final Minecraft mc = Minecraft.getMinecraft(); - private boolean rightDown = false; - private boolean leftDown = false; - - @SubscribeEvent - public void mouseEvent(MouseEvent event) { - if(!ReplayHandler.isInReplay()) { - return; - } - - if(event.dwheel != 0 && mc.currentScreen == null) { - boolean increase = event.dwheel > 0; - CameraEntity.modifyCameraSpeed(increase); - } - - if(Mouse.isButtonDown(0)) { - if(!leftDown) { - leftDown = true; - if(mc.pointedEntity != null && ReplayHandler.isCamera() && mc.currentScreen == null) { - if(SpectatingHandler.canSpectate(mc.pointedEntity)) - ReplayHandler.spectateEntity(mc.pointedEntity); - } - } - } else { - leftDown = false; - } - - if(Mouse.isButtonDown(1)) { - if(!rightDown) { - rightDown = true; - if(mc.pointedEntity != null && ReplayHandler.isCamera() && mc.currentScreen == null) { - if(SpectatingHandler.canSpectate(mc.pointedEntity)) - ReplayHandler.spectateEntity(mc.pointedEntity); - } - } - } else { - rightDown = false; - } - } + // TODO +// private final Minecraft mc = Minecraft.getMinecraft(); +// private boolean rightDown = false; +// private boolean leftDown = false; +// +// @SubscribeEvent +// public void mouseEvent(MouseEvent event) { +// if(!ReplayHandler.isInReplay()) { +// return; +// } +// +// if(event.dwheel != 0 && mc.currentScreen == null) { +// boolean increase = event.dwheel > 0; +// CameraEntity.modifyCameraSpeed(increase); +// } +// +// if(Mouse.isButtonDown(0)) { +// if(!leftDown) { +// leftDown = true; +// if(mc.pointedEntity != null && ReplayHandler.isCameraView() && mc.currentScreen == null) { +// if(SpectatingHandler.canSpectate(mc.pointedEntity)) +// ReplayHandler.spectateEntity(mc.pointedEntity); +// } +// } +// } else { +// leftDown = false; +// } +// +// if(Mouse.isButtonDown(1)) { +// if(!rightDown) { +// rightDown = true; +// if(mc.pointedEntity != null && ReplayHandler.isCameraView() && mc.currentScreen == null) { +// if(SpectatingHandler.canSpectate(mc.pointedEntity)) +// ReplayHandler.spectateEntity(mc.pointedEntity); +// } +// } +// } else { +// rightDown = false; +// } +// } } diff --git a/src/main/java/eu/crushedpixel/replaymod/events/handlers/TickAndRenderListener.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/TickAndRenderListener.java index 0257442c..5c879d36 100644 --- a/src/main/java/eu/crushedpixel/replaymod/events/handlers/TickAndRenderListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/TickAndRenderListener.java @@ -1,18 +1,10 @@ package eu.crushedpixel.replaymod.events.handlers; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.chat.ChatMessageHandler; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.replay.ReplayProcess; -import eu.crushedpixel.replaymod.video.ReplayScreenshot; import net.minecraft.client.Minecraft; import net.minecraftforge.client.event.MouseEvent; import net.minecraftforge.client.event.RenderWorldLastEvent; -import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; -import org.lwjgl.input.Mouse; -import org.lwjgl.opengl.Display; public class TickAndRenderListener { @@ -30,75 +22,78 @@ public class TickAndRenderListener { @SubscribeEvent public void onRenderWorld(RenderWorldLastEvent event) throws Exception { - if(!ReplayHandler.isInReplay()) return; //If not in Replay, cancel - if (ReplayProcess.isVideoRecording()) return; // If recording, cancel - - if(requestScreenshot == 1) { - mc.addScheduledTask(new Runnable() { - @Override - public void run() { - ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.savingthumb", ChatMessageHandler.ChatMessageType.INFORMATION); - ReplayScreenshot.prepareScreenshot(); - requestScreenshot = 2; - } - }); - } else if(requestScreenshot == 2) { - mc.addScheduledTask(new Runnable() { - @Override - public void run() { - ReplayScreenshot.saveScreenshot(); - } - }); - } - - if(ReplayHandler.isInPath()) ReplayProcess.tickReplay(false); - if(ReplayHandler.isCamera()) mc.setRenderViewEntity(ReplayHandler.getCameraEntity()); - - if(mc.isGamePaused() && ReplayHandler.isInPath()) { - mc.isGamePaused = false; - } + // TODO +// if(!ReplayHandler.isInReplay()) return; //If not in Replay, cancel +// if (ReplayProcess.isVideoRecording()) return; // If recording, cancel +// +// if(requestScreenshot == 1) { +// mc.addScheduledTask(new Runnable() { +// @Override +// public void run() { +// ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.savingthumb", ChatMessageHandler.ChatMessageType.INFORMATION); +// ReplayScreenshot.prepareScreenshot(); +// requestScreenshot = 2; +// } +// }); +// } else if(requestScreenshot == 2) { +// mc.addScheduledTask(new Runnable() { +// @Override +// public void run() { +// ReplayScreenshot.saveScreenshot(); +// } +// }); +// } +// +// if(ReplayHandler.isInPath()) ReplayProcess.tickReplay(false); +// if(ReplayHandler.isCameraView()) mc.setRenderViewEntity(ReplayHandler.getCameraEntity()); +// +// if(mc.isGamePaused() && ReplayHandler.isInPath()) { +// mc.isGamePaused = false; +// } } @SubscribeEvent public void onRenderTick(TickEvent.RenderTickEvent event) { - if(!ReplayHandler.isInReplay() || ReplayProcess.isVideoRecording()) return; - - if(ReplayHandler.getCameraEntity() != null) - ReplayHandler.getCameraEntity().updateMovement(); - if(ReplayHandler.isInPath()) { - ReplayProcess.tickReplay(true); - } else onMouseMove(new MouseEvent()); - - FMLCommonHandler.instance().fireKeyInput(); + // TODO +// if(!ReplayHandler.isInReplay() || ReplayProcess.isVideoRecording()) return; +// +// if(ReplayHandler.getCameraEntity() != null) +// ReplayHandler.getCameraEntity().updateMovement(); +// if(ReplayHandler.isInPath()) { +// ReplayProcess.tickReplay(true); +// } else onMouseMove(new MouseEvent()); +// +// FMLCommonHandler.instance().fireKeyInput(); } @SubscribeEvent public void onMouseMove(MouseEvent event) { - if(!ReplayHandler.isInReplay()) return; - - mc.mcProfiler.startSection("mouse"); - - if(Minecraft.isRunningOnMac && mc.inGameHasFocus && !Mouse.isInsideWindow()) { - Mouse.setGrabbed(false); - Mouse.setCursorPosition(Display.getWidth() / 2, Display.getHeight() / 2); - Mouse.setGrabbed(true); - } - - if(mc.inGameHasFocus && !(ReplayHandler.isInPath())) { - mc.mouseHelper.mouseXYChange(); - float f1 = mc.gameSettings.mouseSensitivity * 0.6F + 0.2F; - float f2 = f1 * f1 * f1 * 8.0F; - float f3 = (float) mc.mouseHelper.deltaX * f2; - float f4 = (float) mc.mouseHelper.deltaY * f2; - byte b0 = 1; - - if(mc.gameSettings.invertMouse) { - b0 = -1; - } - - if(ReplayHandler.getCameraEntity() != null) { - ReplayHandler.getCameraEntity().setAngles(f3, f4 * (float) b0); - } - } + // TODO +// if(!ReplayHandler.isInReplay()) return; +// +// mc.mcProfiler.startSection("mouse"); +// +// if(Minecraft.isRunningOnMac && mc.inGameHasFocus && !Mouse.isInsideWindow()) { +// Mouse.setGrabbed(false); +// Mouse.setCursorPosition(Display.getWidth() / 2, Display.getHeight() / 2); +// Mouse.setGrabbed(true); +// } +// +// if(mc.inGameHasFocus && !(ReplayHandler.isInPath())) { +// mc.mouseHelper.mouseXYChange(); +// float f1 = mc.gameSettings.mouseSensitivity * 0.6F + 0.2F; +// float f2 = f1 * f1 * f1 * 8.0F; +// float f3 = (float) mc.mouseHelper.deltaX * f2; +// float f4 = (float) mc.mouseHelper.deltaY * f2; +// byte b0 = 1; +// +// if(mc.gameSettings.invertMouse) { +// b0 = -1; +// } +// +// if(ReplayHandler.getCameraEntity() != null) { +// ReplayHandler.getCameraEntity().setAngles(f3, f4 * (float) b0); +// } +// } } } diff --git a/src/main/java/eu/crushedpixel/replaymod/events/handlers/keyboard/KeyInputHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/handlers/keyboard/KeyInputHandler.java index c0635528..e46e69d8 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/handlers/keyboard/KeyInputHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/handlers/keyboard/KeyInputHandler.java @@ -1,22 +1,8 @@ package eu.crushedpixel.replaymod.events.handlers.keyboard; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.entities.CameraEntity.MoveDirection; -import eu.crushedpixel.replaymod.events.handlers.TickAndRenderListener; -import eu.crushedpixel.replaymod.gui.GuiAssetManager; -import eu.crushedpixel.replaymod.gui.GuiKeyframeRepository; -import eu.crushedpixel.replaymod.gui.GuiMouseInput; -import eu.crushedpixel.replaymod.gui.GuiObjectManager; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; -import eu.crushedpixel.replaymod.registry.KeybindRegistry; -import eu.crushedpixel.replaymod.registry.PlayerHandler; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.InputEvent; import org.lwjgl.Sys; -import org.lwjgl.input.Keyboard; public class KeyInputHandler { @@ -25,200 +11,200 @@ public class KeyInputHandler { private long prevKeysDown = Sys.getTime(); public void onKeyInput() throws Exception { - if(!ReplayHandler.isInReplay()) return; - - KeyBinding[] keyBindings = Minecraft.getMinecraft().gameSettings.keyBindings; - - boolean speedup = false; - - if(mc.currentScreen == null) { - boolean forward = false, backward = false, left = false, right = false, up = false, down = false; - - if(!ReplayHandler.isInPath()) { - for(KeyBinding kb : keyBindings) { - if(!kb.isKeyDown()) continue; - if(ReplayHandler.isCamera()) { - if(kb.getKeyDescription().equals("key.forward")) { - forward = true; - speedup = true; - } - - if(kb.getKeyDescription().equals("key.back")) { - backward = true; - speedup = true; - } - - if(kb.getKeyDescription().equals("key.jump")) { - up = true; - speedup = true; - } - - if(kb.getKeyDescription().equals("key.left")) { - left = true; - speedup = true; - } - - if(kb.getKeyDescription().equals("key.right")) { - right = true; - speedup = true; - } - } - - if(kb.getKeyDescription().equals("key.sneak")) { - if(ReplayHandler.isCamera()) { - down = true; - speedup = true; - } - ReplayHandler.spectateCamera(); - } - } - - forwardCameraMovement(forward, backward, left, right, up, down); - } - } - - if(ReplayHandler.getCameraEntity() != null) { - if(speedup) { - ReplayHandler.getCameraEntity().speedUp(); - prevKeysDown = Sys.getTime(); - } else { - if(Sys.getTime() - prevKeysDown > 100) { - ReplayHandler.getCameraEntity().stopSpeedUp(); - } - } - } - } - - @SubscribeEvent - public void keyInput(InputEvent.KeyInputEvent event) { - try { - onKeyInput(); - } catch(Exception e) { - e.printStackTrace(); - } - - KeyBinding[] keyBindings = Minecraft.getMinecraft().gameSettings.keyBindings; - - boolean found = false; - - for(KeyBinding kb : keyBindings) { - if(!kb.isKeyDown()) continue; - - handleCustomKeybindings(kb, found, -1); - found = true; - } - - if(!ReplayHandler.isInReplay() || (mc.currentScreen != null && !(mc.currentScreen instanceof GuiMouseInput))) return; - - for(StaticKeybinding staticKeybinding : KeybindRegistry.staticKeybindings) { - if(!Keyboard.isKeyDown(staticKeybinding.getKeyCode())) { - staticKeybinding.setDown(false); - return; - } - - if(staticKeybinding.isDown()) return; - staticKeybinding.setDown(true); - - switch(staticKeybinding.getId()) { - case KeybindRegistry.STATIC_DELETE_KEYFRAME: - if(ReplayHandler.getSelectedKeyframe() != null) ReplayHandler.removeKeyframe(ReplayHandler.getSelectedKeyframe()); - break; - } - } - } - - private void forwardCameraMovement(boolean forward, boolean backward, boolean left, boolean right, boolean up, boolean down) { - if(forward && !backward) { - ReplayHandler.getCameraEntity().setMovement(MoveDirection.FORWARD); - } else if(backward && !forward) { - ReplayHandler.getCameraEntity().setMovement(MoveDirection.BACKWARD); - } - - if(left && !right) { - ReplayHandler.getCameraEntity().setMovement(MoveDirection.LEFT); - } else if(right && !left) { - ReplayHandler.getCameraEntity().setMovement(MoveDirection.RIGHT); - } - - if(up && !down) { - ReplayHandler.getCameraEntity().setMovement(MoveDirection.UP); - } else if(down && !up) { - ReplayHandler.getCameraEntity().setMovement(MoveDirection.DOWN); - } + // TODO +// if(!ReplayHandler.isInReplay()) return; +// +// KeyBinding[] keyBindings = Minecraft.getMinecraft().gameSettings.keyBindings; +// +// boolean speedup = false; +// +// if(mc.currentScreen == null) { +// boolean forward = false, backward = false, left = false, right = false, up = false, down = false; +// +// if(!ReplayHandler.isInPath()) { +// for(KeyBinding kb : keyBindings) { +// if(!kb.isKeyDown()) continue; +// if(ReplayHandler.isCameraView()) { +// if(kb.getKeyDescription().equals("key.forward")) { +// forward = true; +// speedup = true; +// } +// +// if(kb.getKeyDescription().equals("key.back")) { +// backward = true; +// speedup = true; +// } +// +// if(kb.getKeyDescription().equals("key.jump")) { +// up = true; +// speedup = true; +// } +// +// if(kb.getKeyDescription().equals("key.left")) { +// left = true; +// speedup = true; +// } +// +// if(kb.getKeyDescription().equals("key.right")) { +// right = true; +// speedup = true; +// } +// } +// +// if(kb.getKeyDescription().equals("key.sneak")) { +// if(ReplayHandler.isCameraView()) { +// down = true; +// speedup = true; +// } +// ReplayHandler.spectateCamera(); +// } +// } +// +// forwardCameraMovement(forward, backward, left, right, up, down); +// } +// } +// +// if(ReplayHandler.getCameraEntity() != null) { +// if(speedup) { +// ReplayHandler.getCameraEntity().speedUp(); +// prevKeysDown = Sys.getTime(); +// } else { +// if(Sys.getTime() - prevKeysDown > 100) { +// ReplayHandler.getCameraEntity().stopSpeedUp(); +// } +// } +// } } +// +// @SubscribeEvent +// public void keyInput(InputEvent.KeyInputEvent event) { +// try { +// onKeyInput(); +// } catch(Exception e) { +// e.printStackTrace(); +// } +// +// KeyBinding[] keyBindings = Minecraft.getMinecraft().gameSettings.keyBindings; +// +// boolean found = false; +// +// for(KeyBinding kb : keyBindings) { +// if(!kb.isKeyDown()) continue; +// +// handleCustomKeybindings(kb, found, -1); +// found = true; +// } +// +// if(!ReplayHandler.isInReplay() || (mc.currentScreen != null && !(mc.currentScreen instanceof GuiMouseInput))) return; +// +// for(StaticKeybinding staticKeybinding : KeybindRegistry.staticKeybindings) { +// if(!Keyboard.isKeyDown(staticKeybinding.getKeyCode())) { +// staticKeybinding.setDown(false); +// return; +// } +// +// if(staticKeybinding.isDown()) return; +// staticKeybinding.setDown(true); +// +// switch(staticKeybinding.getId()) { +// case KeybindRegistry.STATIC_DELETE_KEYFRAME: +// if(ReplayHandler.getSelectedKeyframe() != null) ReplayHandler.removeKeyframe(ReplayHandler.getSelectedKeyframe()); +// break; +// } +// } +// } +// +// private void forwardCameraMovement(boolean forward, boolean backward, boolean left, boolean right, boolean up, boolean down) { +// if(forward && !backward) { +// ReplayHandler.getCameraEntity().setMovement(MoveDirection.FORWARD); +// } else if(backward && !forward) { +// ReplayHandler.getCameraEntity().setMovement(MoveDirection.BACKWARD); +// } +// +// if(left && !right) { +// ReplayHandler.getCameraEntity().setMovement(MoveDirection.LEFT); +// } else if(right && !left) { +// ReplayHandler.getCameraEntity().setMovement(MoveDirection.RIGHT); +// } +// +// if(up && !down) { +// ReplayHandler.getCameraEntity().setMovement(MoveDirection.UP); +// } else if(down && !up) { +// ReplayHandler.getCameraEntity().setMovement(MoveDirection.DOWN); +// } +// } public void handleCustomKeybindings(KeyBinding kb, boolean found, int keyCode) { //Custom registered handlers - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ADD_MARKER) && (kb.isPressed() || kb.getKeyCode() == keyCode)) { - if(ReplayHandler.isInReplay() && (mc.currentScreen == null || mc.currentScreen instanceof GuiMouseInput)) { - ReplayHandler.toggleMarker(); - } else if(ConnectionEventHandler.isRecording()) { - ConnectionEventHandler.addMarker(); - } - } - - if(!ReplayHandler.isInReplay() || (mc.currentScreen != null && !(mc.currentScreen instanceof GuiMouseInput))) return; - - if((kb.getKeyDescription().equals("key.chat") || kb.getKeyDescription().equals("key.command")) - && (kb.isPressed() || kb.getKeyCode() == keyCode)) { - mc.displayGuiScreen(new GuiMouseInput(ReplayMod.overlay)); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_PLAY_PAUSE) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayMod.overlay.togglePlayPause(); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ROLL_CLOCKWISE) && (kb.isKeyDown() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayHandler.addCameraTilt(Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL) ? 0.2f : 1); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ROLL_COUNTERCLOCKWISE) && (kb.isKeyDown() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayHandler.addCameraTilt(Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL) ? -0.2f : -1); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_RESET_TILT) && (kb.isKeyDown() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayHandler.setCameraTilt(0); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_THUMBNAIL) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !found && !ReplayHandler.isInPath()) { - TickAndRenderListener.requestScreenshot(); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_PLAYER_OVERVIEW) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !found && !ReplayHandler.isInPath()) { - PlayerHandler.openPlayerOverview(); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_LIGHTING) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayMod.replaySettings.setLightingEnabled(!ReplayMod.replaySettings.isLightingEnabled()); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_CLEAR_KEYFRAMES) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayHandler.resetKeyframes(false, ReplayMod.replaySettings.showClearKeyframesCallback()); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_SYNC_TIMELINE) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayHandler.syncTimeCursor(Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT)); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_KEYFRAME_PRESETS) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - mc.displayGuiScreen(new GuiKeyframeRepository(ReplayHandler.getKeyframeRepository())); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_PATH_PREVIEW) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayMod.replaySettings.setShowPathPreview(!ReplayMod.replaySettings.showPathPreview()); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ASSET_MANAGER) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - mc.displayGuiScreen(new GuiAssetManager()); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_OBJECT_MANAGER) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - mc.displayGuiScreen(new GuiObjectManager()); - } - - if(kb.getKeyDescription().equals(KeybindRegistry.KEY_TOGGLE_INTERPOLATION) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { - ReplayMod.replaySettings.toggleInterpolation(); - } + // TODO: Transfer to new key binding registry +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ADD_MARKER) && (kb.isPressed() || kb.getKeyCode() == keyCode)) { +// if(ReplayHandler.isInReplay() && (mc.currentScreen == null || mc.currentScreen instanceof GuiMouseInput)) { +// ReplayHandler.toggleMarker(); +// } +// } +// +// if(!ReplayHandler.isInReplay() || (mc.currentScreen != null && !(mc.currentScreen instanceof GuiMouseInput))) return; +// +// if((kb.getKeyDescription().equals("key.chat") || kb.getKeyDescription().equals("key.command")) +// && (kb.isPressed() || kb.getKeyCode() == keyCode)) { +// mc.displayGuiScreen(new GuiMouseInput(ReplayMod.overlay)); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_PLAY_PAUSE) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayMod.overlay.togglePlayPause(); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ROLL_CLOCKWISE) && (kb.isKeyDown() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayHandler.addCameraTilt(Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL) ? 0.2f : 1); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ROLL_COUNTERCLOCKWISE) && (kb.isKeyDown() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayHandler.addCameraTilt(Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL) ? -0.2f : -1); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_RESET_TILT) && (kb.isKeyDown() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayHandler.setCameraTilt(0); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_THUMBNAIL) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !found && !ReplayHandler.isInPath()) { +// TickAndRenderListener.requestScreenshot(); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_PLAYER_OVERVIEW) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !found && !ReplayHandler.isInPath()) { +// PlayerHandler.openPlayerOverview(); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_LIGHTING) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayMod.replaySettings.setLightingEnabled(!ReplayMod.replaySettings.isLightingEnabled()); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_CLEAR_KEYFRAMES) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayHandler.resetKeyframes(false, ReplayMod.replaySettings.showClearKeyframesCallback()); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_SYNC_TIMELINE) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayHandler.syncTimeCursor(Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT)); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_KEYFRAME_PRESETS) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// mc.displayGuiScreen(new GuiKeyframeRepository(ReplayHandler.getKeyframeRepository())); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_PATH_PREVIEW) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayMod.replaySettings.setShowPathPreview(!ReplayMod.replaySettings.showPathPreview()); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ASSET_MANAGER) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// mc.displayGuiScreen(new GuiAssetManager()); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_OBJECT_MANAGER) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// mc.displayGuiScreen(new GuiObjectManager()); +// } +// +// if(kb.getKeyDescription().equals(KeybindRegistry.KEY_TOGGLE_INTERPOLATION) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { +// ReplayMod.replaySettings.toggleInterpolation(); +// } } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiAssetManager.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiAssetManager.java index 2836509b..3a09b099 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiAssetManager.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiAssetManager.java @@ -6,8 +6,6 @@ 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.gui.overlay.GuiReplayOverlay; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.MouseUtils; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.resources.I18n; @@ -22,7 +20,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -public class GuiAssetManager extends GuiScreen implements GuiReplayOverlay.NoOverlay { +public class GuiAssetManager extends GuiScreen { private String screenTitle; @@ -43,8 +41,10 @@ public class GuiAssetManager extends GuiScreen implements GuiReplayOverlay.NoOve private ReplayAsset currentAsset; public GuiAssetManager() { - this.initialRepository = new AssetRepository(ReplayHandler.getAssetRepository()); - this.assetRepository = ReplayHandler.getAssetRepository(); + // TODO + initialRepository = null; +// this.initialRepository = new AssetRepository(ReplayHandler.getAssetRepository()); +// this.assetRepository = ReplayHandler.getAssetRepository(); } @Override diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java index 39568ffb..ee8edb81 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java @@ -1,35 +1,30 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.elements.*; -import eu.crushedpixel.replaymod.gui.elements.listeners.NumberValueChangeListener; -import eu.crushedpixel.replaymod.holders.*; +import eu.crushedpixel.replaymod.holders.Keyframe; import eu.crushedpixel.replaymod.interpolation.KeyframeList; import eu.crushedpixel.replaymod.interpolation.KeyframeValue; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.MouseUtils; -import eu.crushedpixel.replaymod.utils.RoundUtils; import eu.crushedpixel.replaymod.utils.TimestampUtils; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.GuiTextField; import net.minecraft.client.resources.I18n; import org.lwjgl.input.Keyboard; import org.lwjgl.util.Point; import java.awt.*; import java.io.IOException; -import java.util.Arrays; import java.util.List; public abstract class GuiEditKeyframe extends GuiScreen { @SuppressWarnings("unchecked") public static GuiEditKeyframe create(Keyframe kf) { - if(kf.getValue() instanceof Marker) return new GuiEditKeyframeMarker(kf); - if(kf.getValue() instanceof TimestampValue) return new GuiEditKeyframeTime(kf); - if(kf.getValue() instanceof SpectatorData) return new GuiEditKeyframeSpectator(kf); - if(kf.getValue() instanceof AdvancedPosition) return new GuiEditKeyframePosition(kf); + // TODO +// if(kf.getValue() instanceof TimestampValue) return new GuiEditKeyframeTime(kf); +// if(kf.getValue() instanceof SpectatorData) return new GuiEditKeyframeSpectator(kf); +// if(kf.getValue() instanceof AdvancedPosition) return new GuiEditKeyframePosition(kf); throw new UnsupportedOperationException("Keyframe type unknown: " + kf); } @@ -156,13 +151,14 @@ public abstract class GuiEditKeyframe extends GuiScreen @Override public void onGuiClosed() { - if(!save) { - ReplayHandler.removeKeyframe(keyframe); - ReplayHandler.addKeyframe(keyframeBackup); - ReplayHandler.selectKeyframe(keyframeBackup); - } - ReplayHandler.fireKeyframesModifyEvent(); - Keyboard.enableRepeatEvents(false); + // TODO +// if(!save) { +// ReplayHandler.removeKeyframe(keyframe); +// ReplayHandler.addKeyframe(keyframeBackup); +// ReplayHandler.selectKeyframe(keyframeBackup); +// } +// ReplayHandler.fireKeyframesModifyEvent(); +// Keyboard.enableRepeatEvents(false); } @Override @@ -218,300 +214,301 @@ public abstract class GuiEditKeyframe extends GuiScreen protected void drawScreen0() {} - private static class GuiEditKeyframeMarker extends GuiEditKeyframe { - private GuiAdvancedTextField markerNameInput; - - public GuiEditKeyframeMarker(Keyframe keyframe) { - super(keyframe, ReplayHandler.getMarkerKeyframes()); - } - - @Override - public void initGui() { - super.initGui(); - - if (!initialized) { - String name = keyframe.getValue().getName(); - if (name == null) name = ""; - markerNameInput = new GuiAdvancedTextField(fontRendererObj, 0, 0, 200, 20); - markerNameInput.hint = I18n.format("replaymod.gui.editkeyframe.markername"); - markerNameInput.setText(name); - - inputs.addPart(markerNameInput); - - new KeyframeValueChangeListener(this); - } - - markerNameInput.xPosition = width/2 - 100; - markerNameInput.yPosition = height/2-10; - - initialized = true; - } - - @Override - public void onGuiClosed() { - keyframe.getValue().setName(markerNameInput.getText().trim()); - super.onGuiClosed(); - } - } - - private static class GuiEditKeyframeTime extends GuiEditKeyframe { - private GuiNumberInput kfMin, kfSec, kfMs; - - public GuiEditKeyframeTime(Keyframe keyframe) { - super(keyframe, ReplayHandler.getTimeKeyframes()); - screenTitle = I18n.format("replaymod.gui.editkeyframe.title.time"); - } - - @Override - public void initGui() { - super.initGui(); - - if (!initialized) { - int time = keyframe.getValue().asInt(); - - kfMin = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 30, 0d, null, (double)TimestampUtils.timestampToWholeMinutes(time), false, "", 1); - kfSec = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 25, 0d, 59d, (double)TimestampUtils.getSecondsFromTimestamp(time), false, "", 1); - kfMs = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 35, 0d, 999d, (double)TimestampUtils.getMillisecondsFromTimestamp(time), false, "", 1); - - inputs.addPart(kfMin); - inputs.addPart(kfSec); - inputs.addPart(kfMs); - - KeyframeValueChangeListener listener = new KeyframeValueChangeListener(this) { - @Override - public void onValueChange(double value) { - keyframe.setValue(new TimestampValue(TimestampUtils.calculateTimestamp( - kfMin.getIntValue(), kfSec.getIntValue(), kfMs.getIntValue()))); - - super.onValueChange(value); - } - }; - - kfMin.addValueChangeListener(listener); - kfSec.addValueChangeListener(listener); - kfMs.addValueChangeListener(listener); - } - - kfMin.xPosition = min.xPosition; - kfSec.xPosition = sec.xPosition; - kfMs.xPosition = ms.xPosition; - - kfMin.yPosition = kfSec.yPosition = kfMs.yPosition = min.yPosition - 10 - 20; - - initialized = true; - } - - @Override - protected void drawScreen0() { - drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.timestamp")+":", (width-w3)/2, kfMin.yPosition + 7, Color.WHITE.getRGB()); - drawString(fontRendererObj, I18n.format("replaymod.gui.minutes"), kfMin.xPosition + kfMin.width + 5, kfMin.yPosition + 7, Color.WHITE.getRGB()); - drawString(fontRendererObj, I18n.format("replaymod.gui.seconds"), kfSec.xPosition + kfSec.width + 5, kfSec.yPosition + 7, Color.WHITE.getRGB()); - drawString(fontRendererObj, I18n.format("replaymod.gui.milliseconds"), kfMs.xPosition + kfMs.width + 5, kfMs.yPosition + 7, Color.WHITE.getRGB()); - } - - } - - private static class GuiEditKeyframePosition extends GuiEditKeyframe { - private GuiNumberInput xCoord, yCoord, zCoord, pitch, yaw, roll; - private ComposedElement posInputs; - - public GuiEditKeyframePosition(Keyframe keyframe) { - super(keyframe, ReplayHandler.getPositionKeyframes()); - screenTitle = I18n.format("replaymod.gui.editkeyframe.title.pos"); - } - - @Override - public void initGui() { - super.initGui(); - - if (!initialized) { - AdvancedPosition pos = keyframe.getValue(); - xCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getX()), true); - yCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getY()), true); - zCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getZ()), true); - yaw = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getYaw()), true); - pitch = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, -90d, 90d, RoundUtils.round2Decimals(pos.getPitch()), true); - roll = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getRoll()), true); - - posInputs = new ComposedElement(xCoord, yCoord, zCoord, yaw, pitch, roll); - inputs.addPart(posInputs); - - KeyframeValueChangeListener listener = new KeyframeValueChangeListener(this) { - @Override - public void onValueChange(double value) { - keyframe.setValue(new AdvancedPosition(xCoord.getPreciseValue(), yCoord.getPreciseValue(), - zCoord.getPreciseValue(), new Float(pitch.getPreciseValue()), (float) yaw.getPreciseValue(), - (float) roll.getPreciseValue())); - - super.onValueChange(value); - } - }; - - xCoord.addValueChangeListener(listener); - yCoord.addValueChangeListener(listener); - zCoord.addValueChangeListener(listener); - pitch.addValueChangeListener(listener); - yaw.addValueChangeListener(listener); - roll.addValueChangeListener(listener); - } - - int w = Math.max(fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.xpos")), - Math.max(fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.ypos")), - fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.zpos")))); - w2 = Math.max(fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.camyaw")), - Math.max(fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.campitch")), - fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.camroll")))); - - totalWidth = w +100+w2+100+5+5+10; - left = (this.width - totalWidth)/2; - - int x = w + left + 5; - int i = 0; - for(GuiElement input : posInputs.getParts()) { - if(input instanceof GuiTextField) { - GuiTextField textField = (GuiTextField)input; - textField.xPosition = i < 3 ? x : left+totalWidth-100; - textField.yPosition = i < 3 ? virtualY + 20 + i*30 : virtualY + 20 + (i-3)*30; - i++; - } - } - - initialized = true; - } - - @Override - protected void drawScreen0() { - if (keyframe.getValue() instanceof SpectatorData) return; - drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.xpos"), left, virtualY + 27, Color.WHITE.getRGB()); - drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.ypos"), left, virtualY + 57, Color.WHITE.getRGB()); - drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.zpos"), left, virtualY + 87, Color.WHITE.getRGB()); - drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.camyaw"), left + totalWidth - 100 - 5 - w2, virtualY + 27, Color.WHITE.getRGB()); - drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.campitch"), left + totalWidth - 100 - 5 - w2, virtualY + 57, Color.WHITE.getRGB()); - drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.camroll"), left + totalWidth - 100 - 5 - w2, virtualY + 87, Color.WHITE.getRGB()); - } - - } - - private static class GuiEditKeyframeSpectator extends GuiEditKeyframe { - private GuiToggleButton perspectiveButton; - - private GuiString shoulderDistanceString, shoulderPitchString, shoulderYawString, shoulderSmoothnessString; - private GuiDraggingNumberInput shoulderDistanceInput, shoulderPitchOffsetInput, - shoulderYawOffsetInput, shoulderSmoothnessInput; - - private ComposedElement spectatorCamSettings; - private ComposedElement shoulderCamSettings; - - private DelegatingElement perspectiveSettings = new DelegatingElement() { - @Override - public GuiElement delegate() { - switch(perspectiveButton.getValue()) { - case 0: - return spectatorCamSettings; - case 1: - return shoulderCamSettings; - default: - return null; - } - } - }; - - public GuiEditKeyframeSpectator(Keyframe keyframe) { - super(keyframe, ReplayHandler.getPositionKeyframes()); - screenTitle = I18n.format("replaymod.gui.editkeyframe.title.spec"); - } - - @Override - public void initGui() { - super.initGui(); - - if (!initialized) { - SpectatorData data = (SpectatorData)keyframe.getValue(); - - perspectiveButton = new GuiToggleButton(0, 0, 0, 200, 20, I18n.format("replaymod.gui.editkeyframe.spec.method")+": ", new String[]{ - I18n.format("replaymod.gui.editkeyframe.spec.method.firstperson"), - I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder")}); - - perspectiveButton.setValue(Arrays.asList(SpectatorData.SpectatingMethod.values()).indexOf(data.getSpectatingMethod())); - - spectatorCamSettings = new ComposedElement(); - - //create elements in shoulderCamSettings - shoulderDistanceString = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder.distance")); - shoulderPitchString = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder.pitch")); - shoulderYawString = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder.yaw")); - shoulderSmoothnessString = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder.smoothness")); - - shoulderDistanceInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 0, 0d, 30d, data.getThirdPersonInfo().shoulderCamDistance, true, "", 0.1); - shoulderPitchOffsetInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 0, -90d, 90d, data.getThirdPersonInfo().shoulderCamPitchOffset, false, "°", 1); - shoulderYawOffsetInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 0, null, null, data.getThirdPersonInfo().shoulderCamYawOffset, false, "°", 1); - shoulderSmoothnessInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 0, 0.1d, 3d, data.getThirdPersonInfo().shoulderCamSmoothness, true, "", 0.1); - - shoulderCamSettings = new ComposedElement( - shoulderDistanceString, shoulderDistanceInput, - shoulderPitchString, shoulderPitchOffsetInput, - shoulderYawString, shoulderYawOffsetInput, - shoulderSmoothnessString, shoulderSmoothnessInput); - - inputs.addPart(new ComposedElement(perspectiveButton, perspectiveSettings)); - } - - perspectiveButton.xPosition = (this.width-perspectiveButton.width())/2; - perspectiveButton.yPosition = this.virtualY + 20; - - int verticalSpacing = 20 + 5; - int totalWidth = 300; - int elementWidth = 145; - int horizontalSpacing = 10; - - int y = perspectiveButton.yPos() + verticalSpacing; - - int i = 0; - for(GuiElement el : shoulderCamSettings.getParts()) { - el.xPos((width-totalWidth)/2 + (i%2)*(elementWidth + horizontalSpacing)); - el.width(elementWidth); - - el.yPos(y + ((i+1)%2)*7); - - if(i%2 == 1) { - y += verticalSpacing; - } - i++; - } - - initialized = true; - } - - @Override - public void onGuiClosed() { - SpectatorData data = (SpectatorData)keyframe.getValue(); - data.setSpectatingMethod(SpectatorData.SpectatingMethod.values()[perspectiveButton.getValue()]); - data.getThirdPersonInfo().shoulderCamDistance = shoulderDistanceInput.getPreciseValue(); - data.getThirdPersonInfo().shoulderCamPitchOffset = shoulderPitchOffsetInput.getIntValue(); - data.getThirdPersonInfo().shoulderCamYawOffset = shoulderYawOffsetInput.getIntValue(); - data.getThirdPersonInfo().shoulderCamSmoothness = shoulderSmoothnessInput.getPreciseValue(); - super.onGuiClosed(); - } - } - - private static class KeyframeValueChangeListener implements NumberValueChangeListener { - - private GuiEditKeyframe parent; - - public KeyframeValueChangeListener(GuiEditKeyframe parent) { - this.parent = parent; - - parent.min.addValueChangeListener(this); - parent.sec.addValueChangeListener(this); - parent.ms.addValueChangeListener(this); - } - - @Override - public void onValueChange(double value) { - int realTimestamp = TimestampUtils.calculateTimestamp(parent.min.getIntValue(), parent.sec.getIntValue(), parent.ms.getIntValue()); - parent.keyframe.setRealTimestamp(realTimestamp); - - ReplayHandler.fireKeyframesModifyEvent(); - } - } + // TODO +// private static class GuiEditKeyframeMarker extends GuiEditKeyframe { +// private GuiAdvancedTextField markerNameInput; +// +// public GuiEditKeyframeMarker(Keyframe keyframe) { +// super(keyframe, ReplayHandler.getMarkerKeyframes()); +// } +// +// @Override +// public void initGui() { +// super.initGui(); +// +// if (!initialized) { +// String name = keyframe.getValue().getName(); +// if (name == null) name = ""; +// markerNameInput = new GuiAdvancedTextField(fontRendererObj, 0, 0, 200, 20); +// markerNameInput.hint = I18n.format("replaymod.gui.editkeyframe.markername"); +// markerNameInput.setText(name); +// +// inputs.addPart(markerNameInput); +// +// new KeyframeValueChangeListener(this); +// } +// +// markerNameInput.xPosition = width/2 - 100; +// markerNameInput.yPosition = height/2-10; +// +// initialized = true; +// } +// +// @Override +// public void onGuiClosed() { +// keyframe.getValue().setName(markerNameInput.getText().trim()); +// super.onGuiClosed(); +// } +// } +// +// private static class GuiEditKeyframeTime extends GuiEditKeyframe { +// private GuiNumberInput kfMin, kfSec, kfMs; +// +// public GuiEditKeyframeTime(Keyframe keyframe) { +// super(keyframe, ReplayHandler.getTimeKeyframes()); +// screenTitle = I18n.format("replaymod.gui.editkeyframe.title.time"); +// } +// +// @Override +// public void initGui() { +// super.initGui(); +// +// if (!initialized) { +// int time = keyframe.getValue().asInt(); +// +// kfMin = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 30, 0d, null, (double)TimestampUtils.timestampToWholeMinutes(time), false, "", 1); +// kfSec = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 25, 0d, 59d, (double)TimestampUtils.getSecondsFromTimestamp(time), false, "", 1); +// kfMs = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 35, 0d, 999d, (double)TimestampUtils.getMillisecondsFromTimestamp(time), false, "", 1); +// +// inputs.addPart(kfMin); +// inputs.addPart(kfSec); +// inputs.addPart(kfMs); +// +// KeyframeValueChangeListener listener = new KeyframeValueChangeListener(this) { +// @Override +// public void onValueChange(double value) { +// keyframe.setValue(new TimestampValue(TimestampUtils.calculateTimestamp( +// kfMin.getIntValue(), kfSec.getIntValue(), kfMs.getIntValue()))); +// +// super.onValueChange(value); +// } +// }; +// +// kfMin.addValueChangeListener(listener); +// kfSec.addValueChangeListener(listener); +// kfMs.addValueChangeListener(listener); +// } +// +// kfMin.xPosition = min.xPosition; +// kfSec.xPosition = sec.xPosition; +// kfMs.xPosition = ms.xPosition; +// +// kfMin.yPosition = kfSec.yPosition = kfMs.yPosition = min.yPosition - 10 - 20; +// +// initialized = true; +// } +// +// @Override +// protected void drawScreen0() { +// drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.timestamp")+":", (width-w3)/2, kfMin.yPosition + 7, Color.WHITE.getRGB()); +// drawString(fontRendererObj, I18n.format("replaymod.gui.minutes"), kfMin.xPosition + kfMin.width + 5, kfMin.yPosition + 7, Color.WHITE.getRGB()); +// drawString(fontRendererObj, I18n.format("replaymod.gui.seconds"), kfSec.xPosition + kfSec.width + 5, kfSec.yPosition + 7, Color.WHITE.getRGB()); +// drawString(fontRendererObj, I18n.format("replaymod.gui.milliseconds"), kfMs.xPosition + kfMs.width + 5, kfMs.yPosition + 7, Color.WHITE.getRGB()); +// } +// +// } +// +// private static class GuiEditKeyframePosition extends GuiEditKeyframe { +// private GuiNumberInput xCoord, yCoord, zCoord, pitch, yaw, roll; +// private ComposedElement posInputs; +// +// public GuiEditKeyframePosition(Keyframe keyframe) { +// super(keyframe, ReplayHandler.getPositionKeyframes()); +// screenTitle = I18n.format("replaymod.gui.editkeyframe.title.pos"); +// } +// +// @Override +// public void initGui() { +// super.initGui(); +// +// if (!initialized) { +// AdvancedPosition pos = keyframe.getValue(); +// xCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getX()), true); +// yCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getY()), true); +// zCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getZ()), true); +// yaw = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getYaw()), true); +// pitch = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, -90d, 90d, RoundUtils.round2Decimals(pos.getPitch()), true); +// roll = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getRoll()), true); +// +// posInputs = new ComposedElement(xCoord, yCoord, zCoord, yaw, pitch, roll); +// inputs.addPart(posInputs); +// +// KeyframeValueChangeListener listener = new KeyframeValueChangeListener(this) { +// @Override +// public void onValueChange(double value) { +// keyframe.setValue(new AdvancedPosition(xCoord.getPreciseValue(), yCoord.getPreciseValue(), +// zCoord.getPreciseValue(), new Float(pitch.getPreciseValue()), (float) yaw.getPreciseValue(), +// (float) roll.getPreciseValue())); +// +// super.onValueChange(value); +// } +// }; +// +// xCoord.addValueChangeListener(listener); +// yCoord.addValueChangeListener(listener); +// zCoord.addValueChangeListener(listener); +// pitch.addValueChangeListener(listener); +// yaw.addValueChangeListener(listener); +// roll.addValueChangeListener(listener); +// } +// +// int w = Math.max(fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.xpos")), +// Math.max(fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.ypos")), +// fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.zpos")))); +// w2 = Math.max(fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.camyaw")), +// Math.max(fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.campitch")), +// fontRendererObj.getStringWidth(I18n.format("replaymod.gui.editkeyframe.camroll")))); +// +// totalWidth = w +100+w2+100+5+5+10; +// left = (this.width - totalWidth)/2; +// +// int x = w + left + 5; +// int i = 0; +// for(GuiElement input : posInputs.getParts()) { +// if(input instanceof GuiTextField) { +// GuiTextField textField = (GuiTextField)input; +// textField.xPosition = i < 3 ? x : left+totalWidth-100; +// textField.yPosition = i < 3 ? virtualY + 20 + i*30 : virtualY + 20 + (i-3)*30; +// i++; +// } +// } +// +// initialized = true; +// } +// +// @Override +// protected void drawScreen0() { +// if (keyframe.getValue() instanceof SpectatorData) return; +// drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.xpos"), left, virtualY + 27, Color.WHITE.getRGB()); +// drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.ypos"), left, virtualY + 57, Color.WHITE.getRGB()); +// drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.zpos"), left, virtualY + 87, Color.WHITE.getRGB()); +// drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.camyaw"), left + totalWidth - 100 - 5 - w2, virtualY + 27, Color.WHITE.getRGB()); +// drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.campitch"), left + totalWidth - 100 - 5 - w2, virtualY + 57, Color.WHITE.getRGB()); +// drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.camroll"), left + totalWidth - 100 - 5 - w2, virtualY + 87, Color.WHITE.getRGB()); +// } +// +// } +// +// private static class GuiEditKeyframeSpectator extends GuiEditKeyframe { +// private GuiToggleButton perspectiveButton; +// +// private GuiString shoulderDistanceString, shoulderPitchString, shoulderYawString, shoulderSmoothnessString; +// private GuiDraggingNumberInput shoulderDistanceInput, shoulderPitchOffsetInput, +// shoulderYawOffsetInput, shoulderSmoothnessInput; +// +// private ComposedElement spectatorCamSettings; +// private ComposedElement shoulderCamSettings; +// +// private DelegatingElement perspectiveSettings = new DelegatingElement() { +// @Override +// public GuiElement delegate() { +// switch(perspectiveButton.getValue()) { +// case 0: +// return spectatorCamSettings; +// case 1: +// return shoulderCamSettings; +// default: +// return null; +// } +// } +// }; +// +// public GuiEditKeyframeSpectator(Keyframe keyframe) { +// super(keyframe, ReplayHandler.getPositionKeyframes()); +// screenTitle = I18n.format("replaymod.gui.editkeyframe.title.spec"); +// } +// +// @Override +// public void initGui() { +// super.initGui(); +// +// if (!initialized) { +// SpectatorData data = (SpectatorData)keyframe.getValue(); +// +// perspectiveButton = new GuiToggleButton(0, 0, 0, 200, 20, I18n.format("replaymod.gui.editkeyframe.spec.method")+": ", new String[]{ +// I18n.format("replaymod.gui.editkeyframe.spec.method.firstperson"), +// I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder")}); +// +// perspectiveButton.setValue(Arrays.asList(SpectatorData.SpectatingMethod.values()).indexOf(data.getSpectatingMethod())); +// +// spectatorCamSettings = new ComposedElement(); +// +// //create elements in shoulderCamSettings +// shoulderDistanceString = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder.distance")); +// shoulderPitchString = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder.pitch")); +// shoulderYawString = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder.yaw")); +// shoulderSmoothnessString = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.editkeyframe.spec.method.shoulder.smoothness")); +// +// shoulderDistanceInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 0, 0d, 30d, data.getThirdPersonInfo().shoulderCamDistance, true, "", 0.1); +// shoulderPitchOffsetInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 0, -90d, 90d, data.getThirdPersonInfo().shoulderCamPitchOffset, false, "°", 1); +// shoulderYawOffsetInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 0, null, null, data.getThirdPersonInfo().shoulderCamYawOffset, false, "°", 1); +// shoulderSmoothnessInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 0, 0.1d, 3d, data.getThirdPersonInfo().shoulderCamSmoothness, true, "", 0.1); +// +// shoulderCamSettings = new ComposedElement( +// shoulderDistanceString, shoulderDistanceInput, +// shoulderPitchString, shoulderPitchOffsetInput, +// shoulderYawString, shoulderYawOffsetInput, +// shoulderSmoothnessString, shoulderSmoothnessInput); +// +// inputs.addPart(new ComposedElement(perspectiveButton, perspectiveSettings)); +// } +// +// perspectiveButton.xPosition = (this.width-perspectiveButton.width())/2; +// perspectiveButton.yPosition = this.virtualY + 20; +// +// int verticalSpacing = 20 + 5; +// int totalWidth = 300; +// int elementWidth = 145; +// int horizontalSpacing = 10; +// +// int y = perspectiveButton.yPos() + verticalSpacing; +// +// int i = 0; +// for(GuiElement el : shoulderCamSettings.getParts()) { +// el.xPos((width-totalWidth)/2 + (i%2)*(elementWidth + horizontalSpacing)); +// el.width(elementWidth); +// +// el.yPos(y + ((i+1)%2)*7); +// +// if(i%2 == 1) { +// y += verticalSpacing; +// } +// i++; +// } +// +// initialized = true; +// } +// +// @Override +// public void onGuiClosed() { +// SpectatorData data = (SpectatorData)keyframe.getValue(); +// data.setSpectatingMethod(SpectatorData.SpectatingMethod.values()[perspectiveButton.getValue()]); +// data.getThirdPersonInfo().shoulderCamDistance = shoulderDistanceInput.getPreciseValue(); +// data.getThirdPersonInfo().shoulderCamPitchOffset = shoulderPitchOffsetInput.getIntValue(); +// data.getThirdPersonInfo().shoulderCamYawOffset = shoulderYawOffsetInput.getIntValue(); +// data.getThirdPersonInfo().shoulderCamSmoothness = shoulderSmoothnessInput.getPreciseValue(); +// super.onGuiClosed(); +// } +// } +// +// private static class KeyframeValueChangeListener implements NumberValueChangeListener { +// +// private GuiEditKeyframe parent; +// +// public KeyframeValueChangeListener(GuiEditKeyframe parent) { +// this.parent = parent; +// +// parent.min.addValueChangeListener(this); +// parent.sec.addValueChangeListener(this); +// parent.ms.addValueChangeListener(this); +// } +// +// @Override +// public void onValueChange(double value) { +// int realTimestamp = TimestampUtils.calculateTimestamp(parent.min.getIntValue(), parent.sec.getIntValue(), parent.ms.getIntValue()); +// parent.keyframe.setRealTimestamp(realTimestamp); +// +// ReplayHandler.fireKeyframesModifyEvent(); +// } +// } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiKeyframeRepository.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiKeyframeRepository.java index d9428856..059b9a82 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiKeyframeRepository.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiKeyframeRepository.java @@ -1,13 +1,9 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.elements.GuiEntryList; import eu.crushedpixel.replaymod.gui.elements.listeners.SelectionListener; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; -import eu.crushedpixel.replaymod.holders.Keyframe; import eu.crushedpixel.replaymod.holders.KeyframeSet; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.utils.CameraPathValidator; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; @@ -22,7 +18,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class GuiKeyframeRepository extends GuiScreen implements GuiReplayOverlay.NoOverlay { +public class GuiKeyframeRepository extends GuiScreen { private boolean initialized = false; @@ -133,39 +129,40 @@ public class GuiKeyframeRepository extends GuiScreen implements GuiReplayOverlay @Override public void actionPerformed(GuiButton button) { - if(!button.enabled) return; - switch(button.id) { - case GuiConstants.KEYFRAME_REPOSITORY_ADD_BUTTON: - List kfs = new ArrayList(ReplayHandler.getAllKeyframes()); - - Keyframe[] keyframes = kfs.toArray(new Keyframe[ReplayHandler.getAllKeyframes().size()]); - KeyframeSet newSet = new KeyframeSet(I18n.format("replaymod.gui.keyframerepository.preset.defaultname"), keyframes, ReplayHandler.getCustomImageObjects()); - - try { - CameraPathValidator.validateCameraPath(ReplayHandler.getPositionKeyframes(), ReplayHandler.getTimeKeyframes()); - } catch(CameraPathValidator.InvalidCameraPathException e) { - message = e.getLocalizedMessage(); - break; - } - - if(keyframeSetList.getCopyOfElements().contains(newSet)) { - message = I18n.format("replaymod.gui.keyframerepository.duplicate"); - break; - } - message = null; - - keyframeSetList.addElement(newSet); - keyframeSetList.setSelectionIndex(keyframeSetList.getEntryCount()-1); - break; - case GuiConstants.KEYFRAME_REPOSITORY_REMOVE_BUTTON: - keyframeSetList.removeElement(keyframeSetList.getSelectionIndex()); - break; - case GuiConstants.KEYFRAME_REPOSITORY_LOAD_BUTTON: - ReplayHandler.useKeyframePreset(keyframeSetList.getElement(keyframeSetList.getSelectionIndex())); - saveOnQuit(); - mc.displayGuiScreen(null); - break; - } + // TODO +// if(!button.enabled) return; +// switch(button.id) { +// case GuiConstants.KEYFRAME_REPOSITORY_ADD_BUTTON: +// List kfs = new ArrayList(ReplayHandler.getAllKeyframes()); +// +// Keyframe[] keyframes = kfs.toArray(new Keyframe[ReplayHandler.getAllKeyframes().size()]); +// KeyframeSet newSet = new KeyframeSet(I18n.format("replaymod.gui.keyframerepository.preset.defaultname"), keyframes, ReplayHandler.getCustomImageObjects()); +// +// try { +// CameraPathValidator.validateCameraPath(ReplayHandler.getPositionKeyframes(), ReplayHandler.getTimeKeyframes()); +// } catch(CameraPathValidator.InvalidCameraPathException e) { +// message = e.getLocalizedMessage(); +// break; +// } +// +// if(keyframeSetList.getCopyOfElements().contains(newSet)) { +// message = I18n.format("replaymod.gui.keyframerepository.duplicate"); +// break; +// } +// message = null; +// +// keyframeSetList.addElement(newSet); +// keyframeSetList.setSelectionIndex(keyframeSetList.getEntryCount()-1); +// break; +// case GuiConstants.KEYFRAME_REPOSITORY_REMOVE_BUTTON: +// keyframeSetList.removeElement(keyframeSetList.getSelectionIndex()); +// break; +// case GuiConstants.KEYFRAME_REPOSITORY_LOAD_BUTTON: +// ReplayHandler.useKeyframePreset(keyframeSetList.getElement(keyframeSetList.getSelectionIndex())); +// saveOnQuit(); +// mc.displayGuiScreen(null); +// break; +// } } @Override @@ -241,7 +238,8 @@ public class GuiKeyframeRepository extends GuiScreen implements GuiReplayOverlay if(initialKeyframeSets.equals(copy)) return; this.keyframeRepository = copy.toArray(new KeyframeSet[copy.size()]); - ReplayHandler.setKeyframeRepository(keyframeRepository, true); + // TODO +// ReplayHandler.setKeyframeRepository(keyframeRepository, true); } @Override diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiMouseInput.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiMouseInput.java deleted file mode 100755 index e04f61f2..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiMouseInput.java +++ /dev/null @@ -1,53 +0,0 @@ -package eu.crushedpixel.replaymod.gui; - -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; -import org.lwjgl.input.Mouse; - -import java.io.IOException; - -public class GuiMouseInput extends GuiScreen { - - private final Minecraft mc = Minecraft.getMinecraft(); - - private final GuiReplayOverlay overlay; - - private boolean shouldClose = false; - - public GuiMouseInput(GuiReplayOverlay overlay) { - this.overlay = overlay; - Mouse.setGrabbed(false); - Mouse.setCursorPosition(mc.displayWidth/2, mc.displayHeight/2); - - if(mc.currentScreen instanceof GuiMouseInput) { - shouldClose = true; - } - } - - @Override - public void initGui() { - if(shouldClose) mc.displayGuiScreen(null); - } - - @Override - protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { - overlay.mouseClicked(mouseX, mouseY, mouseButton); - } - - @Override - protected void mouseReleased(int mouseX, int mouseY, int state) { - overlay.mouseReleased(mouseX, mouseY, state); - } - - @Override - protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) { - overlay.mouseDrag(mouseX, mouseY, clickedMouseButton); - } - - @Override - public void onGuiClosed() { - ReplayMod.overlay.closeToolbar(); - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java index 3c436fc0..fe8156e1 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java @@ -1,723 +1,698 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.assets.CustomImageObject; -import eu.crushedpixel.replaymod.assets.ReplayAsset; -import eu.crushedpixel.replaymod.gui.elements.*; -import eu.crushedpixel.replaymod.gui.elements.listeners.NumberValueChangeListener; -import eu.crushedpixel.replaymod.gui.elements.listeners.SelectionListener; -import eu.crushedpixel.replaymod.gui.elements.timelines.GuiTimeline; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; -import eu.crushedpixel.replaymod.holders.*; -import eu.crushedpixel.replaymod.interpolation.KeyframeList; -import eu.crushedpixel.replaymod.interpolation.KeyframeValue; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.utils.MouseUtils; -import lombok.Getter; -import lombok.Setter; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.resources.I18n; -import org.lwjgl.input.Keyboard; -import org.lwjgl.util.Point; -import java.awt.*; -import java.io.IOException; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.UUID; - -public class GuiObjectManager extends GuiScreen implements GuiReplayOverlay.NoOverlay { - - private boolean initialized = false; - - private GuiEntryList objectList; - private GuiAdvancedButton addButton, removeButton; - - private GuiAdvancedTextField nameInput; - - private GuiString dropdownLabel; - private GuiDropdown> assetDropdown; - - private final List initialObjects = ReplayHandler.getCustomImageObjects(); - - private GuiDraggingNumberInput anchorXInput, anchorYInput, anchorZInput; - private GuiDraggingNumberInput positionXInput, positionYInput, positionZInput; - private GuiDraggingNumberInput orientationXInput, orientationYInput, orientationZInput; - private GuiDraggingNumberInput scaleXInput, scaleYInput, scaleZInput; - private GuiDraggingNumberInput opacityInput; - - private NumberPositionInputGroup anchorNumberInputs, positionNumberInputs, orientationNumberInputs, scaleNumberInputs; - private NumberValueInputGroup opacityNumberInputs; - - @Getter - private GuiObjectKeyframeTimeline objectKeyframeTimeline; - - private ComposedElement disableElements, allElements; - - private static final int KEYFRAME_BUTTON_X = 80; - private static final int KEYFRAME_BUTTON_Y = 40; - - private static final float ZOOM_STEPS = 0.05f; - - private DelegatingElement keyframeButton(final int x, final int y, final int line) { - return new DelegatingElement() { - - private GuiTexturedButton normal = new GuiTexturedButton(0, x, y, 20, 20, - GuiReplayOverlay.replay_gui, KEYFRAME_BUTTON_X, KEYFRAME_BUTTON_Y, - GuiReplayOverlay.TEXTURE_SIZE, GuiReplayOverlay.TEXTURE_SIZE, new Runnable() { - @Override - public void run() { - objectKeyframeTimeline.addKeyframe(line); - } - }, - null); - - private GuiTexturedButton selected = new GuiTexturedButton(0, x, y, 20, 20, - GuiReplayOverlay.replay_gui, KEYFRAME_BUTTON_X, KEYFRAME_BUTTON_Y+20, - GuiReplayOverlay.TEXTURE_SIZE, GuiReplayOverlay.TEXTURE_SIZE, new Runnable() { - @Override - public void run() { - objectKeyframeTimeline.removeKeyframe(line); - } - }, - null); - - @Override - public GuiElement delegate() { - if(objectKeyframeTimeline.getSelectedKeyframeRow() == line) { - return selected; - } else { - return normal; - } - } - - @Override - public void setElementEnabled(boolean enabled) { - selected.setElementEnabled(enabled); - normal.setElementEnabled(enabled); - } - }; - } - - @Override - public void initGui() { - if(!initialized) { - anchorXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true, "", 0.1); - anchorYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true, "", 0.1); - anchorZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true, "", 0.1); - anchorNumberInputs = new NumberPositionInputGroup(null, anchorXInput, anchorYInput, anchorZInput); - - positionXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); - positionYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); - positionZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); - positionNumberInputs = new NumberPositionInputGroup(null, positionXInput, positionYInput, positionZInput); - - orientationXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); - orientationYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); - orientationZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); - orientationNumberInputs = new NumberPositionInputGroup(null, orientationXInput, orientationYInput, orientationZInput); - - scaleXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%", 0.5); - scaleYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%", 0.5); - scaleZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%", 0.5); - scaleNumberInputs = new NumberPositionInputGroup(null, scaleXInput, scaleYInput, scaleZInput); - - opacityInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, 0d, 100d, 100d, true, "%", 0.5); - opacityNumberInputs = new NumberValueInputGroup(null, opacityInput); - - dropdownLabel = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.assets.filechooser")+": "); - assetDropdown = new GuiDropdown>(fontRendererObj, 0, 0, 0, 5); - List assets = ReplayHandler.getAssetRepository().getCopyOfReplayAssets(); - Collections.sort(assets, new Comparator() { - @Override - public int compare(ReplayAsset o1, ReplayAsset o2) { - return o1.getAssetName().compareTo(o2.getAssetName()); - } - }); - if(assets.isEmpty()) { - assetDropdown.addElement(new GuiEntryListValueEntry(I18n.format("replaymod.gui.assets.emptylist"), null)); - } else { - assetDropdown.addElement(new GuiEntryListValueEntry(I18n.format("replaymod.gui.assets.noselection"), null)); - for(ReplayAsset asset : assets) { - assetDropdown.addElement(new GuiEntryListValueEntry( - asset.getDisplayString(), ReplayHandler.getAssetRepository().getUUIDForAsset(asset))); - } - } - - objectList = new GuiEntryList(fontRendererObj, 0, 0, 0, 0); - objectList.setEmptyMessage(I18n.format("replaymod.gui.objects.empty")); - - addButton = new GuiAdvancedButton(0, 0, 0, 20, I18n.format("replaymod.gui.add"), new Runnable() { - @Override - public void run() { - try { - CustomImageObject customImageObject = new CustomImageObject(I18n.format("replaymod.gui.objects.defaultname"), null); - - Position defaultPosition = new Position(mc.getRenderViewEntity()); - customImageObject.getTransformations().setDefaultPosition(defaultPosition); - - objectList.addElement(customImageObject); - } catch(IOException e) { - e.printStackTrace(); - } - } - }, null); - - removeButton = new GuiAdvancedButton(0, 0, 0, 20, I18n.format("replaymod.gui.remove"), new Runnable() { - @Override - public void run() { - if(objectList.getElement(objectList.getSelectionIndex()) != null) - objectList.removeElement(objectList.getSelectionIndex()); - } - }, null); - - nameInput = new GuiAdvancedTextField(fontRendererObj, 0, 0, 0, 20); - nameInput.hint = I18n.format("replaymod.gui.objects.properties.name"); - - for(CustomImageObject customImageObject : initialObjects) { - try { - objectList.addElement(customImageObject.copy()); - } catch(IOException ioe) { - ioe.printStackTrace(); - } - } - - objectList.addSelectionListener(new SelectionListener() { - @Override - public void onSelectionChanged(int selectionIndex) { - CustomImageObject selectedObject = objectList.getElement(selectionIndex); - if(selectedObject != null) { - disableElements.setElementEnabled(true); - - nameInput.setText(selectedObject.getName()); - - //setting the dropdown value - int sel = 0; - if(selectedObject.getLinkedAsset() != null) { - int i = 0; - for(GuiEntryListValueEntry entry : assetDropdown.getAllElements()) { - if(selectedObject.getLinkedAsset().equals(entry.getValue())) { - sel = i; - break; - } - i++; - } - } - - assetDropdown.setSelectionIndexQuietly(sel); - - Transformations transformations = selectedObject.getTransformations(); - - objectKeyframeTimeline.setTransformations(transformations); - - anchorNumberInputs.setUnderlyingKeyframeList(transformations.getAnchorKeyframes()); - positionNumberInputs.setUnderlyingKeyframeList(transformations.getPositionKeyframes()); - orientationNumberInputs.setUnderlyingKeyframeList(transformations.getOrientationKeyframes()); - scaleNumberInputs.setUnderlyingKeyframeList(transformations.getScaleKeyframes()); - opacityNumberInputs.setUnderlyingKeyframeList(transformations.getOpacityKeyframes()); - - updateValuesForTransformation(objectKeyframeTimeline.getTransformations().getTransformationForTimestamp(objectKeyframeTimeline.cursorPosition)); - } else { - disableElements.setElementEnabled(false); - } - } - }); - - assetDropdown.addSelectionListener(new SelectionListener() { - @Override - public void onSelectionChanged(int selectionIndex) { - CustomImageObject selectedObject = objectList.getElement(objectList.getSelectionIndex()); - GuiEntryListValueEntry entry = assetDropdown.getElement(selectionIndex); - if(selectedObject != null && entry != null) { - try { - selectedObject.setLinkedAsset(entry.getValue()); - } catch(IOException e) { - e.printStackTrace(); - } - } - } - }); - } - - String[] labelStrings = new String[] { - I18n.format("replaymod.gui.objects.properties.anchor"), - I18n.format("replaymod.gui.objects.properties.position"), - I18n.format("replaymod.gui.objects.properties.orientation"), - I18n.format("replaymod.gui.objects.properties.scale"), - I18n.format("replaymod.gui.objects.properties.opacity"), - }; - - int maxStringWidth = 0; - for(String label : labelStrings) { - int stringWidth = fontRendererObj.getStringWidth(label); - if(stringWidth > maxStringWidth) { - maxStringWidth = stringWidth; - } - } - - disableElements = new ComposedElement(); - allElements = new ComposedElement(); - - int inputWidth = 40; - - ComposedElement anchorInputs = new ComposedElement(anchorXInput, anchorYInput, anchorZInput); - ComposedElement positionInputs = new ComposedElement(positionXInput, positionYInput, positionZInput); - ComposedElement orientationInputs = new ComposedElement(orientationXInput, orientationYInput, orientationZInput); - ComposedElement scaleInputs = new ComposedElement(scaleXInput, scaleYInput, scaleZInput); - ComposedElement opacityInputs = new ComposedElement(opacityInput); - ComposedElement numberInputs = new ComposedElement(anchorInputs, positionInputs, orientationInputs, scaleInputs, opacityInputs); - - for(int i = numberInputs.getParts().size()-1; i >= 0; i--) { - int yPos = this.height-5-10-(25*(numberInputs.getParts().size()-i)); - DelegatingElement button = keyframeButton(10, yPos, i); - GuiString label = new GuiString(35, yPos + 6, Color.WHITE, labelStrings[i]); - - ComposedElement child = (ComposedElement) numberInputs.getParts().get(i); - int x = 0; - for(GuiElement el : child.getParts()) { - GuiDraggingNumberInput dni = (GuiDraggingNumberInput)el; - dni.width = inputWidth; - dni.xPosition = 35+maxStringWidth+5 + (x*(inputWidth+5)); - dni.yPosition = yPos; - x++; - } - - child.addPart(button); - child.addPart(label); - - disableElements.addPart(child); - } - - int timelineX = anchorZInput.xPosition + anchorZInput.width + 5 - 1; - int timelineY = anchorZInput.yPosition - 1; - - int timelineWidth = this.width-10-timelineX + 2; - int timelineHeight = this.height-10-10-timelineY + 2; - - objectKeyframeTimeline = new GuiObjectKeyframeTimeline(timelineX, timelineY, timelineWidth, timelineHeight); - disableElements.addPart(objectKeyframeTimeline); - - GuiScrollbar timelineScrollbar = new GuiScrollbar(timelineX, this.height - 15, timelineWidth - 21) { - @Override - public void dragged() { - objectKeyframeTimeline.timeStart = (float) sliderPosition; - } - }; - - timelineScrollbar.size = objectKeyframeTimeline.zoom; - timelineScrollbar.sliderPosition = objectKeyframeTimeline.timeStart; - - disableElements.addPart(timelineScrollbar); - - GuiTexturedButton zoomInButton = GuiReplayOverlay.texturedButton(width - 28, this.height - 15, 40, 20, 9, new Runnable() { - @Override - public void run() { - objectKeyframeTimeline.zoom = Math.max(0.025f, objectKeyframeTimeline.zoom - ZOOM_STEPS); - } - }, "replaymod.gui.ingame.menu.zoomin"); - - GuiTexturedButton zoomOutButton = GuiReplayOverlay.texturedButton(width - 18, this.height - 15, 40, 30, 9, new Runnable() { - - @Override - public void run() { - objectKeyframeTimeline.zoom = Math.min(1f, objectKeyframeTimeline.zoom + ZOOM_STEPS); - objectKeyframeTimeline.timeStart = Math.min(objectKeyframeTimeline.timeStart, 1f - objectKeyframeTimeline.zoom); - } - }, "replaymod.gui.ingame.menu.zoomout"); - - disableElements.addPart(zoomInButton); - disableElements.addPart(zoomOutButton); - - objectList.xPosition = 11; - objectList.yPosition = 11; - objectList.width = width/2 - 11 - 5; - - int visibleElements = (int)((anchorXInput.yPosition-5 - objectList.yPosition)/14f); - objectList.setVisibleElements(visibleElements); - - allElements.addPart(objectList); - - nameInput.xPosition = width/2 + 5; - nameInput.yPosition = objectList.yPosition; - nameInput.width = objectList.width; - - disableElements.addPart(nameInput); - - dropdownLabel.positionX = (width/2)+5; - int strWidth = fontRendererObj.getStringWidth(I18n.format("replaymod.gui.assets.filechooser")+": "); - - assetDropdown.xPosition = dropdownLabel.positionX+strWidth+5; - assetDropdown.yPosition = objectList.yPosition+25; - dropdownLabel.positionY = assetDropdown.yPosition + 6; - assetDropdown.width = (objectList.width-strWidth-5); - - disableElements.addPart(dropdownLabel); - disableElements.addPart(assetDropdown); - - addButton.xPosition = nameInput.xPosition; - addButton.width = removeButton.width = nameInput.width/2 - 2; - removeButton.xPosition = addButton.xPosition + nameInput.width/2 + 2; - addButton.yPosition = removeButton.yPosition = objectList.yPosition+objectList.height-25; - - allElements.addPart(addButton); - disableElements.addPart(removeButton); - - allElements.addPart(disableElements); - - objectList.setSelectionIndex(objectList.getSelectionIndex()); // trigger an event for the SelectionListener - - initialized = true; - } - - private void saveOnQuit() { - List objects = objectList.getCopyOfElements(); - - if(objects.equals(initialObjects)) { - return; - } - - ReplayHandler.setCustomImageObjects(objects); - - } - - private void updateValuesForTransformation(Transformation transformation) { - anchorXInput.setValueQuietly(transformation.getAnchor().getX()); - anchorYInput.setValueQuietly(transformation.getAnchor().getY()); - anchorZInput.setValueQuietly(transformation.getAnchor().getZ()); - - positionXInput.setValueQuietly(transformation.getPosition().getX()); - positionYInput.setValueQuietly(transformation.getPosition().getY()); - positionZInput.setValueQuietly(transformation.getPosition().getZ()); - - orientationXInput.setValueQuietly(transformation.getOrientation().getX()); - orientationYInput.setValueQuietly(transformation.getOrientation().getY()); - orientationZInput.setValueQuietly(transformation.getOrientation().getZ()); - - scaleXInput.setValueQuietly(transformation.getScale().getX()); - scaleYInput.setValueQuietly(transformation.getScale().getY()); - scaleZInput.setValueQuietly(transformation.getScale().getZ()); - - opacityInput.setValueQuietly(transformation.getOpacity()); - } - - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { - this.drawDefaultBackground(); - - allElements.draw(mc, mouseX, mouseY); - - GlStateManager.enableBlend(); - - super.drawScreen(mouseX, mouseY, partialTicks); - } - - @Override - protected void keyTyped(char typedChar, int keyCode) throws IOException { - Point mousePos = MouseUtils.getMousePos(); - allElements.buttonPressed(mc, mousePos.getX(), mousePos.getY(), typedChar, keyCode); - - CustomImageObject selectedObject = objectList.getElement(objectList.getSelectionIndex()); - if(selectedObject != null) { - selectedObject.setName(nameInput.getText().trim()); - } - - if(keyCode == Keyboard.KEY_DELETE) { - if(objectKeyframeTimeline.getSelectedKeyframe() != null) { - objectKeyframeTimeline.removeKeyframe(objectKeyframeTimeline.getSelectedKeyframeRow()); - } - } - - super.keyTyped(typedChar, keyCode); - } - - @Override - public void updateScreen() { - allElements.tick(mc); - super.updateScreen(); - } - - @Override - protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { - allElements.mouseClick(mc, mouseX, mouseY, mouseButton); - super.mouseClicked(mouseX, mouseY, mouseButton); - } - - @Override - protected void mouseClickMove(int mouseX, int mouseY, int mouseButton, long timeSinceLastClick) { - allElements.mouseDrag(mc, mouseX, mouseY, mouseButton); - super.mouseClickMove(mouseX, mouseY, mouseButton, timeSinceLastClick); - } - - @Override - protected void mouseReleased(int mouseX, int mouseY, int mouseButton) { - allElements.mouseRelease(mc, mouseX, mouseY, mouseButton); - super.mouseReleased(mouseX, mouseY, mouseButton); - } - - @Override - public void onGuiClosed() { - saveOnQuit(); - } - - public abstract class NumberInputGroup implements NumberValueChangeListener { - - KeyframeList toModify; - - public void setUnderlyingKeyframeList(KeyframeList toModify) { - this.toModify = toModify; - } - - } - - public class NumberValueInputGroup extends NumberInputGroup { - - public NumberValueInputGroup(KeyframeList toModify, GuiNumberInput input) { - this.toModify = toModify; - - input.addValueChangeListener(this); - } - - @Override - @SuppressWarnings("unchecked") - public void onValueChange(double value) { - NumberValue numberValue = new NumberValue(value); - - //if keyframe selected, overwrite its value - if(toModify.contains(objectKeyframeTimeline.getSelectedKeyframe())) { - objectKeyframeTimeline.getSelectedKeyframe().setValue(numberValue); - } else { - toModify.add(new Keyframe(objectKeyframeTimeline.cursorPosition, numberValue)); - } - } - } - - public class NumberPositionInputGroup extends NumberInputGroup { - - public NumberPositionInputGroup(KeyframeList toModify, GuiNumberInput xInput, GuiNumberInput yInput, GuiNumberInput zInput) { - this.toModify = toModify; - this.xInput = xInput; - this.yInput = yInput; - this.zInput = zInput; - - xInput.addValueChangeListener(this); - yInput.addValueChangeListener(this); - zInput.addValueChangeListener(this); - } - - private GuiNumberInput xInput, yInput, zInput; - - @Override - @SuppressWarnings("unchecked") - public void onValueChange(double value) { - Position position = new Position(xInput.getPreciseValue(), yInput.getPreciseValue(), zInput.getPreciseValue()); - - //if keyframe selected, overwrite its value - if(toModify.contains(objectKeyframeTimeline.getSelectedKeyframe())) { - objectKeyframeTimeline.getSelectedKeyframe().setValue(position); - } else { - toModify.add(new Keyframe(objectKeyframeTimeline.cursorPosition, position)); - } - } - } - - public class GuiObjectKeyframeTimeline extends GuiTimeline { - - private static final int KEYFRAME_X = 74; - private static final int KEYFRAME_Y = 35; - private static final int KEYFRAME_SIZE = 5; - - @Getter @Setter - private Transformations transformations; - - @Getter private int selectedKeyframeRow = -1; - @Getter private Keyframe selectedKeyframe = null; - - private boolean dragging = false; - - public GuiObjectKeyframeTimeline(int x, int y, int width, int height) { - super(x, y, width, height); - - this.zoom = 0.1; - this.timelineLength = 10 * 60 * 1000; - this.showMarkers = true; - - this.cursorPosition = ReplayHandler.getRealTimelineCursor(); - } - - @Override - public void draw(Minecraft mc, int mouseX, int mouseY) { - super.draw(mc, mouseX, mouseY); - - int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; - - long leftTime = Math.round(timeStart * timelineLength); - long rightTime = Math.round((timeStart + zoom) * timelineLength); - - double segmentLength = timelineLength * zoom; - - if(transformations != null) { - for(int i = 0; i < 5; i++) { - KeyframeList keyframes = transformations.getKeyframeListByID(i); - - //Draw Keyframe logos - for(Keyframe kf : keyframes) { - drawKeyframe(kf, (int)(i * (height / 5f)) + 10, bodyWidth, leftTime, rightTime, segmentLength); - } - } - } - } - - private int getKeyframeX(int timestamp, long leftTime, int bodyWidth, double segmentLength) { - long positionInSegment = timestamp - leftTime; - double fractionOfSegment = positionInSegment / segmentLength; - return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth); - } - - private void drawKeyframe(Keyframe kf, int y, int bodyWidth, long leftTime, long rightTime, double segmentLength) { - if (kf.getRealTimestamp() <= rightTime && kf.getRealTimestamp() >= leftTime) { - int textureX = KEYFRAME_X; - int textureY = KEYFRAME_Y; - y = positionY+y; - - int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); - - if (kf == selectedKeyframe) { - textureX += KEYFRAME_SIZE; - } - - rect(keyframeX - 2, y, textureX, textureY, 5, 5); - } - } - - @Override - public boolean mouseClick(Minecraft mc, int mouseX, int mouseY, int button) { - if(!enabled) return false; - super.mouseClick(mc, mouseX, mouseY, button); - int time = (int) getTimeAt(mouseX, mouseY); - if(time != -1) { - boolean clicked = false; - - int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); - - if(transformations != null) { - for(int i = 0; i < 5; i++) { - int upper = positionY + (int)(i * (height / 5f)) + 10; - int lower = upper + KEYFRAME_SIZE; - if(mouseY >= upper && mouseY <= lower) { - KeyframeList keyframes = transformations.getKeyframeListByID(i); - - selectedKeyframe = keyframes.getClosestKeyframeForTimestamp(time, tolerance); - - if(selectedKeyframe != null) { - selectedKeyframeRow = i; - } else { - selectedKeyframeRow = -1; - } - - clicked = true; - } - } - } - - if(!clicked) { - selectedKeyframe = null; - selectedKeyframeRow = -1; - } - - cursorPosition = time; - updateValuesForTransformation(getTransformations().getTransformationForTimestamp(cursorPosition)); - - dragging = true; - return true; - } - - return false; - } - - @Override - public void mouseDrag(Minecraft mc, int mouseX, int mouseY, int button) { - if(!enabled) return; - super.mouseDrag(mc, mouseX, mouseY, button); - if(dragging) { - int time = (int) getTimeAt(mouseX, mouseY); - if(time != -1) { - if(selectedKeyframe != null) { - selectedKeyframe.setRealTimestamp(time); - } - cursorPosition = time; - updateValuesForTransformation(getTransformations().getTransformationForTimestamp(cursorPosition)); - } - } - } - - @Override - public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) { - if(!enabled) return; - super.mouseRelease(mc, mouseX, mouseY, button); - this.dragging = false; - } - - @SuppressWarnings("unchecked") - public void addKeyframe(int line) { - CustomImageObject currentObject = objectList.getElement(objectList.getSelectionIndex()); - if(currentObject != null) { - KeyframeList list = currentObject.getTransformations().getKeyframeListByID(line); - int timestamp = objectKeyframeTimeline.cursorPosition; - - Keyframe kf = null; - - switch(line) { - case 0: - - kf = new Keyframe(timestamp, new Position( - anchorXInput.getPreciseValue(), - anchorYInput.getPreciseValue(), - anchorZInput.getPreciseValue())); - break; - case 1: - kf = new Keyframe(timestamp, new Position( - positionXInput.getPreciseValue(), - positionYInput.getPreciseValue(), - positionZInput.getPreciseValue())); - break; - case 2: - kf = new Keyframe(timestamp, new Position( - orientationXInput.getPreciseValue(), - orientationYInput.getPreciseValue(), - orientationZInput.getPreciseValue())); - break; - case 3: - kf = new Keyframe(timestamp, new Position( - scaleXInput.getPreciseValue(), - scaleYInput.getPreciseValue(), - scaleZInput.getPreciseValue())); - break; - case 4: - kf = new Keyframe(timestamp, new NumberValue( - opacityInput.getPreciseValue() - )); - break; - } - - list.add(kf); - - selectedKeyframe = kf; - selectedKeyframeRow = line; - } - } - - public void removeKeyframe(int line) { - if(selectedKeyframeRow == line) { - CustomImageObject currentObject = objectList.getElement(objectList.getSelectionIndex()); - if(currentObject != null) { - KeyframeList list = currentObject.getTransformations().getKeyframeListByID(line); - list.remove(selectedKeyframe); - selectedKeyframe = null; - selectedKeyframeRow = -1; - } - } - } - } +public class GuiObjectManager extends GuiScreen { + // TODO +// +// private boolean initialized = false; +// +// private GuiEntryList objectList; +// private GuiAdvancedButton addButton, removeButton; +// +// private GuiAdvancedTextField nameInput; +// +// private GuiString dropdownLabel; +// private GuiDropdown> assetDropdown; +// +// private final List initialObjects = ReplayHandler.getCustomImageObjects(); +// +// private GuiDraggingNumberInput anchorXInput, anchorYInput, anchorZInput; +// private GuiDraggingNumberInput positionXInput, positionYInput, positionZInput; +// private GuiDraggingNumberInput orientationXInput, orientationYInput, orientationZInput; +// private GuiDraggingNumberInput scaleXInput, scaleYInput, scaleZInput; +// private GuiDraggingNumberInput opacityInput; +// +// private NumberPositionInputGroup anchorNumberInputs, positionNumberInputs, orientationNumberInputs, scaleNumberInputs; +// private NumberValueInputGroup opacityNumberInputs; +// +// @Getter +// private GuiObjectKeyframeTimeline objectKeyframeTimeline; +// +// private ComposedElement disableElements, allElements; +// +// private static final int KEYFRAME_BUTTON_X = 80; +// private static final int KEYFRAME_BUTTON_Y = 40; +// +// private static final float ZOOM_STEPS = 0.05f; +// +// private DelegatingElement keyframeButton(final int x, final int y, final int line) { +// return new DelegatingElement() { +// +// private GuiTexturedButton normal = new GuiTexturedButton(0, x, y, 20, 20, +// GuiReplayOverlay.replay_gui, KEYFRAME_BUTTON_X, KEYFRAME_BUTTON_Y, +// GuiReplayOverlay.TEXTURE_SIZE, GuiReplayOverlay.TEXTURE_SIZE, new Runnable() { +// @Override +// public void run() { +// objectKeyframeTimeline.addKeyframe(line); +// } +// }, +// null); +// +// private GuiTexturedButton selected = new GuiTexturedButton(0, x, y, 20, 20, +// GuiReplayOverlay.replay_gui, KEYFRAME_BUTTON_X, KEYFRAME_BUTTON_Y+20, +// GuiReplayOverlay.TEXTURE_SIZE, GuiReplayOverlay.TEXTURE_SIZE, new Runnable() { +// @Override +// public void run() { +// objectKeyframeTimeline.removeKeyframe(line); +// } +// }, +// null); +// +// @Override +// public GuiElement delegate() { +// if(objectKeyframeTimeline.getSelectedKeyframeRow() == line) { +// return selected; +// } else { +// return normal; +// } +// } +// +// @Override +// public void setElementEnabled(boolean enabled) { +// selected.setElementEnabled(enabled); +// normal.setElementEnabled(enabled); +// } +// }; +// } +// +// @Override +// public void initGui() { +// if(!initialized) { +// anchorXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true, "", 0.1); +// anchorYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true, "", 0.1); +// anchorZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true, "", 0.1); +// anchorNumberInputs = new NumberPositionInputGroup(null, anchorXInput, anchorYInput, anchorZInput); +// +// positionXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); +// positionYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); +// positionZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); +// positionNumberInputs = new NumberPositionInputGroup(null, positionXInput, positionYInput, positionZInput); +// +// orientationXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); +// orientationYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); +// orientationZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 0d, true); +// orientationNumberInputs = new NumberPositionInputGroup(null, orientationXInput, orientationYInput, orientationZInput); +// +// scaleXInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%", 0.5); +// scaleYInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%", 0.5); +// scaleZInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, null, null, 100d, true, "%", 0.5); +// scaleNumberInputs = new NumberPositionInputGroup(null, scaleXInput, scaleYInput, scaleZInput); +// +// opacityInput = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 50, 0d, 100d, 100d, true, "%", 0.5); +// opacityNumberInputs = new NumberValueInputGroup(null, opacityInput); +// +// dropdownLabel = new GuiString(0, 0, Color.WHITE, I18n.format("replaymod.gui.assets.filechooser")+": "); +// assetDropdown = new GuiDropdown>(fontRendererObj, 0, 0, 0, 5); +// List assets = ReplayHandler.getAssetRepository().getCopyOfReplayAssets(); +// Collections.sort(assets, new Comparator() { +// @Override +// public int compare(ReplayAsset o1, ReplayAsset o2) { +// return o1.getAssetName().compareTo(o2.getAssetName()); +// } +// }); +// if(assets.isEmpty()) { +// assetDropdown.addElement(new GuiEntryListValueEntry(I18n.format("replaymod.gui.assets.emptylist"), null)); +// } else { +// assetDropdown.addElement(new GuiEntryListValueEntry(I18n.format("replaymod.gui.assets.noselection"), null)); +// for(ReplayAsset asset : assets) { +// assetDropdown.addElement(new GuiEntryListValueEntry( +// asset.getDisplayString(), ReplayHandler.getAssetRepository().getUUIDForAsset(asset))); +// } +// } +// +// objectList = new GuiEntryList(fontRendererObj, 0, 0, 0, 0); +// objectList.setEmptyMessage(I18n.format("replaymod.gui.objects.empty")); +// +// addButton = new GuiAdvancedButton(0, 0, 0, 20, I18n.format("replaymod.gui.add"), new Runnable() { +// @Override +// public void run() { +// try { +// CustomImageObject customImageObject = new CustomImageObject(I18n.format("replaymod.gui.objects.defaultname"), null); +// +// Position defaultPosition = new Position(mc.getRenderViewEntity()); +// customImageObject.getTransformations().setDefaultPosition(defaultPosition); +// +// objectList.addElement(customImageObject); +// } catch(IOException e) { +// e.printStackTrace(); +// } +// } +// }, null); +// +// removeButton = new GuiAdvancedButton(0, 0, 0, 20, I18n.format("replaymod.gui.remove"), new Runnable() { +// @Override +// public void run() { +// if(objectList.getElement(objectList.getSelectionIndex()) != null) +// objectList.removeElement(objectList.getSelectionIndex()); +// } +// }, null); +// +// nameInput = new GuiAdvancedTextField(fontRendererObj, 0, 0, 0, 20); +// nameInput.hint = I18n.format("replaymod.gui.objects.properties.name"); +// +// for(CustomImageObject customImageObject : initialObjects) { +// try { +// objectList.addElement(customImageObject.copy()); +// } catch(IOException ioe) { +// ioe.printStackTrace(); +// } +// } +// +// objectList.addSelectionListener(new SelectionListener() { +// @Override +// public void onSelectionChanged(int selectionIndex) { +// CustomImageObject selectedObject = objectList.getElement(selectionIndex); +// if(selectedObject != null) { +// disableElements.setElementEnabled(true); +// +// nameInput.setText(selectedObject.getName()); +// +// //setting the dropdown value +// int sel = 0; +// if(selectedObject.getLinkedAsset() != null) { +// int i = 0; +// for(GuiEntryListValueEntry entry : assetDropdown.getAllElements()) { +// if(selectedObject.getLinkedAsset().equals(entry.getValue())) { +// sel = i; +// break; +// } +// i++; +// } +// } +// +// assetDropdown.setSelectionIndexQuietly(sel); +// +// Transformations transformations = selectedObject.getTransformations(); +// +// objectKeyframeTimeline.setTransformations(transformations); +// +// anchorNumberInputs.setUnderlyingKeyframeList(transformations.getAnchorKeyframes()); +// positionNumberInputs.setUnderlyingKeyframeList(transformations.getPositionKeyframes()); +// orientationNumberInputs.setUnderlyingKeyframeList(transformations.getOrientationKeyframes()); +// scaleNumberInputs.setUnderlyingKeyframeList(transformations.getScaleKeyframes()); +// opacityNumberInputs.setUnderlyingKeyframeList(transformations.getOpacityKeyframes()); +// +// updateValuesForTransformation(objectKeyframeTimeline.getTransformations().getTransformationForTimestamp(objectKeyframeTimeline.cursorPosition)); +// } else { +// disableElements.setElementEnabled(false); +// } +// } +// }); +// +// assetDropdown.addSelectionListener(new SelectionListener() { +// @Override +// public void onSelectionChanged(int selectionIndex) { +// CustomImageObject selectedObject = objectList.getElement(objectList.getSelectionIndex()); +// GuiEntryListValueEntry entry = assetDropdown.getElement(selectionIndex); +// if(selectedObject != null && entry != null) { +// try { +// selectedObject.setLinkedAsset(entry.getValue()); +// } catch(IOException e) { +// e.printStackTrace(); +// } +// } +// } +// }); +// } +// +// String[] labelStrings = new String[] { +// I18n.format("replaymod.gui.objects.properties.anchor"), +// I18n.format("replaymod.gui.objects.properties.position"), +// I18n.format("replaymod.gui.objects.properties.orientation"), +// I18n.format("replaymod.gui.objects.properties.scale"), +// I18n.format("replaymod.gui.objects.properties.opacity"), +// }; +// +// int maxStringWidth = 0; +// for(String label : labelStrings) { +// int stringWidth = fontRendererObj.getStringWidth(label); +// if(stringWidth > maxStringWidth) { +// maxStringWidth = stringWidth; +// } +// } +// +// disableElements = new ComposedElement(); +// allElements = new ComposedElement(); +// +// int inputWidth = 40; +// +// ComposedElement anchorInputs = new ComposedElement(anchorXInput, anchorYInput, anchorZInput); +// ComposedElement positionInputs = new ComposedElement(positionXInput, positionYInput, positionZInput); +// ComposedElement orientationInputs = new ComposedElement(orientationXInput, orientationYInput, orientationZInput); +// ComposedElement scaleInputs = new ComposedElement(scaleXInput, scaleYInput, scaleZInput); +// ComposedElement opacityInputs = new ComposedElement(opacityInput); +// ComposedElement numberInputs = new ComposedElement(anchorInputs, positionInputs, orientationInputs, scaleInputs, opacityInputs); +// +// for(int i = numberInputs.getParts().size()-1; i >= 0; i--) { +// int yPos = this.height-5-10-(25*(numberInputs.getParts().size()-i)); +// DelegatingElement button = keyframeButton(10, yPos, i); +// GuiString label = new GuiString(35, yPos + 6, Color.WHITE, labelStrings[i]); +// +// ComposedElement child = (ComposedElement) numberInputs.getParts().get(i); +// int x = 0; +// for(GuiElement el : child.getParts()) { +// GuiDraggingNumberInput dni = (GuiDraggingNumberInput)el; +// dni.width = inputWidth; +// dni.xPosition = 35+maxStringWidth+5 + (x*(inputWidth+5)); +// dni.yPosition = yPos; +// x++; +// } +// +// child.addPart(button); +// child.addPart(label); +// +// disableElements.addPart(child); +// } +// +// int timelineX = anchorZInput.xPosition + anchorZInput.width + 5 - 1; +// int timelineY = anchorZInput.yPosition - 1; +// +// int timelineWidth = this.width-10-timelineX + 2; +// int timelineHeight = this.height-10-10-timelineY + 2; +// +// objectKeyframeTimeline = new GuiObjectKeyframeTimeline(timelineX, timelineY, timelineWidth, timelineHeight); +// disableElements.addPart(objectKeyframeTimeline); +// +// GuiScrollbar timelineScrollbar = new GuiScrollbar(timelineX, this.height - 15, timelineWidth - 21) { +// @Override +// public void dragged() { +// objectKeyframeTimeline.timeStart = (float) sliderPosition; +// } +// }; +// +// timelineScrollbar.size = objectKeyframeTimeline.zoom; +// timelineScrollbar.sliderPosition = objectKeyframeTimeline.timeStart; +// +// disableElements.addPart(timelineScrollbar); +// +// GuiTexturedButton zoomInButton = GuiReplayOverlay.texturedButton(width - 28, this.height - 15, 40, 20, 9, new Runnable() { +// @Override +// public void run() { +// objectKeyframeTimeline.zoom = Math.max(0.025f, objectKeyframeTimeline.zoom - ZOOM_STEPS); +// } +// }, "replaymod.gui.ingame.menu.zoomin"); +// +// GuiTexturedButton zoomOutButton = GuiReplayOverlay.texturedButton(width - 18, this.height - 15, 40, 30, 9, new Runnable() { +// +// @Override +// public void run() { +// objectKeyframeTimeline.zoom = Math.min(1f, objectKeyframeTimeline.zoom + ZOOM_STEPS); +// objectKeyframeTimeline.timeStart = Math.min(objectKeyframeTimeline.timeStart, 1f - objectKeyframeTimeline.zoom); +// } +// }, "replaymod.gui.ingame.menu.zoomout"); +// +// disableElements.addPart(zoomInButton); +// disableElements.addPart(zoomOutButton); +// +// objectList.xPosition = 11; +// objectList.yPosition = 11; +// objectList.width = width/2 - 11 - 5; +// +// int visibleElements = (int)((anchorXInput.yPosition-5 - objectList.yPosition)/14f); +// objectList.setVisibleElements(visibleElements); +// +// allElements.addPart(objectList); +// +// nameInput.xPosition = width/2 + 5; +// nameInput.yPosition = objectList.yPosition; +// nameInput.width = objectList.width; +// +// disableElements.addPart(nameInput); +// +// dropdownLabel.positionX = (width/2)+5; +// int strWidth = fontRendererObj.getStringWidth(I18n.format("replaymod.gui.assets.filechooser")+": "); +// +// assetDropdown.xPosition = dropdownLabel.positionX+strWidth+5; +// assetDropdown.yPosition = objectList.yPosition+25; +// dropdownLabel.positionY = assetDropdown.yPosition + 6; +// assetDropdown.width = (objectList.width-strWidth-5); +// +// disableElements.addPart(dropdownLabel); +// disableElements.addPart(assetDropdown); +// +// addButton.xPosition = nameInput.xPosition; +// addButton.width = removeButton.width = nameInput.width/2 - 2; +// removeButton.xPosition = addButton.xPosition + nameInput.width/2 + 2; +// addButton.yPosition = removeButton.yPosition = objectList.yPosition+objectList.height-25; +// +// allElements.addPart(addButton); +// disableElements.addPart(removeButton); +// +// allElements.addPart(disableElements); +// +// objectList.setSelectionIndex(objectList.getSelectionIndex()); // trigger an event for the SelectionListener +// +// initialized = true; +// } +// +// private void saveOnQuit() { +// List objects = objectList.getCopyOfElements(); +// +// if(objects.equals(initialObjects)) { +// return; +// } +// +// ReplayHandler.setCustomImageObjects(objects); +// +// } +// +// private void updateValuesForTransformation(Transformation transformation) { +// anchorXInput.setValueQuietly(transformation.getAnchor().getX()); +// anchorYInput.setValueQuietly(transformation.getAnchor().getY()); +// anchorZInput.setValueQuietly(transformation.getAnchor().getZ()); +// +// positionXInput.setValueQuietly(transformation.getPosition().getX()); +// positionYInput.setValueQuietly(transformation.getPosition().getY()); +// positionZInput.setValueQuietly(transformation.getPosition().getZ()); +// +// orientationXInput.setValueQuietly(transformation.getOrientation().getX()); +// orientationYInput.setValueQuietly(transformation.getOrientation().getY()); +// orientationZInput.setValueQuietly(transformation.getOrientation().getZ()); +// +// scaleXInput.setValueQuietly(transformation.getScale().getX()); +// scaleYInput.setValueQuietly(transformation.getScale().getY()); +// scaleZInput.setValueQuietly(transformation.getScale().getZ()); +// +// opacityInput.setValueQuietly(transformation.getOpacity()); +// } +// +// @Override +// public void drawScreen(int mouseX, int mouseY, float partialTicks) { +// this.drawDefaultBackground(); +// +// allElements.draw(mc, mouseX, mouseY); +// +// GlStateManager.enableBlend(); +// +// super.drawScreen(mouseX, mouseY, partialTicks); +// } +// +// @Override +// protected void keyTyped(char typedChar, int keyCode) throws IOException { +// Point mousePos = MouseUtils.getMousePos(); +// allElements.buttonPressed(mc, mousePos.getX(), mousePos.getY(), typedChar, keyCode); +// +// CustomImageObject selectedObject = objectList.getElement(objectList.getSelectionIndex()); +// if(selectedObject != null) { +// selectedObject.setName(nameInput.getText().trim()); +// } +// +// if(keyCode == Keyboard.KEY_DELETE) { +// if(objectKeyframeTimeline.getSelectedKeyframe() != null) { +// objectKeyframeTimeline.removeKeyframe(objectKeyframeTimeline.getSelectedKeyframeRow()); +// } +// } +// +// super.keyTyped(typedChar, keyCode); +// } +// +// @Override +// public void updateScreen() { +// allElements.tick(mc); +// super.updateScreen(); +// } +// +// @Override +// protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { +// allElements.mouseClick(mc, mouseX, mouseY, mouseButton); +// super.mouseClicked(mouseX, mouseY, mouseButton); +// } +// +// @Override +// protected void mouseClickMove(int mouseX, int mouseY, int mouseButton, long timeSinceLastClick) { +// allElements.mouseDrag(mc, mouseX, mouseY, mouseButton); +// super.mouseClickMove(mouseX, mouseY, mouseButton, timeSinceLastClick); +// } +// +// @Override +// protected void mouseReleased(int mouseX, int mouseY, int mouseButton) { +// allElements.mouseRelease(mc, mouseX, mouseY, mouseButton); +// super.mouseReleased(mouseX, mouseY, mouseButton); +// } +// +// @Override +// public void onGuiClosed() { +// saveOnQuit(); +// } +// +// public abstract class NumberInputGroup implements NumberValueChangeListener { +// +// KeyframeList toModify; +// +// public void setUnderlyingKeyframeList(KeyframeList toModify) { +// this.toModify = toModify; +// } +// +// } +// +// public class NumberValueInputGroup extends NumberInputGroup { +// +// public NumberValueInputGroup(KeyframeList toModify, GuiNumberInput input) { +// this.toModify = toModify; +// +// input.addValueChangeListener(this); +// } +// +// @Override +// @SuppressWarnings("unchecked") +// public void onValueChange(double value) { +// NumberValue numberValue = new NumberValue(value); +// +// //if keyframe selected, overwrite its value +// if(toModify.contains(objectKeyframeTimeline.getSelectedKeyframe())) { +// objectKeyframeTimeline.getSelectedKeyframe().setValue(numberValue); +// } else { +// toModify.add(new Keyframe(objectKeyframeTimeline.cursorPosition, numberValue)); +// } +// } +// } +// +// public class NumberPositionInputGroup extends NumberInputGroup { +// +// public NumberPositionInputGroup(KeyframeList toModify, GuiNumberInput xInput, GuiNumberInput yInput, GuiNumberInput zInput) { +// this.toModify = toModify; +// this.xInput = xInput; +// this.yInput = yInput; +// this.zInput = zInput; +// +// xInput.addValueChangeListener(this); +// yInput.addValueChangeListener(this); +// zInput.addValueChangeListener(this); +// } +// +// private GuiNumberInput xInput, yInput, zInput; +// +// @Override +// @SuppressWarnings("unchecked") +// public void onValueChange(double value) { +// Position position = new Position(xInput.getPreciseValue(), yInput.getPreciseValue(), zInput.getPreciseValue()); +// +// //if keyframe selected, overwrite its value +// if(toModify.contains(objectKeyframeTimeline.getSelectedKeyframe())) { +// objectKeyframeTimeline.getSelectedKeyframe().setValue(position); +// } else { +// toModify.add(new Keyframe(objectKeyframeTimeline.cursorPosition, position)); +// } +// } +// } +// +// public class GuiObjectKeyframeTimeline extends GuiTimeline { +// +// private static final int KEYFRAME_X = 74; +// private static final int KEYFRAME_Y = 35; +// private static final int KEYFRAME_SIZE = 5; +// +// @Getter @Setter +// private Transformations transformations; +// +// @Getter private int selectedKeyframeRow = -1; +// @Getter private Keyframe selectedKeyframe = null; +// +// private boolean dragging = false; +// +// public GuiObjectKeyframeTimeline(int x, int y, int width, int height) { +// super(x, y, width, height); +// +// this.zoom = 0.1; +// this.timelineLength = 10 * 60 * 1000; +// this.showMarkers = true; +// +// this.cursorPosition = ReplayHandler.getRealTimelineCursor(); +// } +// +// @Override +// public void draw(Minecraft mc, int mouseX, int mouseY) { +// super.draw(mc, mouseX, mouseY); +// +// int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; +// +// long leftTime = Math.round(timeStart * timelineLength); +// long rightTime = Math.round((timeStart + zoom) * timelineLength); +// +// double segmentLength = timelineLength * zoom; +// +// if(transformations != null) { +// for(int i = 0; i < 5; i++) { +// KeyframeList keyframes = transformations.getKeyframeListByID(i); +// +// //Draw Keyframe logos +// for(Keyframe kf : keyframes) { +// drawKeyframe(kf, (int)(i * (height / 5f)) + 10, bodyWidth, leftTime, rightTime, segmentLength); +// } +// } +// } +// } +// +// private int getKeyframeX(int timestamp, long leftTime, int bodyWidth, double segmentLength) { +// long positionInSegment = timestamp - leftTime; +// double fractionOfSegment = positionInSegment / segmentLength; +// return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth); +// } +// +// private void drawKeyframe(Keyframe kf, int y, int bodyWidth, long leftTime, long rightTime, double segmentLength) { +// if (kf.getRealTimestamp() <= rightTime && kf.getRealTimestamp() >= leftTime) { +// int textureX = KEYFRAME_X; +// int textureY = KEYFRAME_Y; +// y = positionY+y; +// +// int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); +// +// if (kf == selectedKeyframe) { +// textureX += KEYFRAME_SIZE; +// } +// +// rect(keyframeX - 2, y, textureX, textureY, 5, 5); +// } +// } +// +// @Override +// public boolean mouseClick(Minecraft mc, int mouseX, int mouseY, int button) { +// if(!enabled) return false; +// super.mouseClick(mc, mouseX, mouseY, button); +// int time = (int) getTimeAt(mouseX, mouseY); +// if(time != -1) { +// boolean clicked = false; +// +// int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); +// +// if(transformations != null) { +// for(int i = 0; i < 5; i++) { +// int upper = positionY + (int)(i * (height / 5f)) + 10; +// int lower = upper + KEYFRAME_SIZE; +// if(mouseY >= upper && mouseY <= lower) { +// KeyframeList keyframes = transformations.getKeyframeListByID(i); +// +// selectedKeyframe = keyframes.getClosestKeyframeForTimestamp(time, tolerance); +// +// if(selectedKeyframe != null) { +// selectedKeyframeRow = i; +// } else { +// selectedKeyframeRow = -1; +// } +// +// clicked = true; +// } +// } +// } +// +// if(!clicked) { +// selectedKeyframe = null; +// selectedKeyframeRow = -1; +// } +// +// cursorPosition = time; +// updateValuesForTransformation(getTransformations().getTransformationForTimestamp(cursorPosition)); +// +// dragging = true; +// return true; +// } +// +// return false; +// } +// +// @Override +// public void mouseDrag(Minecraft mc, int mouseX, int mouseY, int button) { +// if(!enabled) return; +// super.mouseDrag(mc, mouseX, mouseY, button); +// if(dragging) { +// int time = (int) getTimeAt(mouseX, mouseY); +// if(time != -1) { +// if(selectedKeyframe != null) { +// selectedKeyframe.setRealTimestamp(time); +// } +// cursorPosition = time; +// updateValuesForTransformation(getTransformations().getTransformationForTimestamp(cursorPosition)); +// } +// } +// } +// +// @Override +// public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) { +// if(!enabled) return; +// super.mouseRelease(mc, mouseX, mouseY, button); +// this.dragging = false; +// } +// +// @SuppressWarnings("unchecked") +// public void addKeyframe(int line) { +// CustomImageObject currentObject = objectList.getElement(objectList.getSelectionIndex()); +// if(currentObject != null) { +// KeyframeList list = currentObject.getTransformations().getKeyframeListByID(line); +// int timestamp = objectKeyframeTimeline.cursorPosition; +// +// Keyframe kf = null; +// +// switch(line) { +// case 0: +// +// kf = new Keyframe(timestamp, new Position( +// anchorXInput.getPreciseValue(), +// anchorYInput.getPreciseValue(), +// anchorZInput.getPreciseValue())); +// break; +// case 1: +// kf = new Keyframe(timestamp, new Position( +// positionXInput.getPreciseValue(), +// positionYInput.getPreciseValue(), +// positionZInput.getPreciseValue())); +// break; +// case 2: +// kf = new Keyframe(timestamp, new Position( +// orientationXInput.getPreciseValue(), +// orientationYInput.getPreciseValue(), +// orientationZInput.getPreciseValue())); +// break; +// case 3: +// kf = new Keyframe(timestamp, new Position( +// scaleXInput.getPreciseValue(), +// scaleYInput.getPreciseValue(), +// scaleZInput.getPreciseValue())); +// break; +// case 4: +// kf = new Keyframe(timestamp, new NumberValue( +// opacityInput.getPreciseValue() +// )); +// break; +// } +// +// list.add(kf); +// +// selectedKeyframe = kf; +// selectedKeyframeRow = line; +// } +// } +// +// public void removeKeyframe(int line) { +// if(selectedKeyframeRow == line) { +// CustomImageObject currentObject = objectList.getElement(objectList.getSelectionIndex()); +// if(currentObject != null) { +// KeyframeList list = currentObject.getTransformations().getKeyframeListByID(line); +// list.remove(selectedKeyframe); +// selectedKeyframe = null; +// selectedKeyframeRow = -1; +// } +// } +// } +// } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiPlayerOverview.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiPlayerOverview.java index b2cecf7d..318f65e9 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiPlayerOverview.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiPlayerOverview.java @@ -1,347 +1,314 @@ package eu.crushedpixel.replaymod.gui; -import com.mojang.realmsclient.util.Pair; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; -import eu.crushedpixel.replaymod.holders.PlayerVisibility; -import eu.crushedpixel.replaymod.registry.PlayerHandler; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.utils.ReplayFile; -import eu.crushedpixel.replaymod.utils.ReplayFileIO; -import eu.crushedpixel.replaymod.utils.SkinProvider; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.resources.I18n; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EnumPlayerModelParts; -import net.minecraft.potion.Potion; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.fml.client.config.GuiCheckBox; -import org.lwjgl.input.Keyboard; -import org.lwjgl.input.Mouse; -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.List; - -public class GuiPlayerOverview extends GuiScreen implements GuiReplayOverlay.NoOverlay { +public class GuiPlayerOverview extends GuiScreen { public static boolean defaultSave = false; - private List> players; - private List checkBoxes; - - private GuiCheckBox hideAllBox, showAllBox; - private GuiCheckBox rememberHidden; - - private boolean initialized = false; - - private int playerCount; - private int upperPlayer = 0; - - private int lowerBound; - - private boolean drag = false; - private int lastY = 0; - private int fitting = 0; - - private final Minecraft mc = Minecraft.getMinecraft(); - - private final String screenTitle = I18n.format("replaymod.input.playeroverview"); - - private final Set initialHiddenPlayers; - - public GuiPlayerOverview(List players) { - initialHiddenPlayers = new HashSet(PlayerHandler.getHiddenPlayers()); - - Collections.sort(players, new PlayerComparator()); - - this.players = new ArrayList>(); - this.checkBoxes = new ArrayList(); - - for(final EntityPlayer p : players) { - final ResourceLocation loc = SkinProvider.getResourceLocationForPlayerUUID(p.getUniqueID()); - this.players.add(Pair.of(p, loc)); - } - - playerCount = players.size(); - - ReplayMod.replaySender.setReplaySpeed(0); - } - - private boolean isSpectator(EntityPlayer e) { - return e.isInvisible() && e.getActivePotionEffect(Potion.invisibility) == null; - } - - @Override - protected void mouseClicked(int mouseX, int mouseY, int mouseButton) - throws IOException { - - if(fitting < playerCount) { - float visiblePerc = (float) fitting / (float) playerCount; - - int h = this.height - 32 - 32; - int offset = Math.round((upperPlayer / (fitting)) * visiblePerc * h); - - int lower = Math.round(32 + offset + (h * visiblePerc)) - 2; - - int k2 = (int) (this.width * 0.3); - - if(mouseX >= k2 - 16 && mouseX <= k2 - 12 && mouseY >= 32 - 2 + offset && mouseY <= lower) { - lastY = mouseY; - drag = true; - return; - } - } - int k2 = (int) (this.width * 0.3); - - if(mouseX >= k2 && mouseX <= (this.width * 0.6) && mouseY >= upperBound && mouseY <= lowerBound) { - int off = mouseY - upperBound; - int p = (off / 21) + upperPlayer; - ReplayHandler.spectateEntity(players.get(p).first()); - mc.displayGuiScreen(null); - } - - super.mouseClicked(mouseX, mouseY, mouseButton); - } - - @Override - protected void mouseClickMove(int mouseX, int mouseY, - int clickedMouseButton, long timeSinceLastClick) { - - if(drag) { - float step = 1f / (float) playerCount; - - int diff = mouseY - lastY; - int h = this.height - 32 - 32; - - float percDiff = (float) diff / (float) h; - if(Math.abs(percDiff) > Math.abs(step)) { - int s = (int) (percDiff / step); - lastY = mouseY; - upperPlayer += s; - if(upperPlayer > playerCount - fitting) { - upperPlayer = playerCount - fitting; - } else if(upperPlayer < 0) { - upperPlayer = 0; - } - } - } - - super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick); - } - - @Override - protected void mouseReleased(int mouseX, int mouseY, int state) { - drag = false; - - for(GuiCheckBox checkBox : checkBoxes) { - checkBox.mouseReleased(mouseX, mouseY); - } - - super.mouseReleased(mouseX, mouseY, state); - } - - @Override - public void initGui() { - upperPlayer = 0; - lowerBound = this.height - 10; - - @SuppressWarnings("unchecked") - List buttonList = this.buttonList; - - int i = 0; - for(GuiCheckBox checkBox : checkBoxes) { - checkBox.xPosition = (int)(this.width*0.7)-5; - buttonList.add(checkBox); - i++; - if(i >= fitting) break; - } - - if(!initialized) { - hideAllBox = new GuiCheckBox(GuiConstants.PLAYER_OVERVIEW_HIDE_ALL, 0, 0, "", false); - showAllBox = new GuiCheckBox(GuiConstants.PLAYER_OVERVIEW_SHOW_ALL, 0, 0, "", true); - rememberHidden = new GuiCheckBox(GuiConstants.PLAYER_OVERVIEW_REMEMBER, 0, 0, I18n.format("replaymod.gui.playeroverview.remembersettings"), defaultSave); - } - - hideAllBox.xPosition = (int)(this.width*0.7)-5; - showAllBox.xPosition = (int)(this.width*0.7)-20; - hideAllBox.yPosition = showAllBox.yPosition = 45; - - rememberHidden.xPosition = (int)(this.width*0.3); - rememberHidden.yPosition = 45; - - buttonList.add(hideAllBox); - buttonList.add(showAllBox); - buttonList.add(rememberHidden); - - initialized = true; - } - - @Override - protected void actionPerformed(GuiButton button) throws IOException { - if(button == showAllBox) { - showAllBox.setIsChecked(true); - for(Pair p : players) { - PlayerHandler.showPlayer(p.first()); - } - } else if(button == hideAllBox) { - hideAllBox.setIsChecked(false); - for(Pair p : players) { - PlayerHandler.hidePlayer(p.first()); - } - } - - if(!(button instanceof GuiCheckBox)) return; - if(button.id >= fitting) return; - if(!checkBoxes.contains(button)) return; - PlayerHandler.setIsVisible(players.get(upperPlayer + button.id).first(), ((GuiCheckBox)button).isChecked()); - } - - private static final int upperBound = 65; - - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { - this.drawCenteredString(fontRendererObj, screenTitle, this.width / 2, 5, Color.WHITE.getRGB()); - int k2 = (int)(this.width * 0.3); - int l2 = upperBound; - - drawGradientRect(k2 - 20, 20, (int) (this.width * 0.7) + 20, this.height - 30 - 2 + 10, -1072689136, -804253680); - - drawString(fontRendererObj, I18n.format("replaymod.gui.playeroverview.spectate"), k2 - 10, 30, Color.WHITE.getRGB()); - - String visibleString = I18n.format("replaymod.gui.playeroverview.visible"); - drawString(fontRendererObj, visibleString, (int) (this.width * 0.7) + 10 - fontRendererObj.getStringWidth(visibleString), 30, Color.WHITE.getRGB()); - - fitting = 0; - - int sk = 0; - for(Pair p : players) { - if(sk < upperPlayer) { - sk++; - continue; - } - boolean spec = isSpectator(p.first()); - - this.drawString(fontRendererObj, p.first().getName(), k2 + 16 + 5, l2 + 8 - (fontRendererObj.FONT_HEIGHT / 2), - spec ? Color.DARK_GRAY.getRGB() : Color.WHITE.getRGB()); - - mc.getTextureManager().bindTexture(p.second()); - - drawScaledCustomSizeModalRect(k2, l2, 8.0F, 8.0F, 8, 8, 16, 16, 64.0F, 64.0F); - if(p.first().func_175148_a(EnumPlayerModelParts.HAT)) - Gui.drawScaledCustomSizeModalRect(k2, l2, 40.0F, 8.0F, 8, 8, 16, 16, 64.0F, 64.0F); - - GlStateManager.resetColor(); - if(fitting >= checkBoxes.size()) { - checkBoxes.add(new GuiCheckBox(checkBoxes.size(), (int)(this.width*0.7)-5, l2+3, "", true)); - @SuppressWarnings("unchecked") - List buttonList = this.buttonList; - buttonList.add(checkBoxes.get(checkBoxes.size() - 1)); - } - checkBoxes.get(fitting).setIsChecked(!PlayerHandler.isHidden(p.first().getUniqueID())); - - l2 += 16 + 5; - fitting++; - if(l2 + 32 > lowerBound) { - break; - } - } - - int dw = Mouse.getDWheel(); - if(dw > 0) { - dw = -1; - } else if(dw < 0) { - dw = 1; - } - - upperPlayer = Math.max(Math.min(upperPlayer + dw, playerCount - fitting), 0); - - if(fitting < playerCount) { - float visiblePerc = ((float) fitting) / playerCount; - int barHeight = (int) (visiblePerc * (height - 32 - 32)); - - float posPerc = ((float) upperPlayer) / playerCount; - int barY = (int) (posPerc * (height - 32 - 32)); - - drawRect(k2 - 18, upperBound - 2, k2 - 10, this.height - 30 - 2, Color.BLACK.getRGB()); - drawRect(k2 - 16, upperBound+2 - 2 + barY, k2 - 12, 30+2 - 1 + barY + barHeight, Color.LIGHT_GRAY.getRGB()); - } - - int i = 0; - for(GuiCheckBox checkBox : checkBoxes) { - checkBox.drawButton(mc, mouseX, mouseY); - i++; - if(i >= fitting) break; - } - - hideAllBox.drawButton(mc, mouseX, mouseY); - showAllBox.drawButton(mc, mouseX, mouseY); - rememberHidden.drawButton(mc, mouseX, mouseY); - - if(hideAllBox.isMouseOver()) { - ReplayMod.tooltipRenderer.drawTooltip(mouseX, mouseY, I18n.format("replaymod.gui.playeroverview.hideall"), this, Color.WHITE); - } - - if(showAllBox.isMouseOver()) { - ReplayMod.tooltipRenderer.drawTooltip(mouseX, mouseY, I18n.format("replaymod.gui.playeroverview.showall"), this, Color.WHITE); - } - - if(rememberHidden.isMouseOver()) { - ReplayMod.tooltipRenderer.drawTooltip(mouseX, mouseY, I18n.format("replaymod.gui.playeroverview.remembersettings.description"), this, Color.WHITE); - } - - //this is necessary to reset the GL parameters for further GUI rendering - GlStateManager.enableBlend(); - } - - private PlayerVisibility getVisibilityInstance() { - Set hidden = PlayerHandler.getHiddenPlayers(); - return new PlayerVisibility(hidden.toArray(new UUID[hidden.size()])); - } - - - private void saveOnQuit() { - if(rememberHidden.isChecked()) { - if(initialHiddenPlayers.equals(PlayerHandler.getHiddenPlayers())) return; - try { - File f = File.createTempFile(ReplayFile.ENTRY_VISIBILITY, "json"); - ReplayFileIO.write(getVisibilityInstance(), f); - ReplayMod.replayFileAppender.registerModifiedFile(f, ReplayFile.ENTRY_VISIBILITY, ReplayHandler.getReplayFile()); - } catch(Exception e) { - e.printStackTrace(); - } - } else { - if(initialHiddenPlayers.isEmpty()) return; - ReplayMod.replayFileAppender.registerModifiedFile(null, ReplayFile.ENTRY_VISIBILITY, ReplayHandler.getReplayFile()); - } - } - - @Override - public void keyTyped(char typedChar, int keyCode) throws IOException { - if(keyCode == Keyboard.KEY_ESCAPE) { - saveOnQuit(); - super.keyTyped(typedChar, keyCode); - } - } - - private class PlayerComparator implements Comparator { - - @Override - public int compare(EntityPlayer o1, EntityPlayer o2) { - if(isSpectator(o1) && !isSpectator(o2)) { - return 1; - } else if(isSpectator(o2) && !isSpectator(o1)) { - return -1; - } else { - return o1.getName().compareToIgnoreCase(o2.getName()); - } - } - - } +// private List> players; +// private List checkBoxes; +// +// private GuiCheckBox hideAllBox, showAllBox; +// private GuiCheckBox rememberHidden; +// +// private boolean initialized = false; +// +// private int playerCount; +// private int upperPlayer = 0; +// +// private int lowerBound; +// +// private boolean drag = false; +// private int lastY = 0; +// private int fitting = 0; +// +// private final Minecraft mc = Minecraft.getMinecraft(); +// +// private final String screenTitle = I18n.format("replaymod.input.playeroverview"); +// +// private final Set initialHiddenPlayers; +// +// public GuiPlayerOverview(List players) { +// initialHiddenPlayers = new HashSet(PlayerHandler.getHiddenPlayers()); +// +// Collections.sort(players, new PlayerComparator()); +// +// this.players = new ArrayList>(); +// this.checkBoxes = new ArrayList(); +// +// for(final EntityPlayer p : players) { +// final ResourceLocation loc = SkinProvider.getResourceLocationForPlayerUUID(p.getUniqueID()); +// this.players.add(Pair.of(p, loc)); +// } +// +// playerCount = players.size(); +// +// ReplayMod.replaySender.setReplaySpeed(0); +// } +// +// private boolean isSpectator(EntityPlayer e) { +// return e.isInvisible() && e.getActivePotionEffect(Potion.invisibility) == null; +// } +// +// @Override +// protected void mouseClicked(int mouseX, int mouseY, int mouseButton) +// throws IOException { +// +// if(fitting < playerCount) { +// float visiblePerc = (float) fitting / (float) playerCount; +// +// int h = this.height - 32 - 32; +// int offset = Math.round((upperPlayer / (fitting)) * visiblePerc * h); +// +// int lower = Math.round(32 + offset + (h * visiblePerc)) - 2; +// +// int k2 = (int) (this.width * 0.3); +// +// if(mouseX >= k2 - 16 && mouseX <= k2 - 12 && mouseY >= 32 - 2 + offset && mouseY <= lower) { +// lastY = mouseY; +// drag = true; +// return; +// } +// } +// int k2 = (int) (this.width * 0.3); +// +// if(mouseX >= k2 && mouseX <= (this.width * 0.6) && mouseY >= upperBound && mouseY <= lowerBound) { +// int off = mouseY - upperBound; +// int p = (off / 21) + upperPlayer; +// // TODO +//// ReplayHandler.spectateEntity(players.get(p).first()); +// mc.displayGuiScreen(null); +// } +// +// super.mouseClicked(mouseX, mouseY, mouseButton); +// } +// +// @Override +// protected void mouseClickMove(int mouseX, int mouseY, +// int clickedMouseButton, long timeSinceLastClick) { +// +// if(drag) { +// float step = 1f / (float) playerCount; +// +// int diff = mouseY - lastY; +// int h = this.height - 32 - 32; +// +// float percDiff = (float) diff / (float) h; +// if(Math.abs(percDiff) > Math.abs(step)) { +// int s = (int) (percDiff / step); +// lastY = mouseY; +// upperPlayer += s; +// if(upperPlayer > playerCount - fitting) { +// upperPlayer = playerCount - fitting; +// } else if(upperPlayer < 0) { +// upperPlayer = 0; +// } +// } +// } +// +// super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick); +// } +// +// @Override +// protected void mouseReleased(int mouseX, int mouseY, int state) { +// drag = false; +// +// for(GuiCheckBox checkBox : checkBoxes) { +// checkBox.mouseReleased(mouseX, mouseY); +// } +// +// super.mouseReleased(mouseX, mouseY, state); +// } +// +// @Override +// public void initGui() { +// upperPlayer = 0; +// lowerBound = this.height - 10; +// +// @SuppressWarnings("unchecked") +// List buttonList = this.buttonList; +// +// int i = 0; +// for(GuiCheckBox checkBox : checkBoxes) { +// checkBox.xPosition = (int)(this.width*0.7)-5; +// buttonList.add(checkBox); +// i++; +// if(i >= fitting) break; +// } +// +// if(!initialized) { +// hideAllBox = new GuiCheckBox(GuiConstants.PLAYER_OVERVIEW_HIDE_ALL, 0, 0, "", false); +// showAllBox = new GuiCheckBox(GuiConstants.PLAYER_OVERVIEW_SHOW_ALL, 0, 0, "", true); +// rememberHidden = new GuiCheckBox(GuiConstants.PLAYER_OVERVIEW_REMEMBER, 0, 0, I18n.format("replaymod.gui.playeroverview.remembersettings"), defaultSave); +// } +// +// hideAllBox.xPosition = (int)(this.width*0.7)-5; +// showAllBox.xPosition = (int)(this.width*0.7)-20; +// hideAllBox.yPosition = showAllBox.yPosition = 45; +// +// rememberHidden.xPosition = (int)(this.width*0.3); +// rememberHidden.yPosition = 45; +// +// buttonList.add(hideAllBox); +// buttonList.add(showAllBox); +// buttonList.add(rememberHidden); +// +// initialized = true; +// } +// +// @Override +// protected void actionPerformed(GuiButton button) throws IOException { +// if(button == showAllBox) { +// showAllBox.setIsChecked(true); +// for(Pair p : players) { +// PlayerHandler.showPlayer(p.first()); +// } +// } else if(button == hideAllBox) { +// hideAllBox.setIsChecked(false); +// for(Pair p : players) { +// PlayerHandler.hidePlayer(p.first()); +// } +// } +// +// if(!(button instanceof GuiCheckBox)) return; +// if(button.id >= fitting) return; +// if(!checkBoxes.contains(button)) return; +// PlayerHandler.setIsVisible(players.get(upperPlayer + button.id).first(), ((GuiCheckBox)button).isChecked()); +// } +// +// private static final int upperBound = 65; +// +// @Override +// public void drawScreen(int mouseX, int mouseY, float partialTicks) { +// this.drawCenteredString(fontRendererObj, screenTitle, this.width / 2, 5, Color.WHITE.getRGB()); +// int k2 = (int)(this.width * 0.3); +// int l2 = upperBound; +// +// drawGradientRect(k2 - 20, 20, (int) (this.width * 0.7) + 20, this.height - 30 - 2 + 10, -1072689136, -804253680); +// +// drawString(fontRendererObj, I18n.format("replaymod.gui.playeroverview.spectate"), k2 - 10, 30, Color.WHITE.getRGB()); +// +// String visibleString = I18n.format("replaymod.gui.playeroverview.visible"); +// drawString(fontRendererObj, visibleString, (int) (this.width * 0.7) + 10 - fontRendererObj.getStringWidth(visibleString), 30, Color.WHITE.getRGB()); +// +// fitting = 0; +// +// int sk = 0; +// for(Pair p : players) { +// if(sk < upperPlayer) { +// sk++; +// continue; +// } +// boolean spec = isSpectator(p.first()); +// +// this.drawString(fontRendererObj, p.first().getName(), k2 + 16 + 5, l2 + 8 - (fontRendererObj.FONT_HEIGHT / 2), +// spec ? Color.DARK_GRAY.getRGB() : Color.WHITE.getRGB()); +// +// mc.getTextureManager().bindTexture(p.second()); +// +// drawScaledCustomSizeModalRect(k2, l2, 8.0F, 8.0F, 8, 8, 16, 16, 64.0F, 64.0F); +// if(p.first().func_175148_a(EnumPlayerModelParts.HAT)) +// Gui.drawScaledCustomSizeModalRect(k2, l2, 40.0F, 8.0F, 8, 8, 16, 16, 64.0F, 64.0F); +// +// GlStateManager.resetColor(); +// if(fitting >= checkBoxes.size()) { +// checkBoxes.add(new GuiCheckBox(checkBoxes.size(), (int)(this.width*0.7)-5, l2+3, "", true)); +// @SuppressWarnings("unchecked") +// List buttonList = this.buttonList; +// buttonList.add(checkBoxes.get(checkBoxes.size() - 1)); +// } +// checkBoxes.get(fitting).setIsChecked(!PlayerHandler.isHidden(p.first().getUniqueID())); +// +// l2 += 16 + 5; +// fitting++; +// if(l2 + 32 > lowerBound) { +// break; +// } +// } +// +// int dw = Mouse.getDWheel(); +// if(dw > 0) { +// dw = -1; +// } else if(dw < 0) { +// dw = 1; +// } +// +// upperPlayer = Math.max(Math.min(upperPlayer + dw, playerCount - fitting), 0); +// +// if(fitting < playerCount) { +// float visiblePerc = ((float) fitting) / playerCount; +// int barHeight = (int) (visiblePerc * (height - 32 - 32)); +// +// float posPerc = ((float) upperPlayer) / playerCount; +// int barY = (int) (posPerc * (height - 32 - 32)); +// +// drawRect(k2 - 18, upperBound - 2, k2 - 10, this.height - 30 - 2, Color.BLACK.getRGB()); +// drawRect(k2 - 16, upperBound+2 - 2 + barY, k2 - 12, 30+2 - 1 + barY + barHeight, Color.LIGHT_GRAY.getRGB()); +// } +// +// int i = 0; +// for(GuiCheckBox checkBox : checkBoxes) { +// checkBox.drawButton(mc, mouseX, mouseY); +// i++; +// if(i >= fitting) break; +// } +// +// hideAllBox.drawButton(mc, mouseX, mouseY); +// showAllBox.drawButton(mc, mouseX, mouseY); +// rememberHidden.drawButton(mc, mouseX, mouseY); +// +// if(hideAllBox.isMouseOver()) { +// ReplayMod.tooltipRenderer.drawTooltip(mouseX, mouseY, I18n.format("replaymod.gui.playeroverview.hideall"), this, Color.WHITE); +// } +// +// if(showAllBox.isMouseOver()) { +// ReplayMod.tooltipRenderer.drawTooltip(mouseX, mouseY, I18n.format("replaymod.gui.playeroverview.showall"), this, Color.WHITE); +// } +// +// if(rememberHidden.isMouseOver()) { +// ReplayMod.tooltipRenderer.drawTooltip(mouseX, mouseY, I18n.format("replaymod.gui.playeroverview.remembersettings.description"), this, Color.WHITE); +// } +// +// //this is necessary to reset the GL parameters for further GUI rendering +// GlStateManager.enableBlend(); +// } +// +// private void saveOnQuit() { +// try { +// // TODO +//// if(rememberHidden.isChecked()) { +//// if(initialHiddenPlayers.equals(PlayerHandler.getHiddenPlayers())) return; +//// ReplayHandler.getReplayFile().writeInvisiblePlayers(PlayerHandler.getHiddenPlayers()); +//// } else { +//// if(initialHiddenPlayers.isEmpty()) return; +//// ReplayHandler.getReplayFile().writeInvisiblePlayers(Collections.emptySet()); +//// } +// } catch(Exception e) { +// e.printStackTrace(); +// } +// } +// +// @Override +// public void keyTyped(char typedChar, int keyCode) throws IOException { +// if(keyCode == Keyboard.KEY_ESCAPE) { +// saveOnQuit(); +// super.keyTyped(typedChar, keyCode); +// } +// } +// +// private class PlayerComparator implements Comparator { +// +// @Override +// public int compare(EntityPlayer o1, EntityPlayer o2) { +// if(isSpectator(o1) && !isSpectator(o2)) { +// return 1; +// } else if(isSpectator(o2) && !isSpectator(o1)) { +// return -1; +// } else { +// return o1.getName().compareToIgnoreCase(o2.getName()); +// } +// } +// +// } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenderSettings.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenderSettings.java index 5a4025ac..f38b1c9b 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenderSettings.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenderSettings.java @@ -1,12 +1,10 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.elements.*; import eu.crushedpixel.replaymod.gui.elements.listeners.CheckBoxListener; import eu.crushedpixel.replaymod.gui.elements.listeners.SelectionListener; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; import eu.crushedpixel.replaymod.holders.GuiEntryListEntry; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.settings.EncodingPreset; import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.utils.MouseUtils; @@ -29,7 +27,7 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.List; -public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoOverlay { +public class GuiRenderSettings extends GuiScreen { private final Minecraft mc = Minecraft.getMinecraft(); @@ -542,7 +540,8 @@ public class GuiRenderSettings extends GuiScreen implements GuiReplayOverlay.NoO if(FMLClientHandler.instance().hasOptifine()) { mc.displayGuiScreen(new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"), I18n.format("replaymod.gui.rendering.error.optifine"))); } else { - ReplayHandler.startPath(options, true); + // TODO +// ReplayHandler.startPath(options, true); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySaving.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySaving.java index e96b7f7b..bfbddd93 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySaving.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySaving.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.resources.I18n; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java index 40c2c9fa..f6e2cdfb 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java @@ -1,7 +1,5 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.gui.elements.GuiSettingsOnOffButton; -import eu.crushedpixel.replaymod.gui.elements.GuiToggleButton; import eu.crushedpixel.replaymod.settings.ReplaySettings.RecordingOptions; import eu.crushedpixel.replaymod.settings.ReplaySettings.ReplayOptions; import net.minecraft.client.gui.GuiButton; @@ -12,8 +10,6 @@ import net.minecraftforge.fml.client.FMLClientHandler; import java.awt.*; import java.io.IOException; -import static eu.crushedpixel.replaymod.gui.GuiConstants.*; - public class GuiReplaySettings extends GuiScreen { protected String screenTitle = I18n.format("replaymod.gui.settings.title"); private GuiScreen parentGuiScreen; @@ -38,23 +34,23 @@ public class GuiReplaySettings extends GuiScreen { int xPos = this.width / 2 - 155 + i % 2 * 160; int yPos = this.height / 6 + 24 * (i >> 1); - - if(o == RecordingOptions.notifications) { - GuiToggleButton sendChatButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_SEND_CHAT, xPos, yPos, 150, 20, o); - buttonList.add(sendChatButton); - - } else if(o == RecordingOptions.recordServer) { - GuiToggleButton recordServerButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_RECORDSERVER_ID, xPos, yPos, 150, 20, o); - buttonList.add(recordServerButton); - - } else if(o == RecordingOptions.recordSingleplayer) { - GuiToggleButton recordSPButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_RECORDSP_ID, xPos, yPos, 150, 20, o); - buttonList.add(recordSPButton); - - } else if(o == RecordingOptions.indicator) { - GuiToggleButton showIndicatorButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_INDICATOR_ID, xPos, yPos, 150, 20, o); - buttonList.add(showIndicatorButton); - } +// TODO +// if(o == RecordingOptions.notifications) { +// GuiToggleButton sendChatButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_SEND_CHAT, xPos, yPos, 150, 20, o); +// buttonList.add(sendChatButton); +// +// } else if(o == RecordingOptions.recordServer) { +// GuiToggleButton recordServerButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_RECORDSERVER_ID, xPos, yPos, 150, 20, o); +// buttonList.add(recordServerButton); +// +// } else if(o == RecordingOptions.recordSingleplayer) { +// GuiToggleButton recordSPButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_RECORDSP_ID, xPos, yPos, 150, 20, o); +// buttonList.add(recordSPButton); +// +// } else if(o == RecordingOptions.indicator) { +// GuiToggleButton showIndicatorButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_INDICATOR_ID, xPos, yPos, 150, 20, o); +// buttonList.add(showIndicatorButton); +// } ++i; } @@ -69,14 +65,15 @@ public class GuiReplaySettings extends GuiScreen { int xPos = this.width / 2 - 155 + i % 2 * 160; int yPos = this.height / 6 + 24 * (i >> 1); - if(o == ReplayOptions.linear) { - GuiToggleButton linearButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_FORCE_LINEAR, xPos, yPos, 150, 20, o, - I18n.format("replaymod.gui.settings.interpolation.linear"), I18n.format("replaymod.gui.settings.interpolation.cubic")); - buttonList.add(linearButton); - - } else { - buttonList.add(new GuiSettingsOnOffButton(0, xPos, yPos, 150, 20, o)); - } +// TODO +// if(o == ReplayOptions.linear) { +// GuiToggleButton linearButton = new GuiSettingsOnOffButton(REPLAY_SETTINGS_FORCE_LINEAR, xPos, yPos, 150, 20, o, +// I18n.format("replaymod.gui.settings.interpolation.linear"), I18n.format("replaymod.gui.settings.interpolation.cubic")); +// buttonList.add(linearButton); +// +// } else { +// buttonList.add(new GuiSettingsOnOffButton(0, xPos, yPos, 150, 20, o)); +// } ++i; } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySpeedSlider.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySpeedSlider.java index 459874f3..f0dd6806 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySpeedSlider.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySpeedSlider.java @@ -1,12 +1,11 @@ package eu.crushedpixel.replaymod.gui; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.elements.GuiAdvancedButton; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.util.MathHelper; -import net.minecraftforge.fml.client.FMLClientHandler; public class GuiReplaySpeedSlider extends GuiAdvancedButton { @@ -74,7 +73,7 @@ public class GuiReplaySpeedSlider extends GuiAdvancedButton { l = packedFGColour; } else if(!this.enabled) { l = 10526880; - } else if(this.hovered && FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + } else if(this.hovered) { l = 16777120; } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiAdvancedButton.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiAdvancedButton.java index 20c92ac7..43d78a58 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiAdvancedButton.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiAdvancedButton.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui.elements; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiButton; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiAdvancedCheckBox.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiAdvancedCheckBox.java index f4579bc7..4ec5030e 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiAdvancedCheckBox.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiAdvancedCheckBox.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui.elements; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.elements.listeners.CheckBoxListener; import net.minecraft.client.Minecraft; import net.minecraftforge.fml.client.config.GuiCheckBox; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiArrowButton.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiArrowButton.java index ccf2691a..75cd03d7 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiArrowButton.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiArrowButton.java @@ -1,11 +1,12 @@ package eu.crushedpixel.replaymod.gui.elements; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; import eu.crushedpixel.replaymod.utils.OpenGLUtils; import lombok.AllArgsConstructor; import lombok.Getter; import net.minecraft.client.Minecraft; +import static com.replaymod.core.ReplayMod.TEXTURE; + public class GuiArrowButton extends GuiAdvancedButton { private static final int TEXTURE_X = 40; @@ -42,7 +43,7 @@ public class GuiArrowButton extends GuiAdvancedButton { try { super.draw(mc, mouseX, mouseY, hovering); - mc.getTextureManager().bindTexture(GuiReplayOverlay.replay_gui); + mc.getTextureManager().bindTexture(TEXTURE); OpenGLUtils.drawRotatedRectWithCustomSizedTexture(xPosition+4, yPosition+4, dir.getRotation(), TEXTURE_X, TEXTURE_Y, TEXTURE_WIDTH, TEXTURE_HEIGHT, 128, 128); diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiDropdown.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiDropdown.java index 7cb60f3b..5bb72e8c 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiDropdown.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiDropdown.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui.elements; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.elements.listeners.SelectionListener; import eu.crushedpixel.replaymod.holders.GuiEntryListEntry; import eu.crushedpixel.replaymod.utils.MouseUtils; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiScrollbar.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiScrollbar.java index ac2fddf3..bdaf4b3f 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiScrollbar.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiScrollbar.java @@ -6,8 +6,8 @@ import net.minecraft.client.renderer.GlStateManager; import java.awt.*; -import static eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay.TEXTURE_SIZE; -import static eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay.replay_gui; +import static com.replaymod.core.ReplayMod.TEXTURE; +import static com.replaymod.core.ReplayMod.TEXTURE_SIZE; import static org.lwjgl.opengl.GL11.GL_BLEND; import static org.lwjgl.opengl.GL11.glEnable; @@ -105,7 +105,7 @@ public class GuiScrollbar extends Gui implements GuiElement { @Override public void draw(Minecraft mc, int mouseX, int mouseY) { GlStateManager.resetColor(); - mc.renderEngine.bindTexture(replay_gui); + mc.renderEngine.bindTexture(TEXTURE); glEnable(GL_BLEND); // Background @@ -157,7 +157,7 @@ public class GuiScrollbar extends Gui implements GuiElement { if(!enabled) { GlStateManager.color(Color.GRAY.getRed()/255f, Color.GRAY.getGreen()/255f, Color.GRAY.getBlue()/255f, 1f); } - Minecraft.getMinecraft().renderEngine.bindTexture(replay_gui); + Minecraft.getMinecraft().renderEngine.bindTexture(TEXTURE); glEnable(GL_BLEND); drawModalRectWithCustomSizedTexture(x, y, u, v, width, height, TEXTURE_SIZE, TEXTURE_SIZE); diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiSettingsOnOffButton.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiSettingsOnOffButton.java index 7958bc1d..010fde76 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiSettingsOnOffButton.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiSettingsOnOffButton.java @@ -1,39 +1,39 @@ package eu.crushedpixel.replaymod.gui.elements; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.settings.ReplaySettings; +import com.replaymod.core.SettingsRegistry; public class GuiSettingsOnOffButton extends GuiOnOffButton { - private ReplaySettings.ValueEnum toChange; + private SettingsRegistry settingsRegistry; + private SettingsRegistry.SettingKey toChange; - public GuiSettingsOnOffButton(int buttonId, int x, int y, int width, int height, ReplaySettings.ValueEnum toChange) { - super(buttonId, x, y, width, height, toChange.getName()+": "); + public GuiSettingsOnOffButton(int buttonId, int x, int y, int width, int height, SettingsRegistry settingsRegistry, + SettingsRegistry.SettingKey toChange) { + super(buttonId, x, y, width, height, toChange.getDisplayString()+": "); + this.settingsRegistry = settingsRegistry; this.toChange = toChange; - if(toChange.getValue() instanceof Boolean) { - this.setValue((Boolean) toChange.getValue() ? 0 : 1); - } + this.setValue(settingsRegistry.get(toChange) ? 0 : 1); } - public GuiSettingsOnOffButton(int buttonId, int x, int y, int width, int height, ReplaySettings.ValueEnum toChange, String onValue, String offValue) { - super(buttonId, x, y, width, height, toChange.getName()+": ", onValue, offValue); + public GuiSettingsOnOffButton(int buttonId, int x, int y, int width, int height, SettingsRegistry settingsRegistry, + SettingsRegistry.SettingKey toChange, String onValue, String offValue) { + super(buttonId, x, y, width, height, toChange.getDisplayString()+": ", onValue, offValue); + this.settingsRegistry = settingsRegistry; this.toChange = toChange; - if(toChange.getValue() instanceof Boolean) { - this.setValue((Boolean) toChange.getValue() ? 0 : 1); - } + this.setValue(settingsRegistry.get(toChange) ? 0 : 1); } @Override public void toggle() { super.toggle(); - toChange.setValue(isOn()); - ReplayMod.replaySettings.rewriteSettings(); + settingsRegistry.set(toChange, isOn()); + settingsRegistry.save(); } @Override public void setValue(int value) { super.setValue(value); - toChange.setValue(isOn()); - ReplayMod.replaySettings.rewriteSettings(); + settingsRegistry.set(toChange, isOn()); + settingsRegistry.save(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiKeyframeTimeline.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiKeyframeTimeline.java index 543dee99..648056fb 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiKeyframeTimeline.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiKeyframeTimeline.java @@ -1,13 +1,6 @@ package eu.crushedpixel.replaymod.gui.elements.timelines; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.gui.GuiEditKeyframe; -import eu.crushedpixel.replaymod.holders.*; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.GlStateManager; - -import java.util.ListIterator; +import eu.crushedpixel.replaymod.holders.Keyframe; public class GuiKeyframeTimeline extends GuiTimeline { private static final int KEYFRAME_PLACE_X = 74; @@ -31,230 +24,231 @@ public class GuiKeyframeTimeline extends GuiTimeline { this.placeKeyframes = showPlaceKeyframes; } - @Override - public boolean mouseClick(Minecraft mc, int mouseX, int mouseY, int button) { - if(!enabled) return false; - - long time = getTimeAt(mouseX, mouseY); - if(time == -1) { - return false; - } - - int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); - - Keyframe closest; - if(mouseY >= positionY + BORDER_TOP + 5 && timeKeyframes) { - closest = ReplayHandler.getTimeKeyframes().getClosestKeyframeForTimestamp((int) time, tolerance); - } else if(mouseY >= positionY + BORDER_TOP && placeKeyframes) { - closest = ReplayHandler.getPositionKeyframes().getClosestKeyframeForTimestamp((int) time, tolerance); - } else { - closest = null; - } - - //left mouse button - if(button == 0) { - ReplayHandler.selectKeyframe(closest); //can be null, deselects keyframe - - // If we clicked on a key frame, then continue monitoring the mouse for movements - long currentTime = System.currentTimeMillis(); - if(closest != null) { - if(currentTime - clickTime < 500) { // if double clicked then open GUI instead - mc.displayGuiScreen(GuiEditKeyframe.create(closest)); - this.clickedKeyFrame = null; - } else { - this.clickedKeyFrame = closest; - this.dragging = false; - } - } else { // If we didn't then just update the cursor - ReplayHandler.setRealTimelineCursor((int) time); - this.dragging = true; - } - this.clickTime = currentTime; - - } else if(button == 1) { - if(closest != null) { - if(closest.getValue() instanceof AdvancedPosition) { - AdvancedPosition pos = (AdvancedPosition)closest.getValue(); - ReplayHandler.getCameraEntity().movePath(pos); - } else if(closest.getValue() instanceof TimestampValue) { - ReplayMod.overlay.performJump(((TimestampValue)closest.getValue()).asInt()); - } - } - } - - return isHovering(mouseX, mouseY); - } - - @Override - public void mouseDrag(Minecraft mc, int mouseX, int mouseY, int button) { - if(!enabled) return; - long time = getTimeAt(mouseX, mouseY); - if (time != -1) { - if (clickedKeyFrame != null) { - int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); - - if (dragging || Math.abs(clickedKeyFrame.getRealTimestamp() - time) > tolerance) { - clickedKeyFrame.setRealTimestamp((int) time); - ReplayHandler.getPositionKeyframes().sort(); - ReplayHandler.getTimeKeyframes().sort(); - dragging = true; - } - } else if (dragging) { - ReplayHandler.setRealTimelineCursor((int) time); - } - } - } - - @Override - public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) { - mouseDrag(mc, mouseX, mouseY, button); - clickedKeyFrame = null; - dragging = false; - } - - @Override - public void draw(Minecraft mc, int mouseX, int mouseY) { - super.draw(mc, mouseX, mouseY); - - int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; - - long leftTime = Math.round(timeStart * timelineLength); - long rightTime = Math.round((timeStart + zoom) * timelineLength); - - double segmentLength = timelineLength * zoom; - - //iterate over keyframes to find spectator segments - if(placeKeyframes) { - ListIterator> iterator = ReplayHandler.getPositionKeyframes().listIterator(); - while(iterator.hasNext()) { - Keyframe kf = iterator.next(); - - if(!(kf.getValue() instanceof SpectatorData)) - continue; - - int i = iterator.nextIndex(); - int nextSpectatorKeyframeRealTime = -1; - - if (iterator.hasNext()) { - Keyframe kf2 = iterator.next(); - - if(kf2.getValue() instanceof SpectatorData) { - SpectatorData spectatorData1 = (SpectatorData)kf.getValue(); - SpectatorData spectatorData2 = (SpectatorData)kf2.getValue(); - - if(spectatorData1.getSpectatedEntityID() == spectatorData2.getSpectatedEntityID()) { - nextSpectatorKeyframeRealTime = kf2.getRealTimestamp(); - } - } - } - - int i2 = iterator.previousIndex(); - - while(i2 >= i) { - iterator.previous(); - i2--; - } - - if(nextSpectatorKeyframeRealTime != -1) { - int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); - int nextX = getKeyframeX(nextSpectatorKeyframeRealTime, leftTime, bodyWidth, segmentLength); - - int color = ((SpectatorData)kf.getValue()).getSpectatingMethod().getColor(); - - drawRect(Math.max(keyframeX + 2, positionX+BORDER_LEFT), positionY + BORDER_TOP + 1, Math.min(nextX - 2, positionX+width-BORDER_RIGHT+1), - positionY + BORDER_TOP + 4, color); - - GlStateManager.color(1, 1, 1, 1); - } - } - } - - //iterate over time keyframes to find negative replay speeds - if(timeKeyframes) { - ListIterator> iterator = ReplayHandler.getTimeKeyframes().listIterator(); - while(iterator.hasNext()) { - Keyframe kf = iterator.next(); - - int i = iterator.nextIndex(); - int nextTimeKeyframeRealTime = -1; - - if (iterator.hasNext()) { - Keyframe kf2 = iterator.next(); - if(kf.getValue().asInt() > kf2.getValue().asInt()) { - nextTimeKeyframeRealTime = kf2.getRealTimestamp(); - } - } - - int i2 = iterator.previousIndex(); - - while(i2 >= i) { - iterator.previous(); - i2--; - } - - if(nextTimeKeyframeRealTime != -1) { - int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); - int nextX = getKeyframeX(nextTimeKeyframeRealTime, leftTime, bodyWidth, segmentLength); - - drawGradientRect(Math.max(keyframeX + 2, positionX+BORDER_LEFT), positionY + BORDER_TOP + 6, Math.min(nextX - 2, positionX+width-BORDER_RIGHT+1), - positionY + BORDER_TOP + 9, 0xFFFF0000, 0xFFFF0000); - } - } - } - - drawTimelineCursor(leftTime, rightTime, bodyWidth); - - - //Draw Keyframe logos - for (Keyframe kf : ReplayHandler.getAllKeyframes()) { - if (kf != null && !kf.equals(ReplayHandler.getSelectedKeyframe())) - drawKeyframe(kf, bodyWidth, leftTime, rightTime, segmentLength); - } - - if(ReplayHandler.getSelectedKeyframe() != null && !(ReplayHandler.getSelectedKeyframe().getValue() instanceof Marker)) { - drawKeyframe(ReplayHandler.getSelectedKeyframe(), bodyWidth, leftTime, rightTime, segmentLength); - } - } - - private int getKeyframeX(int timestamp, long leftTime, int bodyWidth, double segmentLength) { - long positionInSegment = timestamp - leftTime; - double fractionOfSegment = positionInSegment / segmentLength; - return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth); - } - - private void drawKeyframe(Keyframe kf, int bodyWidth, long leftTime, long rightTime, double segmentLength) { - if (kf.getRealTimestamp() <= rightTime && kf.getRealTimestamp() >= leftTime) { - int textureX; - int textureY; - int y = positionY; - - int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); - - if(kf.getValue() instanceof AdvancedPosition) { - if(!placeKeyframes) return; - textureX = KEYFRAME_PLACE_X; - textureY = KEYFRAME_PLACE_Y; - y += 0; - - //If Spectator Keyframe, use different texture - if(kf.getValue() instanceof SpectatorData) { - textureX = KEYFRAME_SPEC_X; - textureY = KEYFRAME_SPEC_Y; - } - } else if(kf.getValue() instanceof TimestampValue) { - if(!timeKeyframes) return; - textureX = KEYFRAME_TIME_X; - textureY = KEYFRAME_TIME_Y; - y += 5; - } else { - throw new UnsupportedOperationException("Unknown keyframe type: " + kf.getClass()); - } - - if (ReplayHandler.isSelected(kf)) { - textureX += 5; - } - - rect(keyframeX - 2, y + BORDER_TOP, textureX, textureY, 5, 5); - } - } + // TODO +// @Override +// public boolean mouseClick(Minecraft mc, int mouseX, int mouseY, int button) { +// if(!enabled) return false; +// +// long time = getTimeAt(mouseX, mouseY); +// if(time == -1) { +// return false; +// } +// +// int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); +// +// Keyframe closest; +// if(mouseY >= positionY + BORDER_TOP + 5 && timeKeyframes) { +// closest = ReplayHandler.getTimeKeyframes().getClosestKeyframeForTimestamp((int) time, tolerance); +// } else if(mouseY >= positionY + BORDER_TOP && placeKeyframes) { +// closest = ReplayHandler.getPositionKeyframes().getClosestKeyframeForTimestamp((int) time, tolerance); +// } else { +// closest = null; +// } +// +// //left mouse button +// if(button == 0) { +// ReplayHandler.selectKeyframe(closest); //can be null, deselects keyframe +// +// // If we clicked on a key frame, then continue monitoring the mouse for movements +// long currentTime = System.currentTimeMillis(); +// if(closest != null) { +// if(currentTime - clickTime < 500) { // if double clicked then open GUI instead +// mc.displayGuiScreen(GuiEditKeyframe.create(closest)); +// this.clickedKeyFrame = null; +// } else { +// this.clickedKeyFrame = closest; +// this.dragging = false; +// } +// } else { // If we didn't then just update the cursor +// ReplayHandler.setRealTimelineCursor((int) time); +// this.dragging = true; +// } +// this.clickTime = currentTime; +// +// } else if(button == 1) { +// if(closest != null) { +// if(closest.getValue() instanceof AdvancedPosition) { +// AdvancedPosition pos = (AdvancedPosition)closest.getValue(); +// ReplayHandler.getCameraEntity().movePath(pos); +// } else if(closest.getValue() instanceof TimestampValue) { +// ReplayMod.overlay.performJump(((TimestampValue)closest.getValue()).asInt()); +// } +// } +// } +// +// return isHovering(mouseX, mouseY); +// } +// +// @Override +// public void mouseDrag(Minecraft mc, int mouseX, int mouseY, int button) { +// if(!enabled) return; +// long time = getTimeAt(mouseX, mouseY); +// if (time != -1) { +// if (clickedKeyFrame != null) { +// int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); +// +// if (dragging || Math.abs(clickedKeyFrame.getRealTimestamp() - time) > tolerance) { +// clickedKeyFrame.setRealTimestamp((int) time); +// ReplayHandler.getPositionKeyframes().sort(); +// ReplayHandler.getTimeKeyframes().sort(); +// dragging = true; +// } +// } else if (dragging) { +// ReplayHandler.setRealTimelineCursor((int) time); +// } +// } +// } +// +// @Override +// public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) { +// mouseDrag(mc, mouseX, mouseY, button); +// clickedKeyFrame = null; +// dragging = false; +// } +// +// @Override +// public void draw(Minecraft mc, int mouseX, int mouseY) { +// super.draw(mc, mouseX, mouseY); +// +// int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; +// +// long leftTime = Math.round(timeStart * timelineLength); +// long rightTime = Math.round((timeStart + zoom) * timelineLength); +// +// double segmentLength = timelineLength * zoom; +// +// //iterate over keyframes to find spectator segments +// if(placeKeyframes) { +// ListIterator> iterator = ReplayHandler.getPositionKeyframes().listIterator(); +// while(iterator.hasNext()) { +// Keyframe kf = iterator.next(); +// +// if(!(kf.getValue() instanceof SpectatorData)) +// continue; +// +// int i = iterator.nextIndex(); +// int nextSpectatorKeyframeRealTime = -1; +// +// if (iterator.hasNext()) { +// Keyframe kf2 = iterator.next(); +// +// if(kf2.getValue() instanceof SpectatorData) { +// SpectatorData spectatorData1 = (SpectatorData)kf.getValue(); +// SpectatorData spectatorData2 = (SpectatorData)kf2.getValue(); +// +// if(spectatorData1.getSpectatedEntityID() == spectatorData2.getSpectatedEntityID()) { +// nextSpectatorKeyframeRealTime = kf2.getRealTimestamp(); +// } +// } +// } +// +// int i2 = iterator.previousIndex(); +// +// while(i2 >= i) { +// iterator.previous(); +// i2--; +// } +// +// if(nextSpectatorKeyframeRealTime != -1) { +// int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); +// int nextX = getKeyframeX(nextSpectatorKeyframeRealTime, leftTime, bodyWidth, segmentLength); +// +// int color = ((SpectatorData)kf.getValue()).getSpectatingMethod().getColor(); +// +// drawRect(Math.max(keyframeX + 2, positionX+BORDER_LEFT), positionY + BORDER_TOP + 1, Math.min(nextX - 2, positionX+width-BORDER_RIGHT+1), +// positionY + BORDER_TOP + 4, color); +// +// GlStateManager.color(1, 1, 1, 1); +// } +// } +// } +// +// //iterate over time keyframes to find negative replay speeds +// if(timeKeyframes) { +// ListIterator> iterator = ReplayHandler.getTimeKeyframes().listIterator(); +// while(iterator.hasNext()) { +// Keyframe kf = iterator.next(); +// +// int i = iterator.nextIndex(); +// int nextTimeKeyframeRealTime = -1; +// +// if (iterator.hasNext()) { +// Keyframe kf2 = iterator.next(); +// if(kf.getValue().asInt() > kf2.getValue().asInt()) { +// nextTimeKeyframeRealTime = kf2.getRealTimestamp(); +// } +// } +// +// int i2 = iterator.previousIndex(); +// +// while(i2 >= i) { +// iterator.previous(); +// i2--; +// } +// +// if(nextTimeKeyframeRealTime != -1) { +// int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); +// int nextX = getKeyframeX(nextTimeKeyframeRealTime, leftTime, bodyWidth, segmentLength); +// +// drawGradientRect(Math.max(keyframeX + 2, positionX+BORDER_LEFT), positionY + BORDER_TOP + 6, Math.min(nextX - 2, positionX+width-BORDER_RIGHT+1), +// positionY + BORDER_TOP + 9, 0xFFFF0000, 0xFFFF0000); +// } +// } +// } +// +// drawTimelineCursor(leftTime, rightTime, bodyWidth); +// +// +// //Draw Keyframe logos +// for (Keyframe kf : ReplayHandler.getAllKeyframes()) { +// if (kf != null && !kf.equals(ReplayHandler.getSelectedKeyframe())) +// drawKeyframe(kf, bodyWidth, leftTime, rightTime, segmentLength); +// } +// +// if(ReplayHandler.getSelectedKeyframe() != null) { +// drawKeyframe(ReplayHandler.getSelectedKeyframe(), bodyWidth, leftTime, rightTime, segmentLength); +// } +// } +// +// private int getKeyframeX(int timestamp, long leftTime, int bodyWidth, double segmentLength) { +// long positionInSegment = timestamp - leftTime; +// double fractionOfSegment = positionInSegment / segmentLength; +// return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth); +// } +// +// private void drawKeyframe(Keyframe kf, int bodyWidth, long leftTime, long rightTime, double segmentLength) { +// if (kf.getRealTimestamp() <= rightTime && kf.getRealTimestamp() >= leftTime) { +// int textureX; +// int textureY; +// int y = positionY; +// +// int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); +// +// if(kf.getValue() instanceof AdvancedPosition) { +// if(!placeKeyframes) return; +// textureX = KEYFRAME_PLACE_X; +// textureY = KEYFRAME_PLACE_Y; +// y += 0; +// +// //If Spectator Keyframe, use different texture +// if(kf.getValue() instanceof SpectatorData) { +// textureX = KEYFRAME_SPEC_X; +// textureY = KEYFRAME_SPEC_Y; +// } +// } else if(kf.getValue() instanceof TimestampValue) { +// if(!timeKeyframes) return; +// textureX = KEYFRAME_TIME_X; +// textureY = KEYFRAME_TIME_Y; +// y += 5; +// } else { +// throw new UnsupportedOperationException("Unknown keyframe type: " + kf.getClass()); +// } +// +// if (ReplayHandler.isSelected(kf)) { +// textureX += 5; +// } +// +// rect(keyframeX - 2, y + BORDER_TOP, textureX, textureY, 5, 5); +// } +// } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiTimeline.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiTimeline.java index 8cab99c4..7630b411 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiTimeline.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiTimeline.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui.elements.timelines; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.elements.GuiElement; import eu.crushedpixel.replaymod.utils.RoundUtils; import lombok.AllArgsConstructor; @@ -11,8 +11,8 @@ import net.minecraft.client.renderer.GlStateManager; import java.awt.*; -import static eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay.TEXTURE_SIZE; -import static eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay.replay_gui; +import static com.replaymod.core.ReplayMod.TEXTURE; +import static com.replaymod.core.ReplayMod.TEXTURE_SIZE; import static org.lwjgl.opengl.GL11.GL_BLEND; import static org.lwjgl.opengl.GL11.glEnable; @@ -76,7 +76,7 @@ public class GuiTimeline extends Gui implements GuiElement { * This draws the time at big markers above the timeline. Therefore extra space in negative y direction * should be kept empty if markers are desired. */ - public boolean showMarkers; + public boolean showMarkers; // TODO: Rename to not be confused with Marker protected final int positionX; protected final int positionY; @@ -280,7 +280,7 @@ public class GuiTimeline extends Gui implements GuiElement { if(!enabled) { GlStateManager.color(Color.GRAY.getRed() / 255f, Color.GRAY.getGreen() / 255f, Color.GRAY.getBlue() / 255f, 1f); } - Minecraft.getMinecraft().renderEngine.bindTexture(replay_gui); + Minecraft.getMinecraft().renderEngine.bindTexture(TEXTURE); glEnable(GL_BLEND); drawModalRectWithCustomSizedTexture(x, y, u, v, width, height, TEXTURE_SIZE, TEXTURE_SIZE); diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiLoginPrompt.java b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiLoginPrompt.java index 07518a31..1d205259 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiLoginPrompt.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiLoginPrompt.java @@ -6,7 +6,7 @@ import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiPasswordField; import de.johni0702.minecraft.gui.element.GuiTextField; import de.johni0702.minecraft.gui.layout.CustomLayout; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.GuiConstants; public class GuiLoginPrompt extends AbstractGuiScreen { diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiRegister.java b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiRegister.java index 83cfa255..e6df4189 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiRegister.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiRegister.java @@ -7,7 +7,7 @@ import de.johni0702.minecraft.gui.element.*; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.ApiException; import eu.crushedpixel.replaymod.gui.GuiConstants; import eu.crushedpixel.replaymod.utils.EmailAddressUtils; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiReplayCenter.java b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiReplayCenter.java index 59adec65..8bb8d1ea 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiReplayCenter.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiReplayCenter.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui.online; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.replay.SearchQuery; import eu.crushedpixel.replaymod.api.replay.holders.Category; import eu.crushedpixel.replaymod.api.replay.holders.FileInfo; @@ -12,7 +12,6 @@ import eu.crushedpixel.replaymod.api.replay.pagination.Pagination; import eu.crushedpixel.replaymod.api.replay.pagination.SearchPagination; import eu.crushedpixel.replaymod.gui.GuiConstants; import eu.crushedpixel.replaymod.gui.elements.*; -import eu.crushedpixel.replaymod.gui.replayviewer.GuiReplayViewer; import eu.crushedpixel.replaymod.holders.GuiEntryListStringEntry; import net.minecraft.client.gui.*; import net.minecraft.client.resources.I18n; @@ -265,7 +264,8 @@ public class GuiReplayCenter extends GuiScreen implements GuiYesNoCallback { } else if(button.id == GuiConstants.CENTER_LOGOUT_BUTTON) { mc.displayGuiScreen(getYesNoGui(this, LOGOUT_CALLBACK_ID)); } else if(button.id == GuiConstants.CENTER_MANAGER_BUTTON) { - mc.displayGuiScreen(new GuiReplayViewer()); + // TODO +// mc.displayGuiScreen(new GuiReplayViewer(mod)); } else if(button.id == GuiConstants.CENTER_RECENT_BUTTON) { disableTopBarButton(button.id); showOnlineRecent(); diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiReplayDownloading.java b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiReplayDownloading.java index ba660111..fab239cf 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiReplayDownloading.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiReplayDownloading.java @@ -1,15 +1,14 @@ package eu.crushedpixel.replaymod.gui.online; import com.mojang.realmsclient.gui.ChatFormatting; +import com.replaymod.core.ReplayMod; import de.johni0702.minecraft.gui.container.AbstractGuiScreen; import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.layout.CustomLayout; -import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.api.replay.holders.FileInfo; import eu.crushedpixel.replaymod.gui.elements.listeners.ProgressUpdateListener; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import java.io.File; @@ -49,7 +48,8 @@ public class GuiReplayDownloading extends AbstractGuiScreen img = archive.getThumb(); + if(img.isPresent()) { + thumb = ImageUtils.scaleImage(img.get(), new Dimension(1280, 720)); hasThumbnail = true; } @@ -307,21 +312,15 @@ public class GuiUploadFile extends GuiScreen implements ProgressUpdateListener { if(hideServerIP.isChecked()) { File tmp = File.createTempFile("replay_hidden_ip", "mcpr"); - File tmpMeta = File.createTempFile("metadata", "json"); - ReplayMetaData newMetaData = metaData.copy(); - newMetaData.removeServer(); - ReplayFileIO.write(newMetaData, tmpMeta); - - HashMap toAdd = new HashMap(); - toAdd.put(ReplayFile.ENTRY_METADATA, tmpMeta); - - FileUtils.copyFile(replayFile, tmp); - ReplayFileIO.addFilesToZip(tmp, toAdd); + ReplayMetaData newMetaData = new ReplayMetaData(metaData); + newMetaData.setServerName(null); + ReplayFile replay = new ZipReplayFile(new ReplayStudio(), replayFile); + replay.writeMetaData(newMetaData); + replay.saveTo(tmp); uploader.uploadFile(GuiUploadFile.this, name, tags, tmp, category, desc); - FileUtils.deleteQuietly(tmpMeta); FileUtils.deleteQuietly(tmp); } else { uploader.uploadFile(GuiUploadFile.this, name, tags, replayFile, category, desc); diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java b/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java deleted file mode 100755 index 7d2360c1..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java +++ /dev/null @@ -1,657 +0,0 @@ -package eu.crushedpixel.replaymod.gui.overlay; - -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.entities.CameraEntity; -import eu.crushedpixel.replaymod.gui.GuiMouseInput; -import eu.crushedpixel.replaymod.gui.GuiRenderSettings; -import eu.crushedpixel.replaymod.gui.GuiReplaySpeedSlider; -import eu.crushedpixel.replaymod.gui.elements.*; -import eu.crushedpixel.replaymod.gui.elements.timelines.GuiKeyframeTimeline; -import eu.crushedpixel.replaymod.gui.elements.timelines.GuiMarkerTimeline; -import eu.crushedpixel.replaymod.holders.AdvancedPosition; -import eu.crushedpixel.replaymod.holders.Keyframe; -import eu.crushedpixel.replaymod.holders.SpectatorData; -import eu.crushedpixel.replaymod.holders.TimestampValue; -import eu.crushedpixel.replaymod.registry.KeybindRegistry; -import eu.crushedpixel.replaymod.registry.ReplayGuiRegistry; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.utils.CameraPathValidator; -import eu.crushedpixel.replaymod.utils.MouseUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.entity.EntityOtherPlayerMP; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.GuiChat; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.inventory.GuiInventory; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.resources.I18n; -import net.minecraft.client.settings.KeyBinding; -import net.minecraft.entity.Entity; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.client.event.RenderGameOverlayEvent; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.client.FMLClientHandler; -import net.minecraftforge.fml.common.FMLCommonHandler; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import org.lwjgl.input.Keyboard; -import org.lwjgl.opengl.Display; -import org.lwjgl.util.Point; - -import java.awt.Color; -import java.io.IOException; -import java.util.List; - -import static net.minecraft.client.renderer.GlStateManager.*; -import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; -import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; - -public class GuiReplayOverlay extends Gui { - private static final Minecraft mc = Minecraft.getMinecraft(); - public static final ResourceLocation replay_gui = new ResourceLocation("replaymod", "replay_gui.png"); - public static final int TEXTURE_SIZE = 128; - - public static final int KEYFRAME_TIMELINE_LENGTH = 30 * 60 * 1000; - - public static GuiTexturedButton texturedButton(int x, int y, int u, int v, int size, Runnable action, String hoverText) { - return new GuiTexturedButton(0, x, y, size, size, replay_gui, u, v, TEXTURE_SIZE, TEXTURE_SIZE, action, I18n.format(hoverText)); - } - - private final Point screenDimensions = MouseUtils.getScaledDimensions(); - private final int WIDTH = screenDimensions.getX(); - private final int HEIGHT = screenDimensions.getY(); - - // Top row - private final int TOP_ROW = 10; - private final int BUTTON_PLAY_PAUSE_X = 10; - private final int SPEED_X = 35; - private final int SPEED_WIDTH = 100; - private final int TIMELINE_X = SPEED_X + SPEED_WIDTH + 5; - - // Bottom row - private final int BOTTOM_ROW = TOP_ROW + 33; - private final int BUTTON_PLAY_PATH_X = 10; - private final int BUTTON_EXPORT_X = BUTTON_PLAY_PATH_X + 25; - private final int BUTTON_PLACE_X = BUTTON_EXPORT_X + 25; - private final int BUTTON_TIME_X = BUTTON_PLACE_X + 25; - private final int TIMELINE_REAL_X = BUTTON_TIME_X + 25; - private final int TIMELINE_REAL_WIDTH = WIDTH - 14 - 11 - TIMELINE_REAL_X; - - private final GuiElement buttonPlayPause = new DelegatingElement() { - - private final GuiElement buttonPlay = texturedButton(BUTTON_PLAY_PAUSE_X, TOP_ROW, 0, 0, 20, new Runnable() { - @Override - public void run() { - ReplayMod.replaySender.setReplaySpeed(speedSlider.getSliderValue()); - } - }, "replaymod.gui.ingame.menu.unpause"); - - private final GuiElement buttonPause = texturedButton(BUTTON_PLAY_PAUSE_X, TOP_ROW, 0, 20, 20, new Runnable() { - @Override - public void run() { - ReplayMod.replaySender.setReplaySpeed(0); - } - }, "replaymod.gui.ingame.menu.pause"); - - @Override - public GuiElement delegate() { - return ReplayMod.replaySender.paused() ? buttonPlay : buttonPause; - } - }; - - private final GuiElement buttonExport = texturedButton(BUTTON_EXPORT_X, BOTTOM_ROW, 40, 0, 20, new Runnable() { - @Override - public void run() { - try { - CameraPathValidator.validateCameraPath(ReplayHandler.getPositionKeyframes(), ReplayHandler.getTimeKeyframes()); - } catch(CameraPathValidator.InvalidCameraPathException e) { - e.printToChat(); - return; - } - mc.displayGuiScreen(new GuiRenderSettings()); - - } - }, "replaymod.gui.ingame.menu.renderpath"); - - private final GuiElement buttonPlayPausePath = new DelegatingElement() { - - private final GuiElement buttonPlay = texturedButton(BUTTON_PLAY_PATH_X, BOTTOM_ROW, 0, 0, 20, new Runnable() { - @Override - public void run() { - ReplayHandler.startPath(null, GuiScreen.isCtrlKeyDown()); - - } - }, "replaymod.gui.ingame.menu.playpath"); - - private final GuiElement buttonPlayFromStart = texturedButton(BUTTON_PLAY_PATH_X, BOTTOM_ROW, 0, 0, 20, new Runnable() { - @Override - public void run() { - ReplayHandler.startPath(null, GuiScreen.isCtrlKeyDown()); - - } - }, "replaymod.gui.ingame.menu.playpathfromstart"); - - private final GuiElement buttonPause = texturedButton(BUTTON_PLAY_PATH_X, BOTTOM_ROW, 0, 20, 20, new Runnable() { - @Override - public void run() { - ReplayHandler.interruptReplay(); - - } - }, "replaymod.gui.ingame.menu.pausepath"); - - @Override - public GuiElement delegate() { - return ReplayHandler.isInPath() ? buttonPause : GuiScreen.isCtrlKeyDown() ? buttonPlayFromStart : buttonPlay; - } - }; - - private final GuiElement buttonPlace = new DelegatingElement() { - private final GuiElement buttonNotSelected = texturedButton(BUTTON_PLACE_X, BOTTOM_ROW, 0, 40, 20, new Runnable() { - @Override - public void run() { - Entity cam = mc.getRenderViewEntity(); - if (cam != null) { - AdvancedPosition position = new AdvancedPosition(cam.posX, cam.posY, cam.posZ, cam.rotationPitch, - cam.rotationYaw % 360, ReplayHandler.getCameraTilt()); - - if (ReplayHandler.isCamera()) - ReplayHandler.addKeyframe(new Keyframe(ReplayHandler.getRealTimelineCursor(), position)); - else - ReplayHandler.addKeyframe(new Keyframe(ReplayHandler.getRealTimelineCursor(), new SpectatorData(cam))); - } - } - }, "replaymod.gui.ingame.menu.addposkeyframe"); - - private final GuiElement buttonSelected = texturedButton(BUTTON_PLACE_X, BOTTOM_ROW, 0, 60, 20, new Runnable() { - @Override - public void run() { - ReplayHandler.removeKeyframe(ReplayHandler.getSelectedKeyframe()); - } - }, "replaymod.gui.ingame.menu.removeposkeyframe"); - - private final GuiElement buttonSpectatorNotSelected = texturedButton(BUTTON_PLACE_X, BOTTOM_ROW, 40, 40, 20, new Runnable() { - @Override - public void run() { - Entity cam = mc.getRenderViewEntity(); - if (cam != null) { - AdvancedPosition position = new AdvancedPosition(cam.posX, cam.posY, cam.posZ, cam.rotationPitch, - cam.rotationYaw % 360, ReplayHandler.getCameraTilt()); - - if (ReplayHandler.isCamera()) - ReplayHandler.addKeyframe(new Keyframe(ReplayHandler.getRealTimelineCursor(), position)); - else - ReplayHandler.addKeyframe(new Keyframe(ReplayHandler.getRealTimelineCursor(), new SpectatorData(cam))); - } - } - }, "replaymod.gui.ingame.menu.addspeckeyframe"); - - private final GuiElement buttonSpectatorSelected = texturedButton(BUTTON_PLACE_X, BOTTOM_ROW, 40, 60, 20, new Runnable() { - @Override - public void run() { - ReplayHandler.removeKeyframe(ReplayHandler.getSelectedKeyframe()); - } - }, "replaymod.gui.ingame.menu.removespeckeyframe"); - - @Override - public GuiElement delegate() { - boolean selected = ReplayHandler.getSelectedKeyframe() != null && ReplayHandler.getSelectedKeyframe().getValue() instanceof AdvancedPosition; - boolean camera; - if(selected) { - camera = !(ReplayHandler.getSelectedKeyframe().getValue() instanceof SpectatorData); - } else { - camera = ReplayHandler.isCamera(); - } - - if(camera) { - return selected ? buttonSelected : buttonNotSelected; - } else { - return selected ? buttonSpectatorSelected : buttonSpectatorNotSelected; - } - } - }; - - private final GuiElement buttonTime = new DelegatingElement() { - - private final GuiElement buttonNotSelected = texturedButton(BUTTON_TIME_X, BOTTOM_ROW, 0, 80, 20, new Runnable() { - @Override - public void run() { - ReplayHandler.addKeyframe(new Keyframe(ReplayHandler.getRealTimelineCursor(), new TimestampValue(ReplayMod.replaySender.currentTimeStamp()))); - } - }, "replaymod.gui.ingame.menu.addtimekeyframe"); - - private final GuiElement buttonSelected = texturedButton(BUTTON_TIME_X, BOTTOM_ROW, 0, 100, 20, new Runnable() { - @Override - public void run() { - ReplayHandler.removeKeyframe(ReplayHandler.getSelectedKeyframe()); - } - }, "replaymod.gui.ingame.menu.removetimekeyframe"); - - @Override - public GuiElement delegate() { - return ReplayHandler.getSelectedKeyframe() != null && ReplayHandler.getSelectedKeyframe().getValue() instanceof TimestampValue ? buttonSelected : buttonNotSelected; - } - }; - private final GuiElement buttonZoomIn = texturedButton(WIDTH - 14 - 9, BOTTOM_ROW, 40, 20, 9, new Runnable() { - @Override - public void run() { - timelineReal.zoomIn(); - } - }, "replaymod.gui.ingame.menu.zoomin"); - - private final GuiElement buttonZoomOut = texturedButton(WIDTH - 14 - 9, BOTTOM_ROW + 11, 40, 30, 9, new Runnable() { - - @Override - public void run() { - timelineReal.zoomOut(); - } - }, "replaymod.gui.ingame.menu.zoomout"); - - - private final GuiMarkerTimeline timeline = new GuiMarkerTimeline(TIMELINE_X, TOP_ROW - 1, WIDTH - 14 - TIMELINE_X, 22, false); - - private final GuiKeyframeTimeline timelineReal = new GuiKeyframeTimeline(TIMELINE_REAL_X, BOTTOM_ROW - 1, TIMELINE_REAL_WIDTH, 22, true, true, true); - { - timelineReal.timelineLength = KEYFRAME_TIMELINE_LENGTH; - } - - private final GuiScrollbar scrollbar = new GuiScrollbar(TIMELINE_REAL_X, BOTTOM_ROW + 22, TIMELINE_REAL_WIDTH) { - @Override - public void dragged() { - timelineReal.timeStart = scrollbar.sliderPosition; - } - }; - - private final GuiReplaySpeedSlider speedSlider = new GuiReplaySpeedSlider(SPEED_X, TOP_ROW, I18n.format("replaymod.gui.speed")); - - public double getSpeedSliderValue() { - return speedSlider.getSliderValue(); - } - - private boolean toolbarOpen = false; - - private final DelegatingElement toolbar = new DelegatingElement() { - - private void toggleOpen() { - toolbarOpen = !toolbarOpen; - } - - private GuiElement buttonOpenToolbar = new GuiArrowButton(-1, 10, HEIGHT-10-20, "", GuiArrowButton.Direction.UP) { - @Override - public void performAction() { - toggleOpen(); - } - }; - - private GuiElement buttonCloseToolbar = new GuiArrowButton(-1, 10, HEIGHT-10-20, "", GuiArrowButton.Direction.DOWN) { - @Override - public void performAction() { - toggleOpen(); - } - }; - - private ComposedElement openElements = new ComposedElement(buttonCloseToolbar); - - private int maxButtonY = -1; - - { - int buttonX = 10; - int maxWidth = 0; - int i = 0; - - for(final KeyBinding kb : KeybindRegistry.getReplayModKeyBindings()) { - int buttonY = HEIGHT-55-(i*25); - - if(buttonY < 80) { - buttonX += 25 + maxWidth + 5; - maxWidth = 0; - - i = 0; - buttonY = HEIGHT-55-(i*25); - } - - if(buttonY < maxButtonY || maxButtonY == -1) maxButtonY = buttonY; - - String keyName = "???"; - try { - keyName = Keyboard.getKeyName(kb.getKeyCode()); - } catch (ArrayIndexOutOfBoundsException e) { - // Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55 - } - GuiElement button = new GuiAdvancedButton(buttonX, buttonY, 20, 20, keyName, new Runnable() { - @Override - public void run() { - ReplayMod.keyInputHandler.handleCustomKeybindings(kb, false, kb.getKeyCode()); - } - }, null); - - String stringText = I18n.format(kb.getKeyDescription()); - int stringWidth = mc.fontRendererObj.getStringWidth(stringText); - - if(stringWidth > maxWidth) { - maxWidth = stringWidth; - } - - GuiString string = new GuiString(buttonX+25, buttonY+6, Color.WHITE, stringText); - - openElements.addPart(button); - openElements.addPart(string); - - i++; - } - } - - @Override - public GuiElement delegate() { - if(toolbarOpen) { - return openElements; - } else { - return buttonOpenToolbar; - } - } - - @Override - public void draw(Minecraft mc, int mouseX, int mouseY) { - if(toolbarOpen) { - drawGradientRect(0, maxButtonY - 10, WIDTH, HEIGHT, -1072689136, -804253680); - } - super.draw(mc, mouseX, mouseY); - } - }; - - public void closeToolbar() { - toolbarOpen = false; - } - - private final GuiElement content = new ComposedElement(buttonPlayPause, buttonExport, buttonPlace, buttonTime, - buttonPlayPausePath, buttonZoomIn, buttonZoomOut, timeline, timelineReal, scrollbar, speedSlider, toolbar); - - public boolean isVisible() { - return ReplayHandler.isInReplay(); - } - - /** - * Resets the UI. - * @param resetElements Whether the timeline and Speed Slider should be reset as well - */ - public void resetUI(boolean resetElements) { - if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { - mc.displayGuiScreen(null); - } - if (resetElements) { - timelineReal.zoom = 0.033f; - timelineReal.timeStart = 0; - - ReplayHandler.setRealTimelineCursor(0); - speedSlider.reset(); - } - } - - public void register() { - FMLCommonHandler.instance().bus().register(this); - MinecraftForge.EVENT_BUS.register(this); - } - - public void unregister() { - FMLCommonHandler.instance().bus().unregister(this); - MinecraftForge.EVENT_BUS.unregister(this); - } - - private void checkResize() { - if (!screenDimensions.equals(MouseUtils.getScaledDimensions())) { - GuiReplayOverlay other = new GuiReplayOverlay(); - other.timelineReal.zoom = this.timelineReal.zoom; - other.timelineReal.timeStart = this.timelineReal.timeStart; - other.speedSlider.copyValueFrom(this.speedSlider); - - this.unregister(); - other.register(); - ReplayMod.overlay = other; - - if (mc.currentScreen instanceof GuiMouseInput) { - mc.displayGuiScreen(new GuiMouseInput(other)); - } - } - } - - @SubscribeEvent - public void onRenderTabList(RenderGameOverlayEvent.Pre event) { //cancelling tab list rendering and rendering help instead - if (!isVisible()) return; - if (event.type == RenderGameOverlayEvent.ElementType.PLAYER_LIST) { - event.setCanceled(true); - } - } - - private void tick() { - if (FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { - Entity player = ReplayHandler.getCameraEntity(); - if(player != null) { - player.setVelocity(0, 0, 0); - } - } - - checkResize(); - } - - public void mouseDrag(int mouseX, int mouseY, int button) { - content.mouseDrag(mc, mouseX, mouseY, button); - } - - public void mouseReleased(int mouseX, int mouseY, int button) { - content.mouseRelease(mc, mouseX, mouseY, button); - } - - public void mouseClicked(int mouseX, int mouseY, int button) { - if (ReplayHandler.isInPath()) { // Only allow clicking of cancel button during path replay - buttonPlayPausePath.mouseClick(mc, mouseX, mouseY, button); - } else { - content.mouseClick(mc, mouseX, mouseY, button); - } - } - - public void performJump(long timelineTime) { - performJump(timelineTime, true); - } - - public void performJump(long timelineTime, boolean setLastPosition) { - if (timelineTime != -1) { // Click on timeline - //When hurrying, no Timeline jumping etc. is possible - if(!ReplayMod.replaySender.isHurrying()) { - if(timelineTime < ReplayMod.replaySender.currentTimeStamp()) { - mc.displayGuiScreen(null); - } - - if(setLastPosition) { - CameraEntity cam = ReplayHandler.getCameraEntity(); - if(cam != null) { - ReplayHandler.setLastPosition(new AdvancedPosition(cam.posX, cam.posY, cam.posZ, cam.rotationPitch, cam.rotationYaw), false); - } else { - ReplayHandler.setLastPosition(null, false); - } - } - - long diff = timelineTime - ReplayMod.replaySender.getDesiredTimestamp(); - if(diff != 0) { - if (diff > 0 && diff < 5000) { // Small difference and no time travel - ReplayMod.replaySender.jumpToTime((int) timelineTime); - } else { // We either have to restart the replay or send a significant amount of packets - // Render our please-wait-screen - GuiScreen guiScreen = new GuiScreen() { - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { - drawBackground(0); - drawCenteredString(fontRendererObj, I18n.format("replaymod.gui.pleasewait"), - width / 2, height / 2, 0xffffffff); - } - }; - - // Make sure that the replaysender changes into sync mode - ReplayMod.replaySender.setSyncModeAndWait(); - - // Perform the rendering using OpenGL - pushMatrix(); - clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - enableTexture2D(); - mc.getFramebuffer().bindFramebuffer(true); - mc.entityRenderer.setupOverlayRendering(); - - guiScreen.setWorldAndResolution(mc, WIDTH, HEIGHT); - guiScreen.drawScreen(0, 0, 0); - - mc.getFramebuffer().unbindFramebuffer(); - popMatrix(); - pushMatrix(); - mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); - popMatrix(); - - Display.update(); - - // Send the packets - ReplayMod.replaySender.sendPacketsTill((int) timelineTime); - ReplayMod.replaySender.setAsyncMode(true); - ReplayMod.replaySender.setReplaySpeed(0); - - mc.getNetHandler().getNetworkManager().processReceivedPackets(); - @SuppressWarnings("unchecked") - List entities = (List) mc.theWorld.loadedEntityList; - for (Entity entity : entities) { - if (entity instanceof EntityOtherPlayerMP) { - EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity; - e.setPosition(e.otherPlayerMPX, e.otherPlayerMPY, e.otherPlayerMPZ); - e.rotationYaw = (float) e.otherPlayerMPYaw; - e.rotationPitch = (float) e.otherPlayerMPPitch; - } - entity.lastTickPosX = entity.prevPosX = entity.posX; - entity.lastTickPosY = entity.prevPosY = entity.posY; - entity.lastTickPosZ = entity.prevPosZ = entity.posZ; - entity.prevRotationYaw = entity.rotationYaw; - entity.prevRotationPitch = entity.rotationPitch; - } - try { - mc.runTick(); - } catch (IOException e) { - e.printStackTrace(); // This should never be thrown but whatever - } - - //finally, updating the camera's position (which is not done by the sync jumping) - ReplayHandler.moveCameraToLastPosition(); - - // No need to remove our please-wait-screen. It'll vanish with the next - // render pass as it's never been a real GuiScreen in the first place. - } - } - } - } - } - - @SubscribeEvent - public void onRenderGui(RenderGameOverlayEvent.Post event) throws IllegalArgumentException, IllegalAccessException { - if (event.type != RenderGameOverlayEvent.ElementType.ALL) return; - if (!isVisible()) return; - - FMLClientHandler fml = FMLClientHandler.instance(); - - tick(); - - // Do not draw if GUI doesn't want us to - if(mc.currentScreen instanceof NoOverlay) { - return; - } - - // If we are not currently spectating someone - if (ReplayHandler.isCamera()) { - ReplayGuiRegistry.hide(); // hide all normal UI - } else { - ReplayGuiRegistry.show(); // otherwise show them - } - - // Replace chat and inventory GUI (opened by pressing the respective hotkey) with a dummy input GUI - if(fml.isGUIOpen(GuiChat.class) || fml.isGUIOpen(GuiInventory.class)) { - mc.displayGuiScreen(new GuiMouseInput(this)); - } - - GlStateManager.resetColor(); - - Point mousePoint = MouseUtils.getMousePos(); - int mouseX = mousePoint.getX(); - int mouseY = mousePoint.getY(); - - if (!(mc.currentScreen instanceof GuiMouseInput)) { - // We only react to the mouse if our gui screen is opened. - // Otherwise we just move the mouse away so it doesn't activate any buttons - mouseX = mouseY = -1000; - } - - // Setup scrollbar and timelines - if (timelineReal.timeStart + timelineReal.zoom > 1) { - timelineReal.timeStart = 1 - timelineReal.zoom; - } - scrollbar.size = timelineReal.zoom; - scrollbar.sliderPosition = timelineReal.timeStart; - - timeline.cursorPosition = ReplayMod.replaySender.currentTimeStamp(); - timeline.timelineLength = ReplayMod.replaySender.replayLength(); - - timelineReal.cursorPosition = ReplayHandler.getRealTimelineCursor(); - - // Draw all elements - content.draw(mc, mouseX, mouseY); - content.drawOverlay(mc, mouseX, mouseY); - - GlStateManager.enableBlend(); - } - - public void togglePlayPause() { - if (ReplayMod.replaySender.paused()) { - ReplayMod.replaySender.setReplaySpeed(speedSlider.getSliderValue()); - } else { - ReplayMod.replaySender.setReplaySpeed(0); - } - } - - /** - * Render the Ambient Lighting and Path Preview indicators in the lower right corner. - * @param event Rendered post game overlay - */ - @SubscribeEvent - public void renderIndicators(RenderGameOverlayEvent.Post event) { - if (event.type != RenderGameOverlayEvent.ElementType.ALL) return; - if(!ReplayHandler.isInReplay() || mc.currentScreen instanceof NoOverlay) return; - - int xPos = WIDTH-10; - - if(ReplayMod.replaySettings.isLightingEnabled()) { - int width = 19; - - mc.renderEngine.bindTexture(replay_gui); - GlStateManager.color(1, 1, 1, 1); - GlStateManager.enableAlpha(); - GlStateManager.disableLighting(); - Gui.drawModalRectWithCustomSizedTexture(xPos-width, HEIGHT - 10 - 13, - 90, 20, 19, 13, TEXTURE_SIZE, TEXTURE_SIZE); - - xPos -= width + 5; - } - - if(ReplayMod.replaySettings.showPathPreview()) { - int width = 20; - - mc.renderEngine.bindTexture(replay_gui); - GlStateManager.color(1, 1, 1, 1); - GlStateManager.enableAlpha(); - GlStateManager.disableLighting(); - Gui.drawModalRectWithCustomSizedTexture(xPos - width, HEIGHT - 10 - 13, - 100, 0, 20, 13, TEXTURE_SIZE, TEXTURE_SIZE); - - //noinspection UnusedAssignment - xPos -= width + 5; - } - - //can be continued - } - - /** - * Dummy interface for GUI on which this replay overlay shall not be rendered. - */ - public interface NoOverlay { - - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/replayeditor/GuiReplayEditingProcess.java b/src/main/java/eu/crushedpixel/replaymod/gui/replayeditor/GuiReplayEditingProcess.java index 17e10222..27622be9 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/replayeditor/GuiReplayEditingProcess.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/replayeditor/GuiReplayEditingProcess.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui.replayeditor; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.GuiConstants; import eu.crushedpixel.replaymod.gui.elements.GuiProgressBar; import net.minecraft.client.gui.GuiButton; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/replayeditor/GuiReplayEditor.java b/src/main/java/eu/crushedpixel/replaymod/gui/replayeditor/GuiReplayEditor.java index 48182a15..ab7d93ad 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/replayeditor/GuiReplayEditor.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/replayeditor/GuiReplayEditor.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui.replayeditor; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.GuiConstants; import eu.crushedpixel.replaymod.gui.elements.GuiDropdown; import eu.crushedpixel.replaymod.holders.GuiEntryListStringEntry; diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/Marker.java b/src/main/java/eu/crushedpixel/replaymod/holders/Marker.java deleted file mode 100644 index 32071cb5..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/holders/Marker.java +++ /dev/null @@ -1,33 +0,0 @@ -package eu.crushedpixel.replaymod.holders; - -import eu.crushedpixel.replaymod.interpolation.GenericLinearInterpolation; -import eu.crushedpixel.replaymod.interpolation.GenericSplineInterpolation; -import eu.crushedpixel.replaymod.interpolation.Interpolation; -import eu.crushedpixel.replaymod.interpolation.KeyframeValue; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class Marker implements KeyframeValue { - - private String name; - private AdvancedPosition position; - - @Override - public KeyframeValue newInstance() { - return new Marker(); - } - - @Override - public Interpolation getLinearInterpolator() { - return new GenericLinearInterpolation(); - } - - @Override - public Interpolation getCubicInterpolator() { - return new GenericSplineInterpolation(); - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/PlayerVisibility.java b/src/main/java/eu/crushedpixel/replaymod/holders/PlayerVisibility.java deleted file mode 100644 index fb26f223..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/holders/PlayerVisibility.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.crushedpixel.replaymod.holders; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.util.UUID; - -@Data -@AllArgsConstructor -public class PlayerVisibility { - private UUID[] hidden; -} diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java index c27bdfa6..a1d03c49 100644 --- a/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java @@ -3,8 +3,6 @@ package eu.crushedpixel.replaymod.interpolation; import eu.crushedpixel.replaymod.holders.AdvancedPosition; import eu.crushedpixel.replaymod.holders.Keyframe; import eu.crushedpixel.replaymod.holders.SpectatorData; -import eu.crushedpixel.replaymod.holders.TimestampValue; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import lombok.NoArgsConstructor; import java.util.ListIterator; @@ -56,17 +54,18 @@ public class AdvancedPositionKeyframeList extends KeyframeList spectatorInterpolation = new SpectatorDataInterpolation(linear); } int keyframeTimestamp = keyframe.getRealTimestamp(); - TimestampValue timestampValue = ReplayHandler.getTimeKeyframes().getInterpolatedValueForTimestamp(keyframeTimestamp, true); - int replayTimestamp = timestampValue == null ? 0 : timestampValue.asInt(); - if(firstTimestamp == -1) firstTimestamp = replayTimestamp; - spectatorInterpolation.addPoint(keyframe, replayTimestamp); - if(iterator.hasNext()) { - keyframe = iterator.next(); - found = keyframe.getValue() instanceof SpectatorData; - if(!found) completedKeyframes.add(keyframe); - } else { - found = false; - } + // TODO +// TimestampValue timestampValue = ReplayHandler.getTimeKeyframes().getInterpolatedValueForTimestamp(keyframeTimestamp, true); +// int replayTimestamp = timestampValue == null ? 0 : timestampValue.asInt(); +// if(firstTimestamp == -1) firstTimestamp = replayTimestamp; +// spectatorInterpolation.addPoint(keyframe, replayTimestamp); +// if(iterator.hasNext()) { +// keyframe = iterator.next(); +// found = keyframe.getValue() instanceof SpectatorData; +// if(!found) completedKeyframes.add(keyframe); +// } else { +// found = false; +// } } if(spectatorInterpolation != null && spectatorInterpolation.size() > 1) { diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java index 86f61be7..bf85f566 100644 --- a/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java @@ -4,7 +4,6 @@ import eu.crushedpixel.replaymod.holders.AdvancedPosition; import eu.crushedpixel.replaymod.holders.Keyframe; import eu.crushedpixel.replaymod.holders.SpectatorData; import eu.crushedpixel.replaymod.holders.SpectatorDataThirdPersonInfo; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import org.apache.commons.lang3.tuple.Pair; import java.util.ArrayList; @@ -58,20 +57,21 @@ public class SpectatorDataInterpolation { //updating the spectator keyframe's position in the world to smoothly continue the path //with non-spectator position keyframes for(Pair> pair : points) { - AdvancedPosition entityPosition = ReplayHandler.getEntityPositionTracker().getEntityPositionAtTimestamp(entityID, pair.getKey()); - if(entityPosition == null) continue; - - //transform the entity position (sry for no dry code :x ) - SpectatorDataThirdPersonInfo thirdPersonInfo = ((SpectatorData)pair.getValue().getValue()).getThirdPersonInfo(); - - //first, rotate the camera pitch and yaw according to the settings - entityPosition.setYaw(entityPosition.getYaw() + thirdPersonInfo.shoulderCamYawOffset); - entityPosition.setPitch(entityPosition.getPitch() + thirdPersonInfo.shoulderCamPitchOffset); - - //next, move the camera point to fulfill the specified distance to the entity - entityPosition = entityPosition.getDestination(-1 * thirdPersonInfo.shoulderCamDistance); - - pair.getValue().getValue().apply(entityPosition); + // TODO +// AdvancedPosition entityPosition = ReplayHandler.getEntityPositionTracker().getEntityPositionAtTimestamp(entityID, pair.getKey()); +// if(entityPosition == null) continue; +// +// //transform the entity position (sry for no dry code :x ) +// SpectatorDataThirdPersonInfo thirdPersonInfo = ((SpectatorData)pair.getValue().getValue()).getThirdPersonInfo(); +// +// //first, rotate the camera pitch and yaw according to the settings +// entityPosition.setYaw(entityPosition.getYaw() + thirdPersonInfo.shoulderCamYawOffset); +// entityPosition.setPitch(entityPosition.getPitch() + thirdPersonInfo.shoulderCamPitchOffset); +// +// //next, move the camera point to fulfill the specified distance to the entity +// entityPosition = entityPosition.getDestination(-1 * thirdPersonInfo.shoulderCamDistance); +// +// pair.getValue().getValue().apply(entityPosition); } //feed the underlying keyframe list with AdvancedPosition Keyframes that are derived from the Spectator Keyframes @@ -116,17 +116,18 @@ public class SpectatorDataInterpolation { smoothness = (int)(thirdPersonInfo.shoulderCamSmoothness*1000); //calculate the Position relative to the Entity - AdvancedPosition entityPosition = ReplayHandler.getEntityPositionTracker().getEntityPositionAtTimestamp(entityID, currentTimestamp); - if(entityPosition == null) continue; - - //first, rotate the camera pitch and yaw according to the settings - entityPosition.setYaw(entityPosition.getYaw() + thirdPersonInfo.shoulderCamYawOffset); - entityPosition.setPitch(entityPosition.getPitch() + thirdPersonInfo.shoulderCamPitchOffset); - - //next, move the camera point to fulfill the specified distance to the entity - entityPosition = entityPosition.getDestination(-1 * thirdPersonInfo.shoulderCamDistance); - - underlyingKeyframes.add(new Keyframe(interpolatedRealTimestamp, entityPosition)); + // TODO +// AdvancedPosition entityPosition = ReplayHandler.getEntityPositionTracker().getEntityPositionAtTimestamp(entityID, currentTimestamp); +// if(entityPosition == null) continue; +// +// //first, rotate the camera pitch and yaw according to the settings +// entityPosition.setYaw(entityPosition.getYaw() + thirdPersonInfo.shoulderCamYawOffset); +// entityPosition.setPitch(entityPosition.getPitch() + thirdPersonInfo.shoulderCamPitchOffset); +// +// //next, move the camera point to fulfill the specified distance to the entity +// entityPosition = entityPosition.getDestination(-1 * thirdPersonInfo.shoulderCamDistance); +// +// underlyingKeyframes.add(new Keyframe(interpolatedRealTimestamp, entityPosition)); } i++; diff --git a/src/main/java/eu/crushedpixel/replaymod/localization/LocalizedResourcePack.java b/src/main/java/eu/crushedpixel/replaymod/localization/LocalizedResourcePack.java index 22075e2e..a70d54f6 100644 --- a/src/main/java/eu/crushedpixel/replaymod/localization/LocalizedResourcePack.java +++ b/src/main/java/eu/crushedpixel/replaymod/localization/LocalizedResourcePack.java @@ -2,7 +2,7 @@ package eu.crushedpixel.replaymod.localization; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableSet; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.ApiException; import net.minecraft.client.resources.IResourcePack; import net.minecraft.client.resources.data.IMetadataSection; diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinEntityRenderer.java b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinEntityRenderer.java index b0c83dc4..74d308b0 100644 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinEntityRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinEntityRenderer.java @@ -1,7 +1,7 @@ package eu.crushedpixel.replaymod.mixin; +import com.replaymod.replay.CameraEntity; import eu.crushedpixel.replaymod.renderer.SpectatorRenderer; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.video.EntityRendererHandler; import eu.crushedpixel.replaymod.video.capturer.CubicOpenGlFrameCapturer; @@ -18,6 +18,7 @@ import net.minecraft.util.MovingObjectPosition; import org.lwjgl.opengl.GL11; import org.lwjgl.util.glu.Project; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; @@ -25,6 +26,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(EntityRenderer.class) public abstract class MixinEntityRenderer implements EntityRendererHandler.IEntityRenderer, EntityRendererHandler.GluPerspective { + @Shadow + public Minecraft mc; + private EntityRendererHandler handler; private SpectatorRenderer spectatorRenderer = new SpectatorRenderer(); @@ -65,8 +69,8 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti if (options.getIgnoreCameraRotation()[1]) { entity.prevRotationPitch = entity.rotationPitch = 0; } - if (options.getIgnoreCameraRotation()[2]) { - ReplayHandler.setCameraTilt(0); + if (options.getIgnoreCameraRotation()[2] && entity instanceof CameraEntity) { + ((CameraEntity) entity).roll = 0; } } } @@ -85,7 +89,7 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti orgPitch = entity.rotationPitch; orgPrevYaw = entity.prevRotationYaw; orgPrevPitch = entity.prevRotationPitch; - orgRoll = ReplayHandler.getCameraTilt(); + orgRoll = entity instanceof CameraEntity ? ((CameraEntity) entity).roll : 0; } } @@ -97,28 +101,31 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti entity.rotationPitch = orgPitch; entity.prevRotationYaw = orgPrevYaw; entity.prevRotationPitch = orgPrevPitch; - ReplayHandler.setCameraTilt(orgRoll); + if (entity instanceof CameraEntity) { + ((CameraEntity) entity).roll = orgRoll; + } } } @Inject(method = "renderHand", at = @At("HEAD"), cancellable = true) private void renderSpectatorHand(float partialTicks, int renderPass, CallbackInfo ci) { - if (handler != null) { - if (handler.data instanceof CubicOpenGlFrameCapturer.Data) { - ci.cancel(); - return; // No spectator hands during 360° view, we wouldn't even know where to put it - } - Entity currentEntity = Minecraft.getMinecraft().getRenderViewEntity(); - if (!ReplayHandler.isCamera() && currentEntity instanceof EntityPlayer) { - renderPass = handler.data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 1 : 0; - spectatorRenderer.renderSpectatorHand((EntityPlayer) currentEntity, partialTicks, renderPass); - } - } else if (ReplayHandler.isInReplay() && !ReplayHandler.isCamera()) { - Entity currentEntity = Minecraft.getMinecraft().getRenderViewEntity(); - if (!ReplayHandler.isCamera() && currentEntity instanceof EntityPlayer) { - spectatorRenderer.renderSpectatorHand((EntityPlayer) currentEntity, partialTicks, renderPass); - } - } + // TODO +// if (handler != null) { +// if (handler.data instanceof CubicOpenGlFrameCapturer.Data) { +// ci.cancel(); +// return; // No spectator hands during 360° view, we wouldn't even know where to put it +// } +// Entity currentEntity = Minecraft.getMinecraft().getRenderViewEntity(); +// if (!ReplayHandler.isCameraView() && currentEntity instanceof EntityPlayer) { +// renderPass = handler.data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 1 : 0; +// spectatorRenderer.renderSpectatorHand((EntityPlayer) currentEntity, partialTicks, renderPass); +// } +// } else if (ReplayHandler.isInReplay() && !ReplayHandler.isCameraView()) { +// Entity currentEntity = Minecraft.getMinecraft().getRenderViewEntity(); +// if (!ReplayHandler.isCameraView() && currentEntity instanceof EntityPlayer) { +// spectatorRenderer.renderSpectatorHand((EntityPlayer) currentEntity, partialTicks, renderPass); +// } +// } } @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/MovingObjectPosition;IF)V")) @@ -242,8 +249,8 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti @Inject(method = "orientCamera", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;translate(FFF)V", shift = At.Shift.AFTER, ordinal = 3)) private void setupCameraRoll(float partialTicks, CallbackInfo ci) { - if (ReplayHandler.isInReplay()) { - GL11.glRotated(ReplayHandler.getCameraTilt(), 0D, 0D, 1D); + if (mc.getRenderViewEntity() instanceof CameraEntity) { + GL11.glRotated(((CameraEntity) mc.getRenderViewEntity()).roll, 0D, 0D, 1D); } } } diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinGuiSpectator.java b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinGuiSpectator.java index 050bf313..8c5c75f6 100644 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinGuiSpectator.java +++ b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinGuiSpectator.java @@ -1,17 +1,22 @@ package eu.crushedpixel.replaymod.mixin; -import eu.crushedpixel.replaymod.replay.ReplayHandler; +import com.replaymod.replay.CameraEntity; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiSpectator; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(GuiSpectator.class) public abstract class MixinGuiSpectator { + @Shadow + private Minecraft field_175268_g; + @Inject(method = "func_175260_a", at = @At("HEAD"), cancellable = true) public void isInReplay(int i, CallbackInfo ci) { - if (ReplayHandler.isInReplay()) { + if (field_175268_g.thePlayer instanceof CameraEntity) { ci.cancel(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinMinecraft.java b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinMinecraft.java index df9f7665..8b0a71ff 100644 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinMinecraft.java +++ b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinMinecraft.java @@ -1,6 +1,5 @@ package eu.crushedpixel.replaymod.mixin; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.audio.SoundHandler; import net.minecraft.entity.player.EntityPlayer; @@ -12,6 +11,7 @@ import org.spongepowered.asm.mixin.injection.Redirect; public class MixinMinecraft { @Redirect(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/audio/SoundHandler;setListener(Lnet/minecraft/entity/player/EntityPlayer;F)V")) public void setSoundSystemListener(SoundHandler soundHandler, EntityPlayer listener, float renderPartialTicks) { - soundHandler.setListener(ReplayHandler.isInReplay() ? ReplayHandler.getCameraEntity() : listener, renderPartialTicks); + //TODO might no longer be necessary? +// soundHandler.setListener(ReplayHandler.isInReplay() ? ReplayHandler.getCameraEntity() : listener, renderPartialTicks); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinPlayerControllerMP.java b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinPlayerControllerMP.java index b6daa62d..87a54bf0 100644 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinPlayerControllerMP.java +++ b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinPlayerControllerMP.java @@ -1,7 +1,7 @@ package eu.crushedpixel.replaymod.mixin; -import eu.crushedpixel.replaymod.entities.CameraEntity; -import eu.crushedpixel.replaymod.replay.ReplayHandler; +import com.replaymod.replay.CameraEntity; +import com.replaymod.replay.ReplayModReplay; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.multiplayer.PlayerControllerMP; @@ -25,7 +25,7 @@ public abstract class MixinPlayerControllerMP { @Inject(method = "func_178892_a", at=@At("HEAD"), cancellable = true) public void createReplayCamera(World worldIn, StatFileWriter statFileWriter, CallbackInfoReturnable ci) { - if (ReplayHandler.isInReplay()) { + if (ReplayModReplay.instance.getReplayHandler() != null) { ci.setReturnValue(new CameraEntity(mc, worldIn, netClientHandler, statFileWriter)); ci.cancel(); } diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRenderArrow.java b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRenderArrow.java index f74fb51d..3482eeea 100644 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRenderArrow.java +++ b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRenderArrow.java @@ -1,11 +1,8 @@ package eu.crushedpixel.replaymod.mixin; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import net.minecraft.client.renderer.culling.ICamera; import net.minecraft.client.renderer.entity.Render; import net.minecraft.client.renderer.entity.RenderArrow; import net.minecraft.client.renderer.entity.RenderManager; -import net.minecraft.entity.Entity; import org.spongepowered.asm.mixin.Mixin; @Mixin(RenderArrow.class) @@ -14,8 +11,9 @@ public abstract class MixinRenderArrow extends Render { super(renderManager); } - @Override - public boolean shouldRender(Entity entity, ICamera camera, double camX, double camY, double camZ) { - return ReplayHandler.isInReplay() || super.shouldRender(entity, camera, camX, camY, camZ); - } + // TODO +// @Override +// public boolean shouldRender(Entity entity, ICamera camera, double camX, double camY, double camZ) { +// return ReplayHandler.isInReplay() || super.shouldRender(entity, camera, camX, camY, camZ); +// } } diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRenderGlobal.java b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRenderGlobal.java deleted file mode 100644 index d951898a..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRenderGlobal.java +++ /dev/null @@ -1,32 +0,0 @@ -package eu.crushedpixel.replaymod.mixin; - -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; -import eu.crushedpixel.replaymod.timer.EnchantmentTimer; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.network.play.server.S25PacketBlockBreakAnim; -import net.minecraft.util.BlockPos; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(RenderGlobal.class) -public abstract class MixinRenderGlobal { - @Inject(method = "sendBlockBreakProgress", at = @At("HEAD")) - public void saveBlockBreakProgressPacket(int breakerId, BlockPos pos, int progress, CallbackInfo info) { - if(ConnectionEventHandler.isRecording()) { - EntityPlayer thePlayer = Minecraft.getMinecraft().thePlayer; - if(thePlayer != null && breakerId == thePlayer.getEntityId()) { - ConnectionEventHandler.insertPacket(new S25PacketBlockBreakAnim(breakerId, pos, progress)); - } - } - } - - @Redirect(method = "renderWorldBorder", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) - private long getEnchantmentTime() { - return EnchantmentTimer.getEnchantmentTime(); - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRendererLivingEntity.java b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRendererLivingEntity.java index 2aabf70c..2dd14743 100644 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRendererLivingEntity.java +++ b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinRendererLivingEntity.java @@ -1,7 +1,5 @@ package eu.crushedpixel.replaymod.mixin; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.settings.ReplaySettings; import eu.crushedpixel.replaymod.video.EntityRendererHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.entity.RendererLivingEntity; @@ -22,17 +20,19 @@ public abstract class MixinRendererLivingEntity { ci.setReturnValue(false); //this calls the cancel method } - if(ReplayHandler.isInReplay() && entity.isInvisible() - && ReplaySettings.ReplayOptions.renderInvisible.getValue() == Boolean.FALSE) { - ci.setReturnValue(false); - } + // TODO +// if(ReplayHandler.isInReplay() && entity.isInvisible() +// && ReplaySettings.ReplayOptions.renderInvisible.getValue() == Boolean.FALSE) { +// ci.setReturnValue(false); +// } } @Redirect(method = "renderModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/EntityLivingBase;isInvisibleToPlayer(Lnet/minecraft/entity/player/EntityPlayer;)Z")) private boolean shouldInvisibleNotBeRendered(EntityLivingBase entity, EntityPlayer thePlayer) { - if(ReplaySettings.ReplayOptions.renderInvisible.getValue() == Boolean.TRUE|| !ReplayHandler.isInReplay()) { - return entity.isInvisibleToPlayer(thePlayer); - } + // TODO +// if(ReplaySettings.ReplayOptions.renderInvisible.getValue() == Boolean.TRUE|| !ReplayHandler.isInReplay()) { +// return entity.isInvisibleToPlayer(thePlayer); +// } return true; //the original method inverts the return value } } diff --git a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinViewFrustum.java b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinViewFrustum.java index f63a1b7b..2ffcd735 100644 --- a/src/main/java/eu/crushedpixel/replaymod/mixin/MixinViewFrustum.java +++ b/src/main/java/eu/crushedpixel/replaymod/mixin/MixinViewFrustum.java @@ -1,6 +1,5 @@ package eu.crushedpixel.replaymod.mixin; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import net.minecraft.client.renderer.RenderGlobal; import net.minecraft.client.renderer.ViewFrustum; import net.minecraft.client.renderer.chunk.IRenderChunkFactory; @@ -39,9 +38,10 @@ public abstract class MixinViewFrustum { */ @Inject(method = "updateChunkPositions", at = @At("HEAD"), cancellable = true) public void fixedUpdateChunkPositions(double viewEntityX, double viewEntityZ, CallbackInfo ci) { - if (!ReplayHandler.isInReplay()) { - return; - } + // TODO +// if (!ReplayHandler.isInReplay()) { +// return; +// } int i = MathHelper.floor_double(viewEntityX) - 8; int j = MathHelper.floor_double(viewEntityZ) - 8; diff --git a/src/main/java/eu/crushedpixel/replaymod/online/urischeme/LinuxUriScheme.java b/src/main/java/eu/crushedpixel/replaymod/online/urischeme/LinuxUriScheme.java index b9597534..340d9822 100644 --- a/src/main/java/eu/crushedpixel/replaymod/online/urischeme/LinuxUriScheme.java +++ b/src/main/java/eu/crushedpixel/replaymod/online/urischeme/LinuxUriScheme.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.online.urischeme; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.StringBuilderWriter; diff --git a/src/main/java/eu/crushedpixel/replaymod/online/urischeme/WindowsUriScheme.java b/src/main/java/eu/crushedpixel/replaymod/online/urischeme/WindowsUriScheme.java index 146d3de1..2d614a41 100644 --- a/src/main/java/eu/crushedpixel/replaymod/online/urischeme/WindowsUriScheme.java +++ b/src/main/java/eu/crushedpixel/replaymod/online/urischeme/WindowsUriScheme.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.online.urischeme; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.StringBuilderWriter; diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java deleted file mode 100755 index ea88b3cf..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java +++ /dev/null @@ -1,197 +0,0 @@ -package eu.crushedpixel.replaymod.recording; - -import com.google.common.hash.Hashing; -import com.google.common.io.Files; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.holders.Keyframe; -import eu.crushedpixel.replaymod.holders.Marker; -import eu.crushedpixel.replaymod.holders.PacketData; -import eu.crushedpixel.replaymod.utils.ReplayFile; -import eu.crushedpixel.replaymod.utils.ReplayFileIO; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import org.apache.commons.io.FileUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.*; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; - -public abstract class DataListener extends ChannelInboundHandlerAdapter { - - private static final Logger logger = LogManager.getLogger(); - - protected File file; - protected Long startTime = null; - protected String name; - protected String worldName; - protected boolean serverWasPaused; - protected long lastSentPacket = 0; - protected boolean alive = true; - protected DataWriter dataWriter; - protected Set players = new HashSet(); - protected Set> markers = new HashSet>(); - private boolean singleplayer; - - private int saveState = 0; //0: Idle, 1: Saving, 2: Saved - - private final File tempResourcePacksFolder = Files.createTempDir(); - private final Map requestToHash = new ConcurrentHashMap(); - private final Map resourcePacks = new HashMap(); - - public DataListener(File file, String name, String worldName, long startTime, boolean singleplayer) throws FileNotFoundException { - this.file = file; - this.startTime = startTime; - this.name = name; - this.worldName = worldName; - this.singleplayer = singleplayer; - - FileOutputStream fos = new FileOutputStream(file); - BufferedOutputStream bos = new BufferedOutputStream(fos); - DataOutputStream out = new DataOutputStream(bos); - dataWriter = new DataWriter(out); - - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - @Override - public void run() { - try { - if(saveState == 0) { - System.out.println("Saving Replay File to prevent Corruption"); - DataListener.this.channelInactive(null); - } - } catch(Exception e) { - e.printStackTrace(); - } - } - }, "shutdown-hook-data-listener")); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) { - dataWriter.requestFinish(players, markers); - } - - protected void recordResourcePack(File file, int requestId) { - try { - String hash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); - requestToHash.put(requestId, hash); - synchronized (resourcePacks) { - if (!resourcePacks.containsKey(hash)) { - File tempFile = new File(tempResourcePacksFolder, hash); - FileUtils.copyFile(file, tempFile); - resourcePacks.put(hash, tempFile); - } - } - } catch (IOException e) { - logger.warn("Failed to save resource pack.", e); - } - } - - public class DataWriter { - - private boolean active = true; - private long paused; - - private ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); - private DataOutputStream stream; - - Thread outputThread = new Thread(new Runnable() { - - @Override - public void run() { - - while(active) { - PacketData dataReciever = queue.poll(); - if(dataReciever != null) { - //write the ByteBuf to the given OutputStream - - byte[] array = dataReciever.getByteArray(); - - if(array != null) { - try { - ReplayFileIO.writePacket(dataReciever, stream); - stream.flush(); - } catch(Exception e) { - e.printStackTrace(); - } - } - - } else { - try { - //let the Thread sleep for 1/4 second and queue up new Packets - Thread.sleep(250L); - } catch(InterruptedException e) { - e.printStackTrace(); - } - } - } - - try { - stream.flush(); - stream.close(); - } catch(Exception e) { - e.printStackTrace(); - } - - } - }, "replaymod-packet-writer"); - - public DataWriter(DataOutputStream stream) { - this.stream = stream; - outputThread.start(); - } - - public synchronized void writePacket(byte[] bytes) { - long now = System.currentTimeMillis(); - if(startTime == null) { - startTime = now; - } - - if (serverWasPaused) { - paused = now - startTime - lastSentPacket; - serverWasPaused = false; - } - int timestamp = (int) (now - startTime - paused); - lastSentPacket = timestamp; - queue.add(new PacketData(bytes, timestamp)); - } - - public void requestFinish(Set players, Set> markers) { - active = false; - - try { - ReplayMod.replayFileAppender.startNewReplayFileWriting(); - saveState = 1; - - String mcversion = ReplayMod.getMinecraftVersion(); - - String[] pl = players.toArray(new String[players.size()]); - - String generator = "ReplayMod v" + ReplayMod.getContainer().getVersion(); - - ReplayMetaData metaData = new ReplayMetaData(singleplayer, worldName, generator, (int) lastSentPacket, startTime, pl, mcversion); - - File folder = ReplayFileIO.getReplayFolder(); - - File archive = new File(folder, name + ReplayFile.ZIP_FILE_EXTENSION); - ReplayFileIO.writeReplayFile(archive, file, metaData, markers, resourcePacks, requestToHash); - - FileUtils.forceDelete(file); - FileUtils.deleteDirectory(tempResourcePacksFolder); - - } catch(Exception e) { - e.printStackTrace(); - } finally { - ReplayMod.replayFileAppender.replayFileWritingFinished(); - saveState = 2; - } - } - - } - -} diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/ReplayMetaData.java b/src/main/java/eu/crushedpixel/replaymod/recording/ReplayMetaData.java deleted file mode 100755 index 1ba6d590..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/recording/ReplayMetaData.java +++ /dev/null @@ -1,24 +0,0 @@ -package eu.crushedpixel.replaymod.recording; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class ReplayMetaData { - - private boolean singleplayer; - private String serverName; - private String generator; - private int duration; - private long date; - private String[] players; - private String mcversion; - - public ReplayMetaData copy() { - return new ReplayMetaData(this.singleplayer, this.serverName, this.generator, - this.duration, this.date, this.players, this.mcversion); - } - - public void removeServer() { this.serverName = null; } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/DownloadedFileHandler.java b/src/main/java/eu/crushedpixel/replaymod/registry/DownloadedFileHandler.java index dd02b0b3..b183b624 100644 --- a/src/main/java/eu/crushedpixel/replaymod/registry/DownloadedFileHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/DownloadedFileHandler.java @@ -1,8 +1,7 @@ package eu.crushedpixel.replaymod.registry; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.gui.elements.listeners.ProgressUpdateListener; -import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; @@ -35,7 +34,7 @@ public class DownloadedFileHandler { } private File generateFileForID(int id) { - return new File(downloadFolder, id+ReplayFile.ZIP_FILE_EXTENSION); + return new File(downloadFolder, id+ ".zip"); } public void addToIndex(int id) { diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/FavoritedFileHandler.java b/src/main/java/eu/crushedpixel/replaymod/registry/FavoritedFileHandler.java index eb84b52a..d13db16e 100644 --- a/src/main/java/eu/crushedpixel/replaymod/registry/FavoritedFileHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/FavoritedFileHandler.java @@ -1,7 +1,7 @@ package eu.crushedpixel.replaymod.registry; import com.google.common.primitives.Ints; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.ApiException; import java.io.IOException; diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java b/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java index ef3861d7..b7eea11e 100755 --- a/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java @@ -45,7 +45,6 @@ public class KeybindRegistry { replayModKeyBindings.add(new KeyBinding(KEY_KEYFRAME_PRESETS, Keyboard.KEY_X, "replaymod.title")); replayModKeyBindings.add(new KeyBinding(KEY_PATH_PREVIEW, Keyboard.KEY_H, "replaymod.title")); - replayModKeyBindings.add(new KeyBinding(KEY_ADD_MARKER, Keyboard.KEY_M, "replaymod.title")); replayModKeyBindings.add(new KeyBinding(KEY_SYNC_TIMELINE, Keyboard.KEY_V, "replaymod.title")); replayModKeyBindings.add(new KeyBinding(KEY_THUMBNAIL, Keyboard.KEY_N, "replaymod.title")); diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/LightingHandler.java b/src/main/java/eu/crushedpixel/replaymod/registry/LightingHandler.java index 509f44b4..5ba5dcd7 100644 --- a/src/main/java/eu/crushedpixel/replaymod/registry/LightingHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/LightingHandler.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.registry; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import net.minecraft.client.Minecraft; import net.minecraft.client.settings.GameSettings.Options; diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/PlayerHandler.java b/src/main/java/eu/crushedpixel/replaymod/registry/PlayerHandler.java deleted file mode 100755 index 55812d8e..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/registry/PlayerHandler.java +++ /dev/null @@ -1,71 +0,0 @@ -package eu.crushedpixel.replaymod.registry; - -import com.google.common.base.Predicate; -import eu.crushedpixel.replaymod.entities.CameraEntity; -import eu.crushedpixel.replaymod.gui.GuiPlayerOverview; -import eu.crushedpixel.replaymod.holders.PlayerVisibility; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import net.minecraft.client.Minecraft; -import net.minecraft.entity.player.EntityPlayer; - -import java.util.*; - -public class PlayerHandler { - - private static Minecraft mc = Minecraft.getMinecraft(); - - private static Predicate playerPredicate = new Predicate() { - @Override - public boolean apply(EntityPlayer input) { - return !(input instanceof CameraEntity || input == mc.thePlayer); - } - }; - - private static Set hidden = new HashSet(); - - public static void hidePlayer(EntityPlayer player) { - hidden.remove(player.getUniqueID()); - hidden.add(player.getUniqueID()); - } - - public static void showPlayer(EntityPlayer player) { - hidden.remove(player.getUniqueID()); - } - - public static void setIsVisible(EntityPlayer player, boolean visible) { - if(visible) showPlayer(player); - else hidePlayer(player); - } - - public static void loadPlayerVisibilityConfiguration(PlayerVisibility visibility) { - resetHiddenPlayers(); - if(visibility != null) { - GuiPlayerOverview.defaultSave = true; - Collections.addAll(hidden, visibility.getHidden()); - } else { - GuiPlayerOverview.defaultSave = false; - } - } - - public static Set getHiddenPlayers() { - return hidden; - } - - public static boolean isHidden(UUID uuid) { - return hidden.contains(uuid); - } - - public static void resetHiddenPlayers() { - hidden.clear(); - } - - public static void openPlayerOverview() { - if(!ReplayHandler.isInReplay()) { - return; - } - - @SuppressWarnings("unchecked") - List players = mc.theWorld.getEntities(EntityPlayer.class, playerPredicate); - mc.displayGuiScreen(new GuiPlayerOverview(players)); - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/RatedFileHandler.java b/src/main/java/eu/crushedpixel/replaymod/registry/RatedFileHandler.java index 03d98489..c8798352 100644 --- a/src/main/java/eu/crushedpixel/replaymod/registry/RatedFileHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/RatedFileHandler.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.registry; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.api.ApiException; import eu.crushedpixel.replaymod.api.replay.holders.FileRating; import eu.crushedpixel.replaymod.api.replay.holders.Rating; diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/ReplayFileAppender.java b/src/main/java/eu/crushedpixel/replaymod/registry/ReplayFileAppender.java index d5cd3864..59e12624 100644 --- a/src/main/java/eu/crushedpixel/replaymod/registry/ReplayFileAppender.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/ReplayFileAppender.java @@ -2,7 +2,7 @@ package eu.crushedpixel.replaymod.registry; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; -import eu.crushedpixel.replaymod.events.ReplayExitEvent; +import com.replaymod.replay.events.ReplayCloseEvent; import eu.crushedpixel.replaymod.gui.GuiReplaySaving; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import net.minecraft.client.Minecraft; @@ -91,7 +91,7 @@ public class ReplayFileAppender { } @SubscribeEvent - public void onReplayExit(ReplayExitEvent event) { + public void onReplayExit(ReplayCloseEvent.Post event) { if(!filesToRewrite.isEmpty()) { openGuiSavingScreen(); writeFiles(); diff --git a/src/main/java/eu/crushedpixel/replaymod/renderer/CustomObjectRenderer.java b/src/main/java/eu/crushedpixel/replaymod/renderer/CustomObjectRenderer.java index f75873d0..24c2f791 100644 --- a/src/main/java/eu/crushedpixel/replaymod/renderer/CustomObjectRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/renderer/CustomObjectRenderer.java @@ -1,19 +1,8 @@ package eu.crushedpixel.replaymod.renderer; -import eu.crushedpixel.replaymod.assets.CustomImageObject; -import eu.crushedpixel.replaymod.gui.GuiObjectManager; -import eu.crushedpixel.replaymod.holders.Position; -import eu.crushedpixel.replaymod.holders.Transformation; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.replay.ReplayProcess; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.Tessellator; -import net.minecraft.client.renderer.WorldRenderer; -import net.minecraft.util.ResourceLocation; import net.minecraftforge.client.event.RenderWorldLastEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import org.lwjgl.opengl.GL11; /** * Allows users to render custom images in the World. @@ -24,91 +13,92 @@ public class CustomObjectRenderer { @SubscribeEvent public void renderCustomObjects(RenderWorldLastEvent event) { - if(!ReplayHandler.isInReplay() || mc.getRenderViewEntity() == null || ReplayHandler.getCustomImageObjects().isEmpty()) return; - - double dX = mc.getRenderViewEntity().lastTickPosX + (mc.getRenderViewEntity().posX - mc.getRenderViewEntity().lastTickPosX) * (double)event.partialTicks; - double dY = mc.getRenderViewEntity().lastTickPosY + (mc.getRenderViewEntity().posY - mc.getRenderViewEntity().lastTickPosY) * (double)event.partialTicks; - double dZ = mc.getRenderViewEntity().lastTickPosZ + (mc.getRenderViewEntity().posZ - mc.getRenderViewEntity().lastTickPosZ) * (double)event.partialTicks; - - GlStateManager.enableTexture2D(); - - GlStateManager.enableBlend(); - GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - for(CustomImageObject object : ReplayHandler.getCustomImageObjects()) { - drawCustomImageObject(dX, dY, dZ, object); - } - } - - private void drawCustomImageObject(double playerX, double playerY, double playerZ, CustomImageObject customImageObject) { - ResourceLocation resourceLocation = customImageObject.getResourceLocation(); - - if(customImageObject.getLinkedAsset() == null - || ReplayHandler.getAssetRepository().getAssetByUUID(customImageObject.getLinkedAsset()) == null - || resourceLocation == null) return; - - GlStateManager.pushMatrix(); - GlStateManager.pushAttrib(); - - Tessellator tessellator = Tessellator.getInstance(); - WorldRenderer renderer = tessellator.getWorldRenderer(); - - mc.renderEngine.bindTexture(resourceLocation); - - int renderTimestamp; - if(mc.currentScreen instanceof GuiObjectManager) { - renderTimestamp = ((GuiObjectManager) mc.currentScreen).getObjectKeyframeTimeline().cursorPosition; - } else if(ReplayProcess.isVideoRecording()) { - renderTimestamp = ReplayProcess.getVideoRenderer().getVideoTime(); - } else { - renderTimestamp = ReplayHandler.getRealTimelineCursor(); - } - - Transformation transformation = customImageObject.getTransformations().getTransformationForTimestamp(renderTimestamp); - - Position objectAnchor = transformation.getAnchor(); - Position objectPosition = transformation.getPosition(); - Position objectOrientation = transformation.getOrientation(); - - double x = objectPosition.getX() - playerX; - double y = objectPosition.getY() - playerY; - double z = objectPosition.getZ() - playerZ; - - GL11.glNormal3f(0, 1, 0); - - GlStateManager.translate(x, y + 1.4, z); - - GlStateManager.rotate((float) -objectOrientation.getX(), 0, 1, 0); - GlStateManager.rotate((float) objectOrientation.getY(), 0, 0, 1); - GlStateManager.rotate((float) objectOrientation.getZ(), 1, 0, 0); - - float opacity = (float)transformation.getOpacity() / 100; - GlStateManager.color(1, 1, 1, opacity); - - float width = (float)(customImageObject.getWidth() * transformation.getScale().getX() / 100f); - float height = (float)(customImageObject.getHeight() * transformation.getScale().getY() / 100f); - - float minX = (float)(-width/2 + objectAnchor.getX()); - float maxX = (float)(width/2 + objectAnchor.getX()); - float minY = (float)(-height/2 + objectAnchor.getY()); - float maxY = (float)(height/2 + objectAnchor.getY()); - - renderer.startDrawingQuads(); - - renderer.addVertexWithUV(minX, minY, 0, 1, 1); - renderer.addVertexWithUV(minX, maxY, 0, 1, 0); - renderer.addVertexWithUV(maxX, maxY, 0, 0, 0); - renderer.addVertexWithUV(maxX, minY, 0, 0, 1); - - renderer.addVertexWithUV(maxX, maxY, 0, 0, 0); - renderer.addVertexWithUV(minX, maxY, 0, 1, 0); - renderer.addVertexWithUV(minX, minY, 0, 1, 1); - renderer.addVertexWithUV(maxX, minY, 0, 0, 1); - - tessellator.draw(); - renderer.setTranslation(0, 0, 0); - - GlStateManager.popAttrib(); - GlStateManager.popMatrix(); + // TODO +// if(!ReplayHandler.isInReplay() || mc.getRenderViewEntity() == null || ReplayHandler.getCustomImageObjects().isEmpty()) return; +// +// double dX = mc.getRenderViewEntity().lastTickPosX + (mc.getRenderViewEntity().posX - mc.getRenderViewEntity().lastTickPosX) * (double)event.partialTicks; +// double dY = mc.getRenderViewEntity().lastTickPosY + (mc.getRenderViewEntity().posY - mc.getRenderViewEntity().lastTickPosY) * (double)event.partialTicks; +// double dZ = mc.getRenderViewEntity().lastTickPosZ + (mc.getRenderViewEntity().posZ - mc.getRenderViewEntity().lastTickPosZ) * (double)event.partialTicks; +// +// GlStateManager.enableTexture2D(); +// +// GlStateManager.enableBlend(); +// GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); +// +// for(CustomImageObject object : ReplayHandler.getCustomImageObjects()) { +// drawCustomImageObject(dX, dY, dZ, object); +// } +// } +// +// private void drawCustomImageObject(double playerX, double playerY, double playerZ, CustomImageObject customImageObject) { +// ResourceLocation resourceLocation = customImageObject.getResourceLocation(); +// +// if(customImageObject.getLinkedAsset() == null +// || ReplayHandler.getAssetRepository().getAssetByUUID(customImageObject.getLinkedAsset()) == null +// || resourceLocation == null) return; +// +// GlStateManager.pushMatrix(); +// GlStateManager.pushAttrib(); +// +// Tessellator tessellator = Tessellator.getInstance(); +// WorldRenderer renderer = tessellator.getWorldRenderer(); +// +// mc.renderEngine.bindTexture(resourceLocation); +// +// int renderTimestamp; +// if(mc.currentScreen instanceof GuiObjectManager) { +// renderTimestamp = ((GuiObjectManager) mc.currentScreen).getObjectKeyframeTimeline().cursorPosition; +// } else if(ReplayProcess.isVideoRecording()) { +// renderTimestamp = ReplayProcess.getVideoRenderer().getVideoTime(); +// } else { +// renderTimestamp = ReplayHandler.getRealTimelineCursor(); +// } +// +// Transformation transformation = customImageObject.getTransformations().getTransformationForTimestamp(renderTimestamp); +// +// Position objectAnchor = transformation.getAnchor(); +// Position objectPosition = transformation.getPosition(); +// Position objectOrientation = transformation.getOrientation(); +// +// double x = objectPosition.getX() - playerX; +// double y = objectPosition.getY() - playerY; +// double z = objectPosition.getZ() - playerZ; +// +// GL11.glNormal3f(0, 1, 0); +// +// GlStateManager.translate(x, y + 1.4, z); +// +// GlStateManager.rotate((float) -objectOrientation.getX(), 0, 1, 0); +// GlStateManager.rotate((float) objectOrientation.getY(), 0, 0, 1); +// GlStateManager.rotate((float) objectOrientation.getZ(), 1, 0, 0); +// +// float opacity = (float)transformation.getOpacity() / 100; +// GlStateManager.color(1, 1, 1, opacity); +// +// float width = (float)(customImageObject.getWidth() * transformation.getScale().getX() / 100f); +// float height = (float)(customImageObject.getHeight() * transformation.getScale().getY() / 100f); +// +// float minX = (float)(-width/2 + objectAnchor.getX()); +// float maxX = (float)(width/2 + objectAnchor.getX()); +// float minY = (float)(-height/2 + objectAnchor.getY()); +// float maxY = (float)(height/2 + objectAnchor.getY()); +// +// renderer.startDrawingQuads(); +// +// renderer.addVertexWithUV(minX, minY, 0, 1, 1); +// renderer.addVertexWithUV(minX, maxY, 0, 1, 0); +// renderer.addVertexWithUV(maxX, maxY, 0, 0, 0); +// renderer.addVertexWithUV(maxX, minY, 0, 0, 1); +// +// renderer.addVertexWithUV(maxX, maxY, 0, 0, 0); +// renderer.addVertexWithUV(minX, maxY, 0, 1, 0); +// renderer.addVertexWithUV(minX, minY, 0, 1, 1); +// renderer.addVertexWithUV(maxX, minY, 0, 0, 1); +// +// tessellator.draw(); +// renderer.setTranslation(0, 0, 0); +// +// GlStateManager.popAttrib(); +// GlStateManager.popMatrix(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/renderer/InvisibilityRender.java b/src/main/java/eu/crushedpixel/replaymod/renderer/InvisibilityRender.java index cfb56cd2..586be482 100644 --- a/src/main/java/eu/crushedpixel/replaymod/renderer/InvisibilityRender.java +++ b/src/main/java/eu/crushedpixel/replaymod/renderer/InvisibilityRender.java @@ -1,8 +1,5 @@ package eu.crushedpixel.replaymod.renderer; -import eu.crushedpixel.replaymod.registry.PlayerHandler; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.culling.ICamera; import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.client.renderer.entity.RenderPlayer; @@ -20,8 +17,10 @@ public class InvisibilityRender extends RenderPlayer { @Override public boolean shouldRender(Entity entity, ICamera camera, double camX, double camY, double camZ) { - return !(PlayerHandler.isHidden(entity.getUniqueID()) || (ReplayHandler.isInReplay() - && entity == Minecraft.getMinecraft().thePlayer)) - && super.shouldRender(entity, camera, camX, camY, camZ); + // TODO + return false; +// return !(PlayerHandler.isHidden(entity.getUniqueID()) || (ReplayHandler.isInReplay() +// && entity == Minecraft.getMinecraft().thePlayer)) +// && super.shouldRender(entity, camera, camX, camY, camZ); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java b/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java index 7b4e9742..31ffbf1e 100644 --- a/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java @@ -1,28 +1,23 @@ package eu.crushedpixel.replaymod.renderer; -import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.events.KeyframesModifyEvent; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; import eu.crushedpixel.replaymod.holders.AdvancedPosition; import eu.crushedpixel.replaymod.holders.Keyframe; import eu.crushedpixel.replaymod.holders.SpectatorData; import eu.crushedpixel.replaymod.interpolation.AdvancedPositionKeyframeList; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.WorldRenderer; -import net.minecraft.entity.Entity; import net.minecraft.util.ResourceLocation; import net.minecraftforge.client.event.RenderWorldLastEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.lwjgl.opengl.GL11; import java.awt.*; -import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; -import java.util.List; + +import static com.replaymod.core.ReplayMod.TEXTURE; public class PathPreviewRenderer { @@ -38,83 +33,85 @@ public class PathPreviewRenderer { @SubscribeEvent public void renderCameraPath(RenderWorldLastEvent event) { - if(!ReplayHandler.isInReplay() || ReplayHandler.isInPath() || !ReplayMod.replaySettings.showPathPreview() || mc.gameSettings.hideGUI) return; - - GlStateManager.pushAttrib(); - - int renderDistanceSquared = mc.gameSettings.renderDistanceChunks*16 * mc.gameSettings.renderDistanceChunks*16; - - Entity entity = ReplayHandler.getCameraEntity(); - if(entity == null) return; - - double doubleX = entity.posX; - double doubleY = entity.posY; - double doubleZ = entity.posZ; - - GlStateManager.disableLighting(); - GlStateManager.disableTexture2D(); - GlStateManager.enableBlend(); - - if(keyframes.size() > 1) { - AdvancedPosition previousPosition = null; - - int i = 0; - for(Keyframe keyframe : keyframes) { - int timestamp = keyframe.getRealTimestamp(); - int nextTimestamp = timestamp; - - if(i+1 < keyframes.size()) { - nextTimestamp = keyframes.get(i+1).getRealTimestamp(); - } - - int diff = nextTimestamp-timestamp; - int step = diff/100; - - for(int currentTimestamp = timestamp; currentTimestamp < nextTimestamp; currentTimestamp += step) { - AdvancedPosition position = keyframes.getInterpolatedValueForTimestamp(currentTimestamp, ReplayMod.replaySettings.isLinearMovement()); - if(previousPosition != null) { - drawConnection(doubleX, doubleY, doubleZ, previousPosition, position, DEFAULT_PATH_COLOR, renderDistanceSquared); - } - previousPosition = position; - } - - i++; - } - } - - distanceComparator.setPlayerPos(doubleX, doubleY + 1.4, doubleZ); - - List> distanceSorted = new ArrayList>(keyframes); - Collections.sort(distanceSorted, distanceComparator); - - GlStateManager.enableTexture2D(); - GlStateManager.blendFunc(GL11.GL_DST_COLOR, GL11.GL_SRC_COLOR); - - GlStateManager.disableDepth(); - - for(Keyframe kf : distanceSorted) { - if(kf.getValue().distanceSquared(entity.posX, entity.posY, entity.posZ) < renderDistanceSquared) { - drawPoint(doubleX, doubleY, doubleZ, kf); - } - } - - if(ReplayHandler.getPositionKeyframes().size() > 1) { - AdvancedPosition cameraPosition = ReplayHandler.getPositionKeyframes().getInterpolatedValueForTimestamp(ReplayHandler.getRealTimelineCursor(), - ReplayMod.replaySettings.isLinearMovement()); - if(cameraPosition != null) { - GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - drawCamera(doubleX, doubleY, doubleZ, cameraPosition); - } - } - - GlStateManager.disableBlend(); - - GlStateManager.popAttrib(); + // TODO +// if(!ReplayHandler.isInReplay() || ReplayHandler.isInPath() || !ReplayMod.replaySettings.showPathPreview() || mc.gameSettings.hideGUI) return; +// +// GlStateManager.pushAttrib(); +// +// int renderDistanceSquared = mc.gameSettings.renderDistanceChunks*16 * mc.gameSettings.renderDistanceChunks*16; +// +// Entity entity = ReplayHandler.getCameraEntity(); +// if(entity == null) return; +// +// double doubleX = entity.posX; +// double doubleY = entity.posY; +// double doubleZ = entity.posZ; +// +// GlStateManager.disableLighting(); +// GlStateManager.disableTexture2D(); +// GlStateManager.enableBlend(); +// +// if(keyframes.size() > 1) { +// AdvancedPosition previousPosition = null; +// +// int i = 0; +// for(Keyframe keyframe : keyframes) { +// int timestamp = keyframe.getRealTimestamp(); +// int nextTimestamp = timestamp; +// +// if(i+1 < keyframes.size()) { +// nextTimestamp = keyframes.get(i+1).getRealTimestamp(); +// } +// +// int diff = nextTimestamp-timestamp; +// int step = diff/100; +// +// for(int currentTimestamp = timestamp; currentTimestamp < nextTimestamp; currentTimestamp += step) { +// AdvancedPosition position = keyframes.getInterpolatedValueForTimestamp(currentTimestamp, ReplayMod.replaySettings.isLinearMovement()); +// if(previousPosition != null) { +// drawConnection(doubleX, doubleY, doubleZ, previousPosition, position, DEFAULT_PATH_COLOR, renderDistanceSquared); +// } +// previousPosition = position; +// } +// +// i++; +// } +// } +// +// distanceComparator.setPlayerPos(doubleX, doubleY + 1.4, doubleZ); +// +// List> distanceSorted = new ArrayList>(keyframes); +// Collections.sort(distanceSorted, distanceComparator); +// +// GlStateManager.enableTexture2D(); +// GlStateManager.blendFunc(GL11.GL_DST_COLOR, GL11.GL_SRC_COLOR); +// +// GlStateManager.disableDepth(); +// +// for(Keyframe kf : distanceSorted) { +// if(kf.getValue().distanceSquared(entity.posX, entity.posY, entity.posZ) < renderDistanceSquared) { +// drawPoint(doubleX, doubleY, doubleZ, kf); +// } +// } +// +// if(ReplayHandler.getPositionKeyframes().size() > 1) { +// AdvancedPosition cameraPosition = ReplayHandler.getPositionKeyframes().getInterpolatedValueForTimestamp(ReplayHandler.getRealTimelineCursor(), +// ReplayMod.replaySettings.isLinearMovement()); +// if(cameraPosition != null) { +// GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); +// drawCamera(doubleX, doubleY, doubleZ, cameraPosition); +// } +// } +// +// GlStateManager.disableBlend(); +// +// GlStateManager.popAttrib(); } @SubscribeEvent public void recalcSpline(KeyframesModifyEvent event) { - keyframes = ReplayHandler.getPositionKeyframes(); + // TODO +// keyframes = ReplayHandler.getPositionKeyframes(); } private class DistanceComparator implements Comparator> { @@ -172,7 +169,7 @@ public class PathPreviewRenderer { Tessellator tessellator = Tessellator.getInstance(); WorldRenderer renderer = tessellator.getWorldRenderer(); - mc.renderEngine.bindTexture(GuiReplayOverlay.replay_gui); + mc.renderEngine.bindTexture(TEXTURE); AdvancedPosition pos1 = kf.getValue(); @@ -197,9 +194,10 @@ public class PathPreviewRenderer { float size = 10/128f; - if(kf.equals(ReplayHandler.getSelectedKeyframe())) { - posY += size; - } + // TODO +// if(kf.equals(ReplayHandler.getSelectedKeyframe())) { +// posY += size; +// } if(pos1 instanceof SpectatorData) { posX += size; diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java index 0f9338e5..05e6243c 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -1,595 +1,522 @@ package eu.crushedpixel.replaymod.replay; -import com.mojang.authlib.GameProfile; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.assets.AssetRepository; -import eu.crushedpixel.replaymod.assets.CustomImageObject; -import eu.crushedpixel.replaymod.assets.CustomObjectRepository; -import eu.crushedpixel.replaymod.entities.CameraEntity; -import eu.crushedpixel.replaymod.events.KeyframesModifyEvent; -import eu.crushedpixel.replaymod.events.ReplayExitEvent; -import eu.crushedpixel.replaymod.gui.overlay.GuiReplayOverlay; -import eu.crushedpixel.replaymod.holders.*; -import eu.crushedpixel.replaymod.interpolation.AdvancedPositionKeyframeList; -import eu.crushedpixel.replaymod.interpolation.KeyframeList; -import eu.crushedpixel.replaymod.preparation.EntityPositionTracker; -import eu.crushedpixel.replaymod.registry.LightingHandler; -import eu.crushedpixel.replaymod.registry.PlayerHandler; -import eu.crushedpixel.replaymod.registry.ReplayGuiRegistry; -import eu.crushedpixel.replaymod.settings.RenderOptions; -import eu.crushedpixel.replaymod.utils.ReplayFile; -import eu.crushedpixel.replaymod.utils.ReplayFileIO; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.embedded.EmbeddedChannel; -import lombok.Getter; -import lombok.Setter; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; -import net.minecraft.client.network.NetHandlerPlayClient; -import net.minecraft.client.resources.I18n; -import net.minecraft.crash.CrashReport; -import net.minecraft.entity.Entity; -import net.minecraft.init.Bootstrap; -import net.minecraft.network.EnumPacketDirection; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.play.INetHandlerPlayClient; -import net.minecraft.util.ReportedException; -import net.minecraftforge.fml.common.FMLCommonHandler; -import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; - -import java.io.File; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.*; - +// For later use in render and path module public class ReplayHandler { - - public static long lastExit = 0; - private static NetworkManager networkManager; - private static Minecraft mc = Minecraft.getMinecraft(); - private static EmbeddedChannel channel; - private static int realTimelinePosition = 0; - - private static Keyframe selectedKeyframe; - - private static boolean inPath = false; - - private static AdvancedPositionKeyframeList positionKeyframes = new AdvancedPositionKeyframeList(); - private static KeyframeList timeKeyframes = new KeyframeList(); - - private static boolean inReplay = false; - private static AdvancedPosition lastPosition = null; - - private static KeyframeList initialMarkers = new KeyframeList(); - private static KeyframeList markerKeyframes = new KeyframeList(); - - private static float cameraTilt = 0; - - private static KeyframeSet[] keyframeRepository = new KeyframeSet[]{}; - - @Getter @Setter - private static AssetRepository assetRepository; - - private static CustomObjectRepository customImageObjects = new CustomObjectRepository(); - - /** - * The file currently being played. - */ - private static ReplayFile currentReplayFile; - - /** - * Currently active replay restrictions. - */ - private static Restrictions restrictions; - - /** - * The EntityPositionTracker for the current Replay File. - */ - @Getter - private static EntityPositionTracker entityPositionTracker; - - public static KeyframeSet[] getKeyframeRepository() { - return keyframeRepository; - } - - public static void setKeyframeRepository(KeyframeSet[] repo, boolean write) { - keyframeRepository = repo; - if(write) { - try { - File tempFile = File.createTempFile(ReplayFile.ENTRY_PATHS, "json"); - - ReplayFileIO.write(repo, tempFile); - - ReplayMod.replayFileAppender.registerModifiedFile(tempFile, ReplayFile.ENTRY_PATHS, getReplayFile()); - } catch(Exception e) { - e.printStackTrace(); - } - } - } - - public static KeyframeList getMarkerKeyframes() { - return markerKeyframes; - } - - public static void setMarkers(KeyframeList m, boolean write) { - markerKeyframes.clear(); - markerKeyframes.addAll(m); - - if(write) { - try { - File tempFile = File.createTempFile(ReplayFile.ENTRY_MARKERS, "json"); - - ReplayFileIO.write(m, tempFile); - - ReplayMod.replayFileAppender.registerModifiedFile(tempFile, ReplayFile.ENTRY_MARKERS, getReplayFile()); - } catch(Exception e) { - e.printStackTrace(); - } - } - } - - public static void useKeyframePresetFromRepository(int index) { - useKeyframePreset(keyframeRepository[index]); - } - - public static void useKeyframePreset(KeyframeSet keyframeSet) { - setCustomImageObjects(Arrays.asList(keyframeSet.getCustomObjects())); - - Keyframe[] kfs = keyframeSet.getKeyframes(); - - positionKeyframes.clear(); - timeKeyframes.clear(); - for(Keyframe kf : kfs) { - addKeyframe(kf); - } - - selectKeyframe(null); - - fireKeyframesModifyEvent(); - } - - public static void spectateEntity(Entity e) { - getCameraEntity().spectate(e); - } - - public static void spectateCamera() { - spectateEntity(null); - } - - public static boolean isCamera() { - return mc.thePlayer instanceof CameraEntity && mc.thePlayer == mc.getRenderViewEntity(); - } - - public static void startPath(RenderOptions renderOptions, boolean fromStart) { - if(!ReplayHandler.isInPath()) { - try { - ReplayProcess.startReplayProcess(renderOptions, fromStart); - } catch (ReportedException e) { - // We have to manually unwrap OOM errors as Minecraft doesn't handle them when they're wrapped - Throwable prevCause = null; - Throwable cause = e; - while (cause != null && cause != prevCause) { - if (cause instanceof OutOfMemoryError) { - // Nevertheless save the crash report in case we actually need it - Minecraft minecraft = Minecraft.getMinecraft(); - CrashReport crashReport = e.getCrashReport(); - minecraft.addGraphicsAndWorldToCrashReport(crashReport); - Bootstrap.printToSYSOUT(crashReport.getCompleteReport()); - File folder = new File(minecraft.mcDataDir, "crash-reports"); - File file = new File(folder, "crash-" + (new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss")).format(new Date()) + "-client.txt"); - crashReport.saveToFile(file); - throw (OutOfMemoryError) cause; - } - prevCause = cause; - cause = e.getCause(); - } - throw e; - } - } - } - - public static void interruptReplay() { - ReplayProcess.stopReplayProcess(false); - } - - public static boolean isInPath() { - return inPath; - } - - public static void setInPath(boolean replaying) { - inPath = replaying; - } - - public static CameraEntity getCameraEntity() { - return mc.thePlayer instanceof CameraEntity ? (CameraEntity) mc.thePlayer : null; - } - - public static float getCameraTilt() { - return cameraTilt; - } - - public static void setCameraTilt(float tilt) { - cameraTilt = tilt; - } - - public static void addCameraTilt(float tilt) { - cameraTilt += tilt; - } - - public static void toggleMarker() { - if(selectedKeyframe != null && selectedKeyframe.getValue() instanceof Marker) { - markerKeyframes.remove(selectedKeyframe); - ReplayHandler.selectKeyframe(null); - } else { - AdvancedPosition pos = new AdvancedPosition(mc.getRenderViewEntity()); - int timestamp = ReplayMod.replaySender.currentTimeStamp(); - Keyframe markerKeyframe = new Keyframe(timestamp, new Marker(null, pos)); - markerKeyframes.add(markerKeyframe); - ReplayHandler.selectKeyframe(markerKeyframe); - } - } - - public static void addTimeKeyframe(Keyframe keyframe) { - timeKeyframes.add(keyframe); - selectKeyframe(keyframe); - - fireKeyframesModifyEvent(); - } - - public static void addPositionKeyframe(Keyframe keyframe) { - positionKeyframes.add(keyframe); - selectKeyframe(keyframe); - - fireKeyframesModifyEvent(); - } - - @SuppressWarnings("unchecked") - public static void addKeyframe(Keyframe keyframe) { - if(keyframe.getValue() instanceof AdvancedPosition) { - addPositionKeyframe(keyframe); - } else if(keyframe.getValue() instanceof TimestampValue) { - addTimeKeyframe(keyframe); - } - } - - public static void removeKeyframe(Keyframe keyframe) { - if(keyframe.getValue() instanceof AdvancedPosition) { - positionKeyframes.remove(keyframe); - } else if(keyframe.getValue() instanceof TimestampValue) { - timeKeyframes.remove(keyframe); - } else if(keyframe.getValue() instanceof Marker) { - markerKeyframes.remove(keyframe); - } - - if(keyframe == selectedKeyframe) { - selectKeyframe(null); - } - - fireKeyframesModifyEvent(); - } - - public static AdvancedPositionKeyframeList getPositionKeyframes() { - return positionKeyframes; - } - - public static KeyframeList getTimeKeyframes() { - return timeKeyframes; - } - - public static ArrayList getAllKeyframes() { - ArrayList keyframeList = new ArrayList(); - keyframeList.addAll(positionKeyframes); - keyframeList.addAll(timeKeyframes); - - return keyframeList; - } - - public static void resetKeyframes(final boolean resetMarkers, boolean callback) { - if(getPositionKeyframes().isEmpty() && getTimeKeyframes().isEmpty()) return; - - if(!callback) { - resetKeyframes(resetMarkers); - } else { - mc.displayGuiScreen(new GuiYesNo(new GuiYesNoCallback() { - @Override - public void confirmClicked(boolean result, int id) { - if(result) { - resetKeyframes(resetMarkers); - } - - mc.displayGuiScreen(null); - } - }, I18n.format("replaymod.gui.clearcallback.title"), I18n.format("replaymod.gui.clearcallback.message"), 1)); - } - } - - private static void resetKeyframes(boolean resetMarkers) { - timeKeyframes.clear(); - positionKeyframes.clear(); - selectKeyframe(null); - - if(resetMarkers) { - markerKeyframes.clear(); - } - - setRealTimelineCursor(0); - - fireKeyframesModifyEvent(); - } - - public static void selectKeyframe(Keyframe kf) { - selectedKeyframe = kf; - } - - public static boolean isSelected(Keyframe kf) { - return kf == selectedKeyframe; - } - - public static boolean isInReplay() { - return inReplay; - } - - public static void startReplay(File file) { - startReplay(file, true); - } - - public static void startReplay(File file, boolean asyncMode) { - entityPositionTracker = new EntityPositionTracker(file); - try { - entityPositionTracker.load(); - } catch(IOException ioe) { - ioe.printStackTrace(); - } - - ReplayMod.chatMessageHandler.initialize(); - mc.ingameGUI.getChatGUI().clearChatMessages(); - resetKeyframes(true); - - if(ReplayMod.replaySender != null) { - ReplayMod.replaySender.terminateReplay(); - } - - if(channel != null) { - channel.close(); - } - - setCameraTilt(0); - - restrictions = new Restrictions(); - - networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) { - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) { - t.printStackTrace(); - } - }; - INetHandlerPlayClient pc = new NetHandlerPlayClient(mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player")); - networkManager.setNetHandler(pc); - - channel = new EmbeddedChannel(networkManager); - channel.attr(NetworkDispatcher.FML_DISPATCHER).set(new NetworkDispatcher(networkManager)); - - // Open replay - try { - currentReplayFile = new ReplayFile(file); - } catch(Exception e) { - throw new RuntimeException(e); - } - - KeyframeSet[] paths = currentReplayFile.paths().get(); - ReplayHandler.setKeyframeRepository(paths == null ? new KeyframeSet[0] : paths, false); - - ReplayHandler.selectKeyframe(null); - - List> rawMarkerList = currentReplayFile.markers().get(); - if (rawMarkerList == null) { - rawMarkerList = Collections.emptyList(); - } - KeyframeList markerList = new KeyframeList(rawMarkerList); - ReplayHandler.setMarkers(markerList, false); - ReplayHandler.initialMarkers = markerList; - - PlayerVisibility visibility = currentReplayFile.visibility().get(); - PlayerHandler.loadPlayerVisibilityConfiguration(visibility); - - //load assets - assetRepository = currentReplayFile.assetRepository().get(); - - customImageObjects = new CustomObjectRepository(); - - ReplayMod.replaySender = new ReplaySender(currentReplayFile, asyncMode); - channel.pipeline().addFirst(ReplayMod.replaySender); - channel.pipeline().fireChannelActive(); - - try { - ReplayMod.overlay.resetUI(true); - } catch(Exception e) { - e.printStackTrace(); - // TODO: Fix exception - } - - //Load lighting and trigger update - ReplayMod.replaySettings.setLightingEnabled(ReplayMod.replaySettings.isLightingEnabled()); - - inReplay = true; - } - - public static void restartReplay() { - mc.ingameGUI.getChatGUI().clearChatMessages(); - - if(channel != null) { - channel.close(); - } - - restrictions = new Restrictions(); - - networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) { - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) { - t.printStackTrace(); - } - }; - - INetHandlerPlayClient pc = new NetHandlerPlayClient(mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player")); - networkManager.setNetHandler(pc); - - channel = new EmbeddedChannel(networkManager); - channel.attr(NetworkDispatcher.FML_DISPATCHER).set(new NetworkDispatcher(networkManager)); - - channel.pipeline().addFirst(ReplayMod.replaySender); - channel.pipeline().fireChannelActive(); - - mc.addScheduledTask(new Runnable() { - @Override - public void run() { - try { - ReplayMod.overlay.resetUI(false); - } catch(Exception e) { - e.printStackTrace(); - } - } - }); - - inReplay = true; - } - - public static void endReplay() { - if(ReplayMod.replaySender != null) { - ReplayMod.replaySender.terminateReplay(); - } - - if (currentReplayFile != null) { - try { - - //only if Marker keyframes changed, rewrite them - if(!initialMarkers.equals(markerKeyframes)) { - File markerFile = File.createTempFile(ReplayFile.ENTRY_MARKERS, "json"); - ReplayFileIO.write(getMarkerKeyframes(), markerFile); - ReplayMod.replayFileAppender.registerModifiedFile(markerFile, ReplayFile.ENTRY_MARKERS, ReplayHandler.getReplayFile()); - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - currentReplayFile.close(); - } catch(IOException e) { - e.printStackTrace(); - } - } - - currentReplayFile = null; - } - - resetKeyframes(true); - - PlayerHandler.resetHiddenPlayers(); - ReplayGuiRegistry.show(); - LightingHandler.setLighting(false); - - if (mc.theWorld != null) { - mc.theWorld.sendQuittingDisconnectingPacket(); - mc.loadWorld(null); - } - - CameraEntity.spectating = null; - - inReplay = false; - lastExit = System.currentTimeMillis(); - - FMLCommonHandler.instance().bus().post(new ReplayExitEvent()); - } - - public static void setInReplay(boolean inReplay1) { - inReplay = inReplay1; - } - - public static Keyframe getSelectedKeyframe() { - return selectedKeyframe; - } - - public static int getRealTimelineCursor() { - return realTimelinePosition; - } - - public static void setRealTimelineCursor(int pos) { - realTimelinePosition = pos; - } - - public static AdvancedPosition getLastPosition() { - return lastPosition; - } - - @Getter - private static boolean forceLastPosition = false; - - public static void setLastPosition(AdvancedPosition position, boolean force) { - lastPosition = position; - forceLastPosition = force; - } - - public static void moveCameraToLastPosition() { - //get the camera position we had before jumping in time - AdvancedPosition pos = ReplayHandler.getLastPosition(); - CameraEntity cam = ReplayHandler.getCameraEntity(); - if (cam != null && pos != null) { - // Move camera back in case we have been respawned, unless we're more than ReplayMod.TP_DISTANCE_LIMIT away from that point - // this is ignored if we explicitly said to respect this position, e.g. when jumping to marker keyframes. - if (ReplayHandler.isForceLastPosition() || - (Math.abs(pos.getX() - cam.posX) < ReplayMod.TP_DISTANCE_LIMIT && - Math.abs(pos.getZ() - cam.posZ) < ReplayMod.TP_DISTANCE_LIMIT)) { - cam.moveAbsolute(pos); - } - } - } - - public static File getReplayFile() { - return currentReplayFile == null ? null : currentReplayFile.getFile(); - } - - /** - * Synchronizes the cursor on the Keyframe Timeline with the Replay Time - * @param ignoreReplaySpeed If true, it always uses 1.0 as the stretch factor - */ - public static void syncTimeCursor(boolean ignoreReplaySpeed) { - selectKeyframe(null); - - int curTime = ReplayMod.replaySender.currentTimeStamp(); - - int prevTime, prevRealTime; - - Keyframe keyframe = timeKeyframes.last(); - - if(keyframe == null) { - prevTime = 0; - prevRealTime = 0; - } else { - prevTime = (int)keyframe.getValue().value; - prevRealTime = keyframe.getRealTimestamp(); - } - - double speed = ignoreReplaySpeed ? 1 : ReplayMod.overlay.getSpeedSliderValue(); - - int newCursorPos = Math.min(GuiReplayOverlay.KEYFRAME_TIMELINE_LENGTH, (int)(prevRealTime+((curTime-prevTime)/speed))); - - setRealTimelineCursor(newCursorPos); - } - - public static List getCustomImageObjects() { - return customImageObjects.getObjects(); - } - - public static void setCustomImageObjects(List objects) { - customImageObjects.setObjects(new ArrayList(objects)); - } - - public static void fireKeyframesModifyEvent() { - FMLCommonHandler.instance().bus().post(new KeyframesModifyEvent(positionKeyframes, timeKeyframes)); - positionKeyframes.recalculate(ReplayMod.replaySettings.isLinearMovement()); - timeKeyframes.recalculate(ReplayMod.replaySettings.isLinearMovement()); - } - - public static Restrictions getRestrictions() { - return restrictions; - } +// +// private static Minecraft mc = Minecraft.getMinecraft(); +// private static long lastExit; +// private static NetworkManager networkManager; +// private static EmbeddedChannel channel; +// private int realTimelinePosition = 0; +// +// private static Keyframe selectedKeyframe; +// +// private static boolean inPath = false; +// +// private static AdvancedPositionKeyframeList positionKeyframes = new AdvancedPositionKeyframeList(); +// private static KeyframeList timeKeyframes = new KeyframeList(); +// +// private static boolean inReplay = false; +// private static AdvancedPosition lastPosition = null; +// +// private static Set markers; +// +// private static float cameraTilt = 0; +// +// private static KeyframeSet[] keyframeRepository = new KeyframeSet[]{}; +// +// @Getter @Setter +// private static AssetRepository assetRepository; +// +// private static CustomObjectRepository customImageObjects = new CustomObjectRepository(); +// +// /** +// * The file currently being played. +// */ +// private static ReplayFile currentReplayFile; +// +// /** +// * Currently active replay restrictions. +// */ +// private static Restrictions restrictions; +// +// /** +// * The EntityPositionTracker for the current Replay File. +// */ +// @Getter +// private static EntityPositionTracker entityPositionTracker; +// +// public static KeyframeSet[] getKeyframeRepository() { +// return keyframeRepository; +// } +// +// public static void setKeyframeRepository(KeyframeSet[] repo, boolean write) { +// keyframeRepository = repo; +// if(write) { +// try (OutputStream out = currentReplayFile.write("paths.json")) { +// out.write(new Gson().toJson(repo).getBytes()); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// } +// +// public static Set getMarkers() { +// return markers; +// } +// +// public static void setMarkers(Set markers, boolean write) { +// ReplayHandler.markers = markers; +// if(write) { +// try { +// currentReplayFile.writeMarkers(markers); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// } +// +// public static void useKeyframePresetFromRepository(int index) { +// useKeyframePreset(keyframeRepository[index]); +// } +// +// public static void useKeyframePreset(KeyframeSet keyframeSet) { +// setCustomImageObjects(Arrays.asList(keyframeSet.getCustomObjects())); +// +// Keyframe[] kfs = keyframeSet.getKeyframes(); +// +// positionKeyframes.clear(); +// timeKeyframes.clear(); +// for(Keyframe kf : kfs) { +// addKeyframe(kf); +// } +// +// selectKeyframe(null); +// +// fireKeyframesModifyEvent(); +// } +// +// public static void spectateEntity(Entity e) { +// getCameraEntity().spectate(e); +// } +// +// public static void spectateCamera() { +// spectateEntity(null); +// } +// +// public static boolean isCamera() { +// return mc.thePlayer instanceof CameraEntity && mc.thePlayer == mc.getRenderViewEntity(); +// } +// +// public static void startPath(RenderOptions renderOptions, boolean fromStart) { +// if(!com.replaymod.replay.ReplayHandler.isInPath()) { +// try { +// ReplayProcess.startReplayProcess(renderOptions, fromStart); +// } catch (ReportedException e) { +// // We have to manually unwrap OOM errors as Minecraft doesn't handle them when they're wrapped +// Throwable prevCause = null; +// Throwable cause = e; +// while (cause != null && cause != prevCause) { +// if (cause instanceof OutOfMemoryError) { +// // Nevertheless save the crash report in case we actually need it +// Minecraft minecraft = Minecraft.getMinecraft(); +// CrashReport crashReport = e.getCrashReport(); +// minecraft.addGraphicsAndWorldToCrashReport(crashReport); +// Bootstrap.printToSYSOUT(crashReport.getCompleteReport()); +// File folder = new File(minecraft.mcDataDir, "crash-reports"); +// File file = new File(folder, "crash-" + (new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss")).format(new Date()) + "-client.txt"); +// crashReport.saveToFile(file); +// throw (OutOfMemoryError) cause; +// } +// prevCause = cause; +// cause = e.getCause(); +// } +// throw e; +// } +// } +// } +// +// public static void interruptReplay() { +// ReplayProcess.stopReplayProcess(false); +// } +// +// public static boolean isInPath() { +// return inPath; +// } +// +// public static void setInPath(boolean replaying) { +// inPath = replaying; +// } +// +// public static CameraEntity getCameraEntity() { +// return mc.thePlayer instanceof CameraEntity ? (CameraEntity) mc.thePlayer : null; +// } +// +// public static float getCameraTilt() { +// return cameraTilt; +// } +// +// public static void setCameraTilt(float tilt) { +// cameraTilt = tilt; +// } +// +// public static void addCameraTilt(float tilt) { +// cameraTilt += tilt; +// } +// +// public static void toggleMarker() { +// // TODO +// } +// +// public static void addTimeKeyframe(Keyframe keyframe) { +// timeKeyframes.add(keyframe); +// selectKeyframe(keyframe); +// +// fireKeyframesModifyEvent(); +// } +// +// public static void addPositionKeyframe(Keyframe keyframe) { +// positionKeyframes.add(keyframe); +// selectKeyframe(keyframe); +// +// fireKeyframesModifyEvent(); +// } +// +// @SuppressWarnings("unchecked") +// public static void addKeyframe(Keyframe keyframe) { +// if(keyframe.getValue() instanceof AdvancedPosition) { +// addPositionKeyframe(keyframe); +// } else if(keyframe.getValue() instanceof TimestampValue) { +// addTimeKeyframe(keyframe); +// } +// } +// +// public static void removeKeyframe(Keyframe keyframe) { +// if(keyframe.getValue() instanceof AdvancedPosition) { +// positionKeyframes.remove(keyframe); +// } else if(keyframe.getValue() instanceof TimestampValue) { +// timeKeyframes.remove(keyframe); +// } +// // TODO Marker +// +// if(keyframe == selectedKeyframe) { +// selectKeyframe(null); +// } +// +// fireKeyframesModifyEvent(); +// } +// +// public static AdvancedPositionKeyframeList getPositionKeyframes() { +// return positionKeyframes; +// } +// +// public static KeyframeList getTimeKeyframes() { +// return timeKeyframes; +// } +// +// public static ArrayList getAllKeyframes() { +// ArrayList keyframeList = new ArrayList(); +// keyframeList.addAll(positionKeyframes); +// keyframeList.addAll(timeKeyframes); +// +// return keyframeList; +// } +// +// public static void resetKeyframes(final boolean resetMarkers, boolean callback) { +// if(getPositionKeyframes().isEmpty() && getTimeKeyframes().isEmpty()) return; +// +// if(!callback) { +// resetKeyframes(resetMarkers); +// } else { +// mc.displayGuiScreen(new GuiYesNo(new GuiYesNoCallback() { +// @Override +// public void confirmClicked(boolean result, int id) { +// if(result) { +// resetKeyframes(resetMarkers); +// } +// +// mc.displayGuiScreen(null); +// } +// }, I18n.format("replaymod.gui.clearcallback.title"), I18n.format("replaymod.gui.clearcallback.message"), 1)); +// } +// } +// +// private static void resetKeyframes(boolean resetMarkers) { +// timeKeyframes.clear(); +// positionKeyframes.clear(); +// selectKeyframe(null); +// +// if(resetMarkers) { +// // TODO Markers +// } +// +//// setRealTimelineCursor(0); TODO +// +// fireKeyframesModifyEvent(); +// } +// +// public static void selectKeyframe(Keyframe kf) { +// selectedKeyframe = kf; +// } +// +// public static boolean isSelected(Keyframe kf) { +// return kf == selectedKeyframe; +// } +// +// public static boolean isInReplay() { +// return inReplay; +// } +// +// public static void startReplay(File file) { +// try { +// startReplay(file, true); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// +// public static void startReplay(File file, boolean asyncMode) throws IOException { +// entityPositionTracker = new EntityPositionTracker(file); +// entityPositionTracker.load(); +// +// ReplayMod.chatMessageHandler.initialize(); +// mc.ingameGUI.getChatGUI().clearChatMessages(); +// resetKeyframes(true); +// +// if(ReplayMod.replaySender != null) { +// ReplayMod.replaySender.terminateReplay(); +// } +// +// if(channel != null) { +// channel.close(); +// } +// +// setCameraTilt(0); +// +// restrictions = new Restrictions(); +// +// networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) { +// @Override +// public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) { +// t.printStackTrace(); +// } +// }; +// INetHandlerPlayClient pc = new NetHandlerPlayClient(mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player")); +// networkManager.setNetHandler(pc); +// +// channel = new EmbeddedChannel(networkManager); +// channel.attr(NetworkDispatcher.FML_DISPATCHER).set(new NetworkDispatcher(networkManager)); +// +// // Open replay +// currentReplayFile = new ZipReplayFile(new ReplayStudio(), file); +// +// KeyframeSet[] paths; +// Optional is = currentReplayFile.get("paths.json"); +// if (!is.isPresent()) { +// is = currentReplayFile.get("paths"); +// } +// if (is.isPresent()) { +// try (Reader reader = new InputStreamReader(is.get())) { +// paths = new GsonBuilder().registerTypeAdapter(KeyframeSet[].class, new LegacyKeyframeSetAdapter()) +// .create().fromJson(reader, KeyframeSet[].class); +// } +// } else { +// paths = new KeyframeSet[0]; +// } +// com.replaymod.replay.ReplayHandler.setKeyframeRepository(paths == null ? new KeyframeSet[0] : paths, false); +// com.replaymod.replay.ReplayHandler.selectKeyframe(null); +// com.replaymod.replay.ReplayHandler.setMarkers(currentReplayFile.getMarkers().or(Collections.emptySet()), false); +// PlayerHandler.loadPlayerVisibilityConfiguration(currentReplayFile.getInvisiblePlayers()); +// +// //load assets +// assetRepository = new AssetRepository(); +// for (ReplayAssetEntry entry : currentReplayFile.getAssets()) { +// UUID uuid = entry.getUuid(); +// assetRepository.addAsset(entry.getName(), currentReplayFile.getAsset(uuid).get(), uuid); +// } +// +// customImageObjects = new CustomObjectRepository(); +// +// ReplayMod.replaySender = new ReplaySender(currentReplayFile, asyncMode); +// channel.pipeline().addFirst(ReplayMod.replaySender); +// channel.pipeline().fireChannelActive(); +// +// try { +// ReplayMod.overlay.resetUI(true); +// } catch(Exception e) { +// e.printStackTrace(); +// // TODO: Fix exception +// } +// +// //Load lighting and trigger update +// ReplayMod.replaySettings.setLightingEnabled(ReplayMod.replaySettings.isLightingEnabled()); +// +// inReplay = true; +// } +// +// public static void restartReplay() { +// mc.ingameGUI.getChatGUI().clearChatMessages(); +// +// if(channel != null) { +// channel.close(); +// } +// +// restrictions = new Restrictions(); +// +// networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) { +// @Override +// public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) { +// t.printStackTrace(); +// } +// }; +// +// INetHandlerPlayClient pc = new NetHandlerPlayClient(mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player")); +// networkManager.setNetHandler(pc); +// +// channel = new EmbeddedChannel(networkManager); +// channel.attr(NetworkDispatcher.FML_DISPATCHER).set(new NetworkDispatcher(networkManager)); +// +// channel.pipeline().addFirst(ReplayMod.replaySender); +// channel.pipeline().fireChannelActive(); +// +// mc.addScheduledTask(new Runnable() { +// @Override +// public void run() { +// try { +// ReplayMod.overlay.resetUI(false); +// } catch(Exception e) { +// e.printStackTrace(); +// } +// } +// }); +// +// inReplay = true; +// } +// +// public static void endReplay() { +// if(ReplayMod.replaySender != null) { +// ReplayMod.replaySender.terminateReplay(); +// } +// +// if (currentReplayFile != null) { +// try { +// currentReplayFile.save(); +// currentReplayFile.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// currentReplayFile = null; +// } +// +// resetKeyframes(true); +// +// PlayerHandler.resetHiddenPlayers(); +// ReplayGuiRegistry.show(); +// LightingHandler.setLighting(false); +// +// if (mc.theWorld != null) { +// mc.theWorld.sendQuittingDisconnectingPacket(); +// mc.loadWorld(null); +// } +// +// CameraEntity.spectating = null; +// +// inReplay = false; +// lastExit = System.currentTimeMillis(); +// +// FMLCommonHandler.instance().bus().post(new ReplayExitEvent()); +// } +// +// public static void setInReplay(boolean inReplay1) { +// inReplay = inReplay1; +// } +// +// public static Keyframe getSelectedKeyframe() { +// return selectedKeyframe; +// } +// +//// public static int getRealTimelineCursor() { +//// return realTimelinePosition; +//// } +//// +//// public static void setRealTimelineCursor(int pos) { +//// realTimelinePosition = pos; +//// } +// +// public static AdvancedPosition getLastPosition() { +// return lastPosition; +// } +// +// @Getter +// private static boolean forceLastPosition = false; +// +// public static void setLastPosition(AdvancedPosition position, boolean force) { +// lastPosition = position; +// forceLastPosition = force; +// } +// +// public static void moveCameraToLastPosition() { +// //get the camera position we had before jumping in time +// AdvancedPosition pos = com.replaymod.replay.ReplayHandler.getLastPosition(); +// CameraEntity cam = com.replaymod.replay.ReplayHandler.getCameraEntity(); +// if (cam != null && pos != null) { +// // Move camera back in case we have been respawned, unless we're more than ReplayMod.TP_DISTANCE_LIMIT away from that point +// // this is ignored if we explicitly said to respect this position, e.g. when jumping to marker keyframes. +// if (com.replaymod.replay.ReplayHandler.isForceLastPosition() || +// (Math.abs(pos.getX() - cam.posX) < ReplayMod.TP_DISTANCE_LIMIT && +// Math.abs(pos.getZ() - cam.posZ) < ReplayMod.TP_DISTANCE_LIMIT)) { +// cam.moveAbsolute(pos); +// } +// } +// } +// +// public static ReplayFile getReplayFile() { +// return currentReplayFile; +// } +// +// /** +// * Synchronizes the cursor on the Keyframe Timeline with the Replay Time +// * @param ignoreReplaySpeed If true, it always uses 1.0 as the stretch factor +// */ +// public static void syncTimeCursor(boolean ignoreReplaySpeed) { +// selectKeyframe(null); +// +// int curTime = ReplayMod.replaySender.currentTimeStamp(); +// +// int prevTime, prevRealTime; +// +// Keyframe keyframe = timeKeyframes.last(); +// +// if(keyframe == null) { +// prevTime = 0; +// prevRealTime = 0; +// } else { +// prevTime = (int)keyframe.getValue().value; +// prevRealTime = keyframe.getRealTimestamp(); +// } +// +// double speed = ignoreReplaySpeed ? 1 : ReplayMod.overlay.getSpeedSliderValue(); +// +// int newCursorPos = Math.min(GuiReplayOverlay.KEYFRAME_TIMELINE_LENGTH, (int)(prevRealTime+((curTime-prevTime)/speed))); +// +//// setRealTimelineCursor(newCursorPos); TODO +// } +// +// public static List getCustomImageObjects() { +// return customImageObjects.getObjects(); +// } +// +// public static void setCustomImageObjects(List objects) { +// customImageObjects.setObjects(new ArrayList(objects)); +// } +// +// public static void fireKeyframesModifyEvent() { +// FMLCommonHandler.instance().bus().post(new KeyframesModifyEvent(positionKeyframes, timeKeyframes)); +// positionKeyframes.recalculate(ReplayMod.replaySettings.isLinearMovement()); +// timeKeyframes.recalculate(ReplayMod.replaySettings.isLinearMovement()); +// } +// +// public static Restrictions getRestrictions() { +// return restrictions; +// } } diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java deleted file mode 100755 index a665f960..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java +++ /dev/null @@ -1,318 +0,0 @@ -package eu.crushedpixel.replaymod.replay; - -import com.google.common.collect.Maps; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.chat.ChatMessageHandler.ChatMessageType; -import eu.crushedpixel.replaymod.holders.AdvancedPosition; -import eu.crushedpixel.replaymod.holders.Keyframe; -import eu.crushedpixel.replaymod.holders.SpectatorData; -import eu.crushedpixel.replaymod.holders.TimestampValue; -import eu.crushedpixel.replaymod.interpolation.KeyframeList; -import eu.crushedpixel.replaymod.settings.RenderOptions; -import eu.crushedpixel.replaymod.timer.EnchantmentTimer; -import eu.crushedpixel.replaymod.timer.ReplayTimer; -import eu.crushedpixel.replaymod.utils.CameraPathValidator; -import eu.crushedpixel.replaymod.video.VideoRenderer; -import lombok.Getter; -import net.minecraft.client.Minecraft; -import net.minecraft.client.audio.SoundCategory; -import net.minecraft.client.gui.GuiErrorScreen; -import net.minecraft.client.renderer.OpenGlHelper; -import net.minecraft.client.renderer.chunk.RenderChunk; -import net.minecraft.client.resources.I18n; -import net.minecraft.crash.CrashReport; -import net.minecraft.entity.Entity; -import net.minecraft.util.ReportedException; -import org.lwjgl.opengl.Display; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -public class ReplayProcess { - - private static Minecraft mc = Minecraft.getMinecraft(); - - private static int lastRealReplayTime; - private static long lastRealTime = 0; - - private static boolean linear = false; - - private static int initialTimestamp = 0; - - private static double previousReplaySpeed = 0; - - @Getter - private static VideoRenderer videoRenderer = null; - - private static boolean isVideoRecording = false; - private static boolean requestFinish = false; - private static boolean firstTime = false; - - public static boolean isVideoRecording() { - return isVideoRecording; - } - - //a copy of the initial sound settings, - //which we will modify to prevent game sounds from annoying us while rendering - - @SuppressWarnings("unchecked") //I, too, blame Forge for not re-adding generics to that Map. - private static Map mapSoundLevelsBefore = null; - - private static void resetProcess() { - firstTime = true; - - requestFinish = false; - - lastRealTime = System.currentTimeMillis(); - lastRealReplayTime = 0; - linear = ReplayMod.replaySettings.isLinearMovement(); - - previousReplaySpeed = ReplayMod.replaySender.getReplaySpeed(); - - EnchantmentTimer.resetRecordingTime(); - } - - public static void startReplayProcess(RenderOptions renderOptions, boolean fromStart) { - mc.displayGuiScreen(null); - - ReplayHandler.selectKeyframe(null); - resetProcess(); - - isVideoRecording = renderOptions != null; - - ReplayMod.chatMessageHandler.initialize(); - - try { - CameraPathValidator.validateCameraPath(ReplayHandler.getPositionKeyframes(), ReplayHandler.getTimeKeyframes()); - } catch(CameraPathValidator.InvalidCameraPathException e) { - e.printToChat(); - return; - } - - ReplayHandler.setInPath(true); - ReplayMod.replaySender.setAsyncMode(false); - - //default camera path, no rendering - if (renderOptions == null) { - initialTimestamp = fromStart ? 0 : ReplayHandler.getRealTimelineCursor(); - - //if the cursor is at (or very near) the end, play from the beginning as well - if(initialTimestamp + 50 >= Math.max(ReplayHandler.getTimeKeyframes().last().getRealTimestamp(), - ReplayHandler.getPositionKeyframes().last().getRealTimestamp())) { - initialTimestamp = 0; - } - - lastRealReplayTime = initialTimestamp; - - int ts = ReplayHandler.getTimeKeyframes().getInterpolatedValueForTimestamp(initialTimestamp, true).asInt(); - if (ts < ReplayMod.replaySender.currentTimeStamp()) { - mc.displayGuiScreen(null); - } - ReplayMod.replaySender.sendPacketsTill(ts); - - ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.pathstarted", ChatMessageType.INFORMATION); - mc.timer.timerSpeed = 1; - - //set the sound level map to null - //so a previous map doesn't override the current game settings after a camera path - mapSoundLevelsBefore = null; - } else { - //if FBOs are not enabled/supported, prevent the user from resizing the MC window - if(!OpenGlHelper.isFramebufferEnabled()) { - Display.setResizable(false); - } - - //if rendering, disable all game sounds except for gui sounds - @SuppressWarnings("unchecked") //I, too, blame Forge for not re-adding generics to that Map. - Map orgMap = (Map)mc.gameSettings.mapSoundLevels; - if(orgMap != null) { - mapSoundLevelsBefore = new HashMap(orgMap); - } else { - orgMap = Maps.newEnumMap(SoundCategory.class); - } - - //turn down for what? to mute all sound of course! - //the GUI sounds (button clicks etc) are not muted this way. - for(SoundCategory category : SoundCategory.values()) { - if(category == SoundCategory.MASTER) continue; - orgMap.put(category, 0f); - } - - mc.gameSettings.mapSoundLevels = orgMap; - - initialTimestamp = 0; - boolean success = false; - try { - isVideoRecording = true; - videoRenderer = new VideoRenderer(renderOptions); - success = videoRenderer.renderVideo(); - } catch (IOException e) { - e.printStackTrace(); - - GuiErrorScreen errorScreen = new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"), - I18n.format("replaymod.gui.rendering.error.message")); - mc.displayGuiScreen(errorScreen); - } catch (Throwable t) { - CrashReport crashReport = CrashReport.makeCrashReport(t, "Rendering video"); - throw new ReportedException(crashReport); - } finally { - isVideoRecording = false; - stopReplayProcess(success); - } - } - } - - public static void stopReplayProcess(boolean finished) { - if(!ReplayHandler.isInPath()) return; - - //if canceled, display a different chat message - if(finished) ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.pathfinished", ChatMessageType.INFORMATION); - else { - ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.pathinterrupted", ChatMessageType.INFORMATION); - } - - ReplayHandler.setInPath(false); - ReplayMod.replaySender.setAsyncMode(true); - - ReplayMod.replaySender.stopHurrying(); - - ReplayTimer.get(mc).passive = false; - ReplayMod.replaySender.setReplaySpeed(previousReplaySpeed); - ReplayMod.replaySender.setReplaySpeed(0); - - //re-enable window resizing after rendering - Display.setResizable(true); - - //restore the sound settings - if (mapSoundLevelsBefore != null) { - mc.gameSettings.mapSoundLevels = mapSoundLevelsBefore; - } - } - - //if justCheck is true, no Screenshot will be taken, it will only be checked - //whether all chunks have been rendered. This is necessary because no Render ticks - //are called if the Timer speed is set to 0, leading to this method never being - //called from the RenderWorldLastEvent handlers. - public static void tickReplay(boolean justCheck) { - final long curTime = System.currentTimeMillis(); - KeyframeList positionKeyframes = ReplayHandler.getPositionKeyframes(); - KeyframeList timeKeyframes = ReplayHandler.getTimeKeyframes(); - - int curRealReplayTime; - - if (isVideoRecording) { - return; - } - if(ReplayMod.replaySender.isHurrying()) { - lastRealTime = curTime; - return; - } - - if(firstTime) { - if(RenderChunk.renderChunksUpdated != 0 || mc.currentScreen != null) { - return; - } - - lastRealTime = curTime; - - firstTime = false; - mc.timer.renderPartialTicks = 100; - mc.timer.elapsedPartialTicks = 100; - mc.timer.elapsedTicks = 100; - - curRealReplayTime = lastRealReplayTime = initialTimestamp; - } else { - long timeStep = curTime - lastRealTime; - curRealReplayTime = (int) (lastRealReplayTime + timeStep); - } - - if(justCheck) return; - - Keyframe lastPos = positionKeyframes.getPreviousKeyframe(curRealReplayTime, true); - Keyframe nextPos = positionKeyframes.getNextKeyframe(curRealReplayTime, true); - - boolean spectateCamera = true; - - //check whether between two First Person Spectator Keyframes - if(lastPos != null && nextPos != null) { - if(lastPos.getValue() instanceof SpectatorData && nextPos.getValue() instanceof SpectatorData) { - SpectatorData previousSpectatorData = (SpectatorData)lastPos.getValue(); - SpectatorData nextSpectatorData = (SpectatorData)nextPos.getValue(); - if(previousSpectatorData.getSpectatedEntityID() == nextSpectatorData.getSpectatedEntityID()) { - if(previousSpectatorData.getSpectatingMethod() == SpectatorData.SpectatingMethod.FIRST_PERSON - && nextSpectatorData.getSpectatingMethod() == SpectatorData.SpectatingMethod.FIRST_PERSON) { - spectateCamera = false; - } - } - } - } - - ReplayHandler.setRealTimelineCursor(curRealReplayTime); - - Keyframe lastTime = timeKeyframes.getPreviousKeyframe(curRealReplayTime, true); - Keyframe nextTime = timeKeyframes.getNextKeyframe(curRealReplayTime, true); - - int lastTimeStamp; - int nextTimeStamp; - - double curSpeed = 0; - - if(nextTime != null && lastTime != null && nextTime.getRealTimestamp() == lastTime.getRealTimestamp()) { - curSpeed = 0; - } else { - if(nextTime != null) { - nextTimeStamp = nextTime.getRealTimestamp(); - } else { - nextTimeStamp = lastTime.getRealTimestamp(); - } - - if(lastTime != null) { - lastTimeStamp = lastTime.getRealTimestamp(); - } else { - lastTimeStamp = nextTime.getRealTimestamp(); - } - - if(!(nextTime == null || lastTime == null)) { - curSpeed = ((double) (((int)nextTime.getValue().value - (int)lastTime.getValue().value))) / ((double) ((nextTimeStamp - lastTimeStamp))); - } - - if(lastTimeStamp == nextTimeStamp) { - curSpeed = 0f; - } - } - - AdvancedPosition cameraPosition = null; - if(spectateCamera) { - cameraPosition = positionKeyframes.getInterpolatedValueForTimestamp(curRealReplayTime, linear); - ReplayHandler.spectateCamera(); - } else { - Entity toSpectate = mc.theWorld.getEntityByID(((SpectatorData) lastPos.getValue()).getSpectatedEntityID()); - ReplayHandler.spectateEntity(toSpectate); - } - - if(cameraPosition != null) { - ReplayHandler.setCameraTilt((float)cameraPosition.getRoll()); - ReplayHandler.getCameraEntity().movePath(cameraPosition); - } - - Integer curTimestamp = timeKeyframes.getInterpolatedValueForTimestamp(curRealReplayTime, true).asInt(); - - if(!isVideoRecording()) ReplayMod.replaySender.setReplaySpeed(curSpeed); - - ReplayMod.replaySender.sendPacketsTill(curTimestamp); - - lastRealReplayTime = curRealReplayTime; - lastRealTime = curTime; - - if(requestFinish) { - stopReplayProcess(true); - requestFinish = false; - } - - if(curRealReplayTime > timeKeyframes.last().getRealTimestamp() - && curRealReplayTime > positionKeyframes.last().getRealTimestamp()) { - requestFinish = true; - } - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java b/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java index 1d0b8786..690c3179 100755 --- a/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java +++ b/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java @@ -1,6 +1,5 @@ package eu.crushedpixel.replaymod.settings; -import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.registry.LightingHandler; import net.minecraft.client.resources.I18n; import net.minecraftforge.common.config.Configuration; @@ -10,34 +9,6 @@ public class ReplaySettings { private static final String[] CATEGORIES = new String[]{"recording", "replay", "render", "advanced"}; - public void readValues() { - Configuration config = ReplayMod.config; - - config.load(); - - for(RecordingOptions o : RecordingOptions.values()) { - Property p = getConfigSetting(config, o.name(), o.getValue(), "recording", false); - o.setValue(getValueObject(p)); - } - - for(ReplayOptions o : ReplayOptions.values()) { - Property p = getConfigSetting(config, o.name(), o.getValue(), "replay", false); - o.setValue(getValueObject(p)); - } - - for(RenderOptions o : RenderOptions.values()) { - Property p = getConfigSetting(config, o.name(), o.getValue(), "render", false); - o.setValue(getValueObject(p)); - } - - for(AdvancedOptions o : AdvancedOptions.values()) { - Property p = getConfigSetting(config, o.name(), o.getValue(), "advanced", true); - o.setValue(getValueObject(p)); - } - - config.save(); - } - public String getRecordingPath() { return (String) AdvancedOptions.recordingPath.getValue(); } @@ -54,12 +25,10 @@ public class ReplaySettings { public void setVideoFramerate(int framerate) { RenderOptions.videoFramerate.setValue(Math.min(120, Math.max(10, framerate))); - rewriteSettings(); } public void toggleInterpolation() { ReplayOptions.linear.setValue(!((Boolean)ReplayOptions.linear.getValue())); - rewriteSettings(); } public boolean showRecordingIndicator() { @@ -89,46 +58,18 @@ public class ReplaySettings { public void setLightingEnabled(boolean enabled) { ReplayOptions.lighting.setValue(enabled); LightingHandler.setLighting(enabled); - rewriteSettings(); } public boolean showPathPreview() { return (Boolean) ReplayOptions.previewPath.getValue(); } public void setShowPathPreview(boolean show) { ReplayOptions.previewPath.setValue(show); - rewriteSettings(); } public boolean showClearKeyframesCallback() { return (Boolean) ReplayOptions.keyframeCleanCallback.getValue(); } - public void rewriteSettings() { - ReplayMod.config.load(); - - for(String cat : CATEGORIES) { - ReplayMod.config.removeCategory(ReplayMod.config.getCategory(cat)); - } - - for(RecordingOptions o : RecordingOptions.values()) { - getConfigSetting(ReplayMod.config, o.name(), o.getValue(), "recording", false); - } - - for(ReplayOptions o : ReplayOptions.values()) { - getConfigSetting(ReplayMod.config, o.name(), o.getValue(), "replay", false); - } - - for(RenderOptions o : RenderOptions.values()) { - getConfigSetting(ReplayMod.config, o.name(), o.getValue(), "render", false); - } - - for(AdvancedOptions o : AdvancedOptions.values()) { - getConfigSetting(ReplayMod.config, o.name(), o.getValue(), "advanced", false); - } - - ReplayMod.config.save(); - } - private Property getConfigSetting(Configuration config, String name, Object value, String category, boolean warning) { if(warning) { String warningMsg = "Please be careful when modifying this setting, as setting it to an invalid value might harm your computer."; @@ -174,7 +115,7 @@ public class ReplaySettings { public enum RecordingOptions implements ValueEnum { recordServer(true, "replaymod.gui.settings.recordserver"), recordSingleplayer(true, "replaymod.gui.settings.recordsingleplayer"), - notifications(true, "replaymod.gui.settings.notifications"), + notifications(true, "replaymod.gui.settings.notifications"), // TODO indicator(true, "replaymod.gui.settings.indicator"); private Object value; diff --git a/src/main/java/eu/crushedpixel/replaymod/studio/ConnectMetadataFilter.java b/src/main/java/eu/crushedpixel/replaymod/studio/ConnectMetadataFilter.java index ccb4da1e..ad6094a6 100644 --- a/src/main/java/eu/crushedpixel/replaymod/studio/ConnectMetadataFilter.java +++ b/src/main/java/eu/crushedpixel/replaymod/studio/ConnectMetadataFilter.java @@ -8,7 +8,7 @@ import de.johni0702.replaystudio.filter.StreamFilter; import de.johni0702.replaystudio.replay.ReplayFile; import de.johni0702.replaystudio.replay.ReplayMetaData; import de.johni0702.replaystudio.stream.PacketStream; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import org.apache.commons.io.IOUtils; import org.spacehq.mc.protocol.packet.ingame.server.ServerResourcePackSendPacket; import org.spacehq.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnPlayerPacket; diff --git a/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java b/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java index 790cb5c6..b11589fa 100755 --- a/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java +++ b/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java @@ -90,9 +90,9 @@ public class StudioImplementation { } private static void shiftPaths(ReplayFile replayFile, int beginning, int ending) throws IOException { - Optional in = replayFile.get(eu.crushedpixel.replaymod.utils.ReplayFile.ENTRY_PATHS); + Optional in = replayFile.get("paths.json"); if (!in.isPresent()) { - in = replayFile.get(eu.crushedpixel.replaymod.utils.ReplayFile.ENTRY_PATHS_OLD); + in = replayFile.get("paths"); if (!in.isPresent()) { return; } @@ -126,7 +126,7 @@ public class StudioImplementation { } } - Writer out = new OutputStreamWriter(replayFile.write(eu.crushedpixel.replaymod.utils.ReplayFile.ENTRY_PATHS)); + Writer out = new OutputStreamWriter(replayFile.write("paths.json")); new Gson().toJson(resultSets.toArray(new KeyframeSet[resultSets.size()]), out); out.flush(); out.close(); diff --git a/src/main/java/eu/crushedpixel/replaymod/timer/EnchantmentTimer.java b/src/main/java/eu/crushedpixel/replaymod/timer/EnchantmentTimer.java index 27857e62..71d0a4b8 100755 --- a/src/main/java/eu/crushedpixel/replaymod/timer/EnchantmentTimer.java +++ b/src/main/java/eu/crushedpixel/replaymod/timer/EnchantmentTimer.java @@ -1,9 +1,5 @@ package eu.crushedpixel.replaymod.timer; -import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.replay.ReplayProcess; - public class EnchantmentTimer { private static long lastRealTime = System.currentTimeMillis(); @@ -20,17 +16,18 @@ public class EnchantmentTimer { } public static long getEnchantmentTime() { - if(!(ReplayHandler.isInPath() && ReplayProcess.isVideoRecording())) { - if(ReplayHandler.isInReplay()) { - long timeDiff = System.currentTimeMillis() - lastRealTime; - double toAdd = timeDiff * ReplayMod.replaySender.getReplaySpeed(); - lastFakeTime = Math.round(lastFakeTime + toAdd); - lastRealTime = System.currentTimeMillis(); - return lastFakeTime; - } - lastFakeTime = lastRealTime = System.currentTimeMillis(); - return lastRealTime; - } + // TODO +// if(!(ReplayHandler.isInPath() && ReplayProcess.isVideoRecording())) { +// if(ReplayHandler.isInReplay()) { +// long timeDiff = System.currentTimeMillis() - lastRealTime; +// double toAdd = timeDiff * ReplayMod.replaySender.getReplaySpeed(); +// lastFakeTime = Math.round(lastFakeTime + toAdd); +// lastRealTime = System.currentTimeMillis(); +// return lastFakeTime; +// } +// lastFakeTime = lastRealTime = System.currentTimeMillis(); +// return lastRealTime; +// } return recordingTime; } } diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/CameraPathValidator.java b/src/main/java/eu/crushedpixel/replaymod/utils/CameraPathValidator.java index a2c22a84..d15e7dca 100644 --- a/src/main/java/eu/crushedpixel/replaymod/utils/CameraPathValidator.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/CameraPathValidator.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.utils; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.chat.ChatMessageHandler; import eu.crushedpixel.replaymod.holders.AdvancedPosition; import eu.crushedpixel.replaymod.holders.Keyframe; diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java deleted file mode 100644 index 1ed76094..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java +++ /dev/null @@ -1,269 +0,0 @@ -package eu.crushedpixel.replaymod.utils; - -import com.google.common.base.Supplier; -import com.google.common.reflect.TypeToken; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import eu.crushedpixel.replaymod.assets.AssetRepository; -import eu.crushedpixel.replaymod.holders.Keyframe; -import eu.crushedpixel.replaymod.holders.KeyframeSet; -import eu.crushedpixel.replaymod.holders.Marker; -import eu.crushedpixel.replaymod.holders.PlayerVisibility; -import eu.crushedpixel.replaymod.recording.ReplayMetaData; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.*; -import java.lang.reflect.Type; -import java.util.*; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class ReplayFile extends ZipFile { - - public static final String TEMP_FILE_EXTENSION = ".tmcpr"; - public static final String JSON_FILE_EXTENSION = ".json"; - public static final String ZIP_FILE_EXTENSION = ".mcpr"; - public static final String ENTRY_RECORDING = "recording" + TEMP_FILE_EXTENSION; - public static final String ENTRY_METADATA = "metaData" + JSON_FILE_EXTENSION; - public static final String ENTRY_PATHS_OLD = "paths"; - public static final String ENTRY_PATHS = "paths" + JSON_FILE_EXTENSION; - public static final String ENTRY_THUMB = "thumb"; - public static final String ENTRY_RESOURCE_PACK = "resourcepack/%s.zip"; - public static final String ENTRY_RESOURCE_PACK_INDEX = "resourcepack/index"+JSON_FILE_EXTENSION; - 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; - - public ReplayFile(File f) throws IOException { - super(f); - this.file = f; - } - - public File getFile() { - return file; - } - - public ZipEntry recordingEntry() { - return getEntry(ENTRY_RECORDING); - } - - public Supplier recording() { - return new Supplier() { - @Override - public InputStream get() { - try { - return getInputStream(recordingEntry()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } - - public ZipEntry metadataEntry() { - return getEntry(ENTRY_METADATA); - } - - public Supplier metadata() { - return new Supplier() { - @Override - public ReplayMetaData get() { - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(metadataEntry()))); - return new Gson().fromJson(reader, ReplayMetaData.class); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } - - public ZipEntry pathsEntry() { - ZipEntry newEntry = getEntry(ENTRY_PATHS); - return newEntry != null ? newEntry : getEntry(ENTRY_PATHS_OLD); - } - - public Supplier paths() { - return new Supplier() { - @Override - public KeyframeSet[] get() { - try { - ZipEntry entry = pathsEntry(); - if (entry == null) { - return null; - } - BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(entry))); - - GsonBuilder gsonBuilder = new GsonBuilder().registerTypeAdapter(KeyframeSet[].class, new LegacyKeyframeSetAdapter()); - Gson gson = gsonBuilder.create(); - - return gson.fromJson(reader, KeyframeSet[].class); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } - - public ZipEntry visibilityEntry() { - ZipEntry newEntry = getEntry(ENTRY_VISIBILITY); - return newEntry != null ? newEntry : getEntry(ENTRY_VISIBILITY_OLD); - } - - public Supplier visibility() { - return new Supplier() { - @Override - public PlayerVisibility get() { - try { - ZipEntry entry = visibilityEntry(); - if (entry == null) { - return null; - } - BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(entry))); - return new Gson().fromJson(reader, PlayerVisibility.class); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } - - public ZipEntry markersEntry() { - return getEntry(ENTRY_MARKERS); - } - - public Supplier>> markers() { - return new Supplier>>() { - @Override - public List> get() { - try { - ZipEntry entry = markersEntry(); - if (entry == null) { - return null; - } - BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(entry))); - - Type keyframeType = new TypeToken>>(){}.getType(); - - return new Gson().fromJson(reader, keyframeType); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } - - public ZipEntry thumbEntry() { - return getEntry(ENTRY_THUMB); - } - - public Supplier thumb() { - return new Supplier() { - @Override - public BufferedImage get() { - try { - ZipEntry entry = thumbEntry(); - if (entry == null) { - return null; - } - InputStream is = getInputStream(entry); - int i = 7; - while (i > 0) { - i -= is.skip(i); // Security through obscurity \o/ - } - return ImageIO.read(is); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } - - public ZipEntry resourcePackIndexEntry() { - return getEntry(ENTRY_RESOURCE_PACK_INDEX); - } - - public Supplier> resourcePackIndex() { - return new Supplier>() { - @Override - @SuppressWarnings("unchecked") - public Map get() { - try { - ZipEntry entry = resourcePackIndexEntry(); - if (entry == null) { - return null; - } - Map index = new HashMap(); - JsonObject json = new Gson().fromJson(new InputStreamReader(getInputStream(entry)), JsonObject.class); - for (Map.Entry e : json.entrySet()) { - index.put(Integer.parseInt(e.getKey()), e.getValue().getAsString()); - } - return index; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } - - public ZipEntry resourcePackEntry(String hash) { - return getEntry(String.format(ENTRY_RESOURCE_PACK, hash)); - } - - public Supplier resourcePack(final String hash) { - return new Supplier() { - @Override - public InputStream get() { - try { - ZipEntry entry = resourcePackEntry(hash); - if (entry == null) { - return null; - } - return getInputStream(entry); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } - - public Supplier assetRepository() { - return new Supplier() { - @Override - @SuppressWarnings("unchecked") - public AssetRepository get() { - AssetRepository assetRepository = new AssetRepository(); - - Enumeration entries = entries(); - - while(entries.hasMoreElements()) { - - try { - ZipEntry entry = entries.nextElement(); - - String entryName = entry.getName(); - - if(entryName.startsWith(ENTRY_ASSET_FOLDER)) { - String name = entry.getName().substring(ENTRY_ASSET_FOLDER.length()); - - String[] split = name.split("_"); - UUID uuid = UUID.fromString(split[0]); - - assetRepository.addAsset(name, getInputStream(entry), uuid); - } - } catch(Exception e) { - e.printStackTrace(); - } - } - - return assetRepository; - } - }; - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java index 04431c5f..12109d61 100755 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java @@ -1,12 +1,12 @@ package eu.crushedpixel.replaymod.utils; import com.google.gson.Gson; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.recording.packet.PacketSerializer; +import de.johni0702.replaystudio.replay.ReplayMetaData; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.assets.CustomObjectRepository; -import eu.crushedpixel.replaymod.holders.*; -import eu.crushedpixel.replaymod.interpolation.KeyframeList; -import eu.crushedpixel.replaymod.recording.PacketSerializer; -import eu.crushedpixel.replaymod.recording.ReplayMetaData; +import eu.crushedpixel.replaymod.holders.KeyframeSet; +import eu.crushedpixel.replaymod.holders.PacketData; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import net.minecraft.network.EnumConnectionState; @@ -15,10 +15,12 @@ import net.minecraft.network.Packet; import net.minecraft.network.PacketBuffer; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; import java.io.*; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; @@ -72,57 +74,6 @@ public class ReplayFileIO { return files; } - public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData, Set> markers, - Map resourcePacks, Map resourcePackRequests) throws IOException { - byte[] buffer = new byte[1024]; - - FileOutputStream fos = new FileOutputStream(replayFile); - ZipOutputStream zos = new ZipOutputStream(fos); - - String json = new Gson().toJson(metaData); - - zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_METADATA)); - PrintWriter pw = new PrintWriter(zos); - pw.write(json); - pw.flush(); - zos.closeEntry(); - - zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_RECORDING)); - FileInputStream fis = new FileInputStream(tempFile); - int len; - while((len = fis.read(buffer)) > 0) { - zos.write(buffer, 0, len); - } - - fis.close(); - zos.closeEntry(); - - if(!markers.isEmpty()) { - zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_MARKERS)); - - pw = new PrintWriter(zos); - pw.write(new Gson().toJson(markers.toArray(new Keyframe[markers.size()]))); - pw.flush(); - zos.closeEntry(); - } - - if(!resourcePackRequests.isEmpty()) { - zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_RESOURCE_PACK_INDEX)); - pw = new PrintWriter(zos); - pw.write(new Gson().toJson(resourcePackRequests)); - pw.flush(); - zos.closeEntry(); - - for(Map.Entry entry : resourcePacks.entrySet()) { - zos.putNextEntry(new ZipEntry(String.format(ReplayFile.ENTRY_RESOURCE_PACK, entry.getKey()))); - IOUtils.copy(new FileInputStream(entry.getValue()), zos); - zos.closeEntry(); - } - } - - zos.close(); - } - public static PacketData readPacketData(DataInputStream dis) throws IOException { int timestamp = dis.readInt(); int bytes = dis.readInt(); @@ -160,12 +111,6 @@ public class ReplayFileIO { return array; } - public static void writePacket(PacketData pd, DataOutput out) throws IOException { - out.writeInt(pd.getTimestamp()); - out.writeInt(pd.getByteArray().length); - out.write(pd.getByteArray()); - } - private static final Gson gson = new Gson(); private static void write(Object obj, File file) throws IOException { @@ -177,18 +122,10 @@ public class ReplayFileIO { write((Object) keyframeRegistry, file); } - public static void write(PlayerVisibility visibility, File file) throws IOException { - write((Object) visibility, file); - } - public static void write(ReplayMetaData metaData, File file) throws IOException { write((Object) metaData, file); } - public static void write(KeyframeList markers, File file) throws IOException { - write((Object) markers, file); - } - public static void write(CustomObjectRepository repository, File file) throws IOException { write((Object) repository, file); } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/ReplayScreenshot.java b/src/main/java/eu/crushedpixel/replaymod/video/ReplayScreenshot.java index 7ada6d92..fc1c95d0 100755 --- a/src/main/java/eu/crushedpixel/replaymod/video/ReplayScreenshot.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/ReplayScreenshot.java @@ -1,17 +1,14 @@ package eu.crushedpixel.replaymod.video; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; import eu.crushedpixel.replaymod.chat.ChatMessageHandler.ChatMessageType; import eu.crushedpixel.replaymod.events.handlers.TickAndRenderListener; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.ImageUtils; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; -import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.File; public class ReplayScreenshot { @@ -67,20 +64,13 @@ public class ReplayScreenshot { final BufferedImage nbi = ImageUtils.cropImage(fbi, rect); - File replayFile = ReplayHandler.getReplayFile(); - - File tempImage = File.createTempFile("thumb", null); - int h = 720; int w = 1280; BufferedImage img = ImageUtils.scaleImage(nbi, new Dimension(w, h)); - ImageIO.write(img, "jpg", tempImage); - - ReplayMod.replayFileAppender.registerModifiedFile(tempImage, "thumb", replayFile); - - tempImage.deleteOnExit(); + // TODO +// ReplayHandler.getReplayFile().writeThumb(img); ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.savedthumb", ChatMessageType.INFORMATION); } catch(Exception e) { diff --git a/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java index f5ae743b..132e2f6b 100644 --- a/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java @@ -1,17 +1,13 @@ package eu.crushedpixel.replaymod.video; -import eu.crushedpixel.replaymod.ReplayMod; +import com.replaymod.core.ReplayMod; +import com.replaymod.replay.ReplaySender; import eu.crushedpixel.replaymod.gui.GuiVideoRenderer; import eu.crushedpixel.replaymod.holders.AdvancedPosition; -import eu.crushedpixel.replaymod.holders.Keyframe; -import eu.crushedpixel.replaymod.holders.SpectatorData; import eu.crushedpixel.replaymod.holders.TimestampValue; import eu.crushedpixel.replaymod.interpolation.KeyframeList; import eu.crushedpixel.replaymod.renderer.ChunkLoadingRenderGlobal; -import eu.crushedpixel.replaymod.replay.ReplayHandler; -import eu.crushedpixel.replaymod.replay.ReplaySender; import eu.crushedpixel.replaymod.settings.RenderOptions; -import eu.crushedpixel.replaymod.timer.EnchantmentTimer; import eu.crushedpixel.replaymod.timer.ReplayTimer; import eu.crushedpixel.replaymod.video.capturer.RenderInfo; import eu.crushedpixel.replaymod.video.frame.RGBFrame; @@ -148,8 +144,9 @@ public class VideoRenderer implements RenderInfo { fps = options.getFps(); - positionKeyframes = ReplayHandler.getPositionKeyframes(); - timeKeyframes = ReplayHandler.getTimeKeyframes(); + // TODO +// positionKeyframes = ReplayHandler.getPositionKeyframes(); +// timeKeyframes = ReplayHandler.getTimeKeyframes(); int duration = Math.max(timeKeyframes.last().getRealTimestamp(), positionKeyframes.last().getRealTimestamp()); @@ -179,128 +176,130 @@ public class VideoRenderer implements RenderInfo { } private void updateCam() { - KeyframeList positionKeyframes = ReplayHandler.getPositionKeyframes(); - - if (mc.thePlayer == null) { - return; // Not ready yet - } - int videoTime = framesDone * 1000 / fps; - int posCount = ReplayHandler.getPositionKeyframes().size(); - - AdvancedPosition pos = new AdvancedPosition(); - Keyframe lastPos = positionKeyframes.getPreviousKeyframe(videoTime, true); - Keyframe nextPos = null; - - // Position interpolation - nextPos = positionKeyframes.getNextKeyframe(videoTime, true); - - int lastPosStamp = lastPos.getRealTimestamp(); - int nextPosStamp = (nextPos == null ? lastPos : nextPos).getRealTimestamp(); - - int diffLength = nextPosStamp - lastPosStamp; - float diffPct = (float) (videoTime - lastPosStamp) / diffLength; - if(Float.isInfinite(diffPct) || Float.isNaN(diffPct)) diffPct = 0; - - float totalPct = (positionKeyframes.indexOf(lastPos) + diffPct) / (posCount-1); - pos = positionKeyframes.getInterpolatedValueForPathPosition(Math.max(0, Math.min(1, totalPct)), ReplayMod.replaySettings.isLinearMovement()); - - boolean spectateCamera = true; - - //check whether between two First Person Spectator Keyframes - if(lastPos != null && nextPos != null) { - if(lastPos.getValue() instanceof SpectatorData && nextPos.getValue() instanceof SpectatorData) { - SpectatorData previousSpectatorData = (SpectatorData)lastPos.getValue(); - SpectatorData nextSpectatorData = (SpectatorData)nextPos.getValue(); - if(previousSpectatorData.getSpectatedEntityID() == nextSpectatorData.getSpectatedEntityID()) { - if(previousSpectatorData.getSpectatingMethod() == SpectatorData.SpectatingMethod.FIRST_PERSON - && nextSpectatorData.getSpectatingMethod() == SpectatorData.SpectatingMethod.FIRST_PERSON) { - spectateCamera = false; - } - } - } - } - - if(spectateCamera) { - // Make sure we're spectating the camera entity - // We do not use .spectateCamera() as that method sets the position of the camera to the previous - // entity, overriding our calculations - ReplayHandler.spectateEntity(ReplayHandler.getCameraEntity()); - - if(pos != null) { - ReplayHandler.setCameraTilt((float)pos.getRoll()); - ReplayHandler.getCameraEntity().movePath(pos); - mc.entityRenderer.fovModifierHand = mc.entityRenderer.fovModifierHandPrev = 1; - } - } else { - ReplayHandler.spectateEntity(mc.theWorld.getEntityByID(((SpectatorData)lastPos.getValue()).getSpectatedEntityID())); - } + //TODO +// KeyframeList positionKeyframes = ReplayHandler.getPositionKeyframes(); +// +// if (mc.thePlayer == null) { +// return; // Not ready yet +// } +// int videoTime = framesDone * 1000 / fps; +// int posCount = ReplayHandler.getPositionKeyframes().size(); +// +// AdvancedPosition pos = new AdvancedPosition(); +// Keyframe lastPos = positionKeyframes.getPreviousKeyframe(videoTime, true); +// Keyframe nextPos = null; +// +// // Position interpolation +// nextPos = positionKeyframes.getNextKeyframe(videoTime, true); +// +// int lastPosStamp = lastPos.getRealTimestamp(); +// int nextPosStamp = (nextPos == null ? lastPos : nextPos).getRealTimestamp(); +// +// int diffLength = nextPosStamp - lastPosStamp; +// float diffPct = (float) (videoTime - lastPosStamp) / diffLength; +// if(Float.isInfinite(diffPct) || Float.isNaN(diffPct)) diffPct = 0; +// +// float totalPct = (positionKeyframes.indexOf(lastPos) + diffPct) / (posCount-1); +// pos = positionKeyframes.getInterpolatedValueForPathPosition(Math.max(0, Math.min(1, totalPct)), ReplayMod.replaySettings.isLinearMovement()); +// +// boolean spectateCamera = true; +// +// //check whether between two First Person Spectator Keyframes +// if(lastPos != null && nextPos != null) { +// if(lastPos.getValue() instanceof SpectatorData && nextPos.getValue() instanceof SpectatorData) { +// SpectatorData previousSpectatorData = (SpectatorData)lastPos.getValue(); +// SpectatorData nextSpectatorData = (SpectatorData)nextPos.getValue(); +// if(previousSpectatorData.getSpectatedEntityID() == nextSpectatorData.getSpectatedEntityID()) { +// if(previousSpectatorData.getSpectatingMethod() == SpectatorData.SpectatingMethod.FIRST_PERSON +// && nextSpectatorData.getSpectatingMethod() == SpectatorData.SpectatingMethod.FIRST_PERSON) { +// spectateCamera = false; +// } +// } +// } +// } +// +// if(spectateCamera) { +// // Make sure we're spectating the camera entity +// // We do not use .spectateCamera() as that method sets the position of the camera to the previous +// // entity, overriding our calculations +// ReplayHandler.spectateEntity(ReplayHandler.getCameraEntity()); +// +// if(pos != null) { +// ReplayHandler.setCameraTilt((float)pos.getRoll()); +// ReplayHandler.getCameraEntity().movePath(pos); +// mc.entityRenderer.fovModifierHand = mc.entityRenderer.fovModifierHandPrev = 1; +// } +// } else { +// ReplayHandler.spectateEntity(mc.theWorld.getEntityByID(((SpectatorData)lastPos.getValue()).getSpectatedEntityID())); +// } } private void updateTime(Timer timer, int framesDone) { - KeyframeList timeKeyframes = ReplayHandler.getTimeKeyframes(); - - int videoTime = framesDone * 1000 / fps; - int timeCount = timeKeyframes.size(); - - // WARNING: The rest of this method contains some magic for which Marius is responsible - // Time interpolation - Keyframe lastTime = timeKeyframes.getPreviousKeyframe(videoTime, true); - Keyframe nextTime = timeKeyframes.getNextKeyframe(videoTime, true); - - int lastTimeStamp = 0; - int nextTimeStamp = 0; - - double curSpeed = 0; - - if(nextTime != null || lastTime != null) { - if(nextTime != null && lastTime != null && nextTime.getRealTimestamp() == lastTime.getRealTimestamp()) { - curSpeed = 0; - } else { - if(nextTime != null) { - nextTimeStamp = nextTime.getRealTimestamp(); - } else { - nextTimeStamp = lastTime.getRealTimestamp(); - } - - if(lastTime != null) { - lastTimeStamp = lastTime.getRealTimestamp(); - } else { - lastTimeStamp = nextTime.getRealTimestamp(); - } - - if(!(nextTime == null || lastTime == null)) { - curSpeed = ((double)(((int)nextTime.getValue().value-(int)lastTime.getValue().value)))/((double)((nextTimeStamp-lastTimeStamp))); - } - - if(lastTimeStamp == nextTimeStamp) { - curSpeed = 0f; - } - } - } - - int currentTimeDiff = nextTimeStamp - lastTimeStamp; - int currentTime = videoTime - lastTimeStamp; - - float currentTimeStepPerc = (float)currentTime/(float)currentTimeDiff; //The percentage of the travelled path between the current timestamps - if(Float.isInfinite(currentTimeStepPerc) || Float.isNaN(currentTimeStepPerc)) currentTimeStepPerc = 0; - - float timePos = (timeKeyframes.indexOf(lastTime) + currentTimeStepPerc) / (timeCount-1f); - - Integer replayTime = timeKeyframes.getInterpolatedValueForPathPosition(Math.max(0, Math.min(1, timePos)), true).asInt(); - - replaySender.sendPacketsTill(replayTime); - - if (curSpeed >= 0) { - replaySender.setReplaySpeed(curSpeed); - } - - // Update Timer - EnchantmentTimer.increaseRecordingTime(1000 / fps); - - timer.elapsedPartialTicks += timer.timerSpeed * 20 / fps; - timer.elapsedTicks = (int) timer.elapsedPartialTicks; - timer.elapsedPartialTicks -= timer.elapsedTicks; - timer.renderPartialTicks = timer.elapsedPartialTicks; + // TODO +// KeyframeList timeKeyframes = ReplayHandler.getTimeKeyframes(); +// +// int videoTime = framesDone * 1000 / fps; +// int timeCount = timeKeyframes.size(); +// +// // WARNING: The rest of this method contains some magic for which Marius is responsible +// // Time interpolation +// Keyframe lastTime = timeKeyframes.getPreviousKeyframe(videoTime, true); +// Keyframe nextTime = timeKeyframes.getNextKeyframe(videoTime, true); +// +// int lastTimeStamp = 0; +// int nextTimeStamp = 0; +// +// double curSpeed = 0; +// +// if(nextTime != null || lastTime != null) { +// if(nextTime != null && lastTime != null && nextTime.getRealTimestamp() == lastTime.getRealTimestamp()) { +// curSpeed = 0; +// } else { +// if(nextTime != null) { +// nextTimeStamp = nextTime.getRealTimestamp(); +// } else { +// nextTimeStamp = lastTime.getRealTimestamp(); +// } +// +// if(lastTime != null) { +// lastTimeStamp = lastTime.getRealTimestamp(); +// } else { +// lastTimeStamp = nextTime.getRealTimestamp(); +// } +// +// if(!(nextTime == null || lastTime == null)) { +// curSpeed = ((double)(((int)nextTime.getValue().value-(int)lastTime.getValue().value)))/((double)((nextTimeStamp-lastTimeStamp))); +// } +// +// if(lastTimeStamp == nextTimeStamp) { +// curSpeed = 0f; +// } +// } +// } +// +// int currentTimeDiff = nextTimeStamp - lastTimeStamp; +// int currentTime = videoTime - lastTimeStamp; +// +// float currentTimeStepPerc = (float)currentTime/(float)currentTimeDiff; //The percentage of the travelled path between the current timestamps +// if(Float.isInfinite(currentTimeStepPerc) || Float.isNaN(currentTimeStepPerc)) currentTimeStepPerc = 0; +// +// float timePos = (timeKeyframes.indexOf(lastTime) + currentTimeStepPerc) / (timeCount-1f); +// +// Integer replayTime = timeKeyframes.getInterpolatedValueForPathPosition(Math.max(0, Math.min(1, timePos)), true).asInt(); +// +// replaySender.sendPacketsTill(replayTime); +// +// if (curSpeed >= 0) { +// replaySender.setReplaySpeed(curSpeed); +// } +// +// // Update Timer +// EnchantmentTimer.increaseRecordingTime(1000 / fps); +// +// timer.elapsedPartialTicks += timer.timerSpeed * 20 / fps; +// timer.elapsedTicks = (int) timer.elapsedPartialTicks; +// timer.elapsedPartialTicks -= timer.elapsedTicks; +// timer.renderPartialTicks = timer.elapsedPartialTicks; } private void tick() { diff --git a/src/main/resources/META-INF/replaymod_at.cfg b/src/main/resources/META-INF/replaymod_at.cfg index c7a9a905..a728ce6d 100644 --- a/src/main/resources/META-INF/replaymod_at.cfg +++ b/src/main/resources/META-INF/replaymod_at.cfg @@ -101,5 +101,8 @@ public net.minecraft.client.renderer.culling.Frustum field_78552_a # clippingHel public net.minecraft.crash.CrashReportCategory field_85077_c # children public net.minecraft.crash.CrashReportCategory$Entry +# KeyBinding +public net.minecraft.client.settings.KeyBinding field_151474_i # pressTime + # Example # public net.minecraft.package.ClassName func_some_id(Ljava/lang/Class;IZS)V # methodName diff --git a/src/main/resources/assets/guiapi/gui.png b/src/main/resources/assets/guiapi/gui.png new file mode 100644 index 0000000000000000000000000000000000000000..20df55ca5765ada5fd5226f84ae2f22177e450df GIT binary patch literal 4056 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index 0bc2a067..b26e5dc3 100755 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -1,15 +1,69 @@ [ -{ - "modid": "replaymod", - "name": "Replay Mod", - "description": "A Mod which allows you to record, replay and share your Minecraft experience.", - "version": "${version}", - "mcversion": "${mcversion}", - "url": "https://replaymod.com", - "updateUrl": "https://replaymod.com/download", - "authorList": ["CrushedPixel", "johni0702"], - "logoFile": "replaymod_logo.png", - "screenshots": [], - "dependencies": [] -} + { + "modid": "replaymod", + "name": "Replay Mod", + "description": "A Mod which allows you to record, replay and share your Minecraft experience.", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://replaymod.com", + "updateUrl": "https://replaymod.com/download", + "authorList": [ + "CrushedPixel", + "johni0702" + ], + "logoFile": "replaymod_logo.png", + "screenshots": [], + "dependencies": [] + }, + { + "modid": "replaymod-recording", + "name": "Replay Mod - Recording", + "description": "Recording Module of the ReplayMod", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://replaymod.com", + "updateUrl": "https://replaymod.com/download", + "authorList": [ + "CrushedPixel", + "johni0702" + ], + "logoFile": "replaymod_logo.png", + "parent": "replaymod", + "screenshots": [], + "dependencies": [] + }, + { + "modid": "replaymod-replay", + "name": "Replay Mod - Replay", + "description": "Replay Module of the ReplayMod", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://replaymod.com", + "updateUrl": "https://replaymod.com/download", + "authorList": [ + "CrushedPixel", + "johni0702" + ], + "logoFile": "replaymod_logo.png", + "parent": "replaymod", + "screenshots": [], + "dependencies": [] + }, + { + "modid": "replaymod-extras", + "name": "Replay Mod - Extras", + "description": "Extras Module of the ReplayMod - Small but neat additions", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://replaymod.com", + "updateUrl": "https://replaymod.com/download", + "authorList": [ + "CrushedPixel", + "johni0702" + ], + "logoFile": "replaymod_logo.png", + "parent": "replaymod", + "screenshots": [], + "dependencies": [] + } ] diff --git a/src/main/resources/mixins.recording.replaymod.json b/src/main/resources/mixins.recording.replaymod.json new file mode 100644 index 00000000..272114ff --- /dev/null +++ b/src/main/resources/mixins.recording.replaymod.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "com.replaymod.recording.mixin", + "mixins": [], + "server": [], + "client": [ + "MixinNetHandlerPlayClient", + "MixinRenderGlobal" + ], + "refmap": "mixins.replaymod.refmap.json" +} \ No newline at end of file diff --git a/src/main/resources/mixins.replaymod.json b/src/main/resources/mixins.replaymod.json index d514ad09..9a3b623e 100644 --- a/src/main/resources/mixins.replaymod.json +++ b/src/main/resources/mixins.replaymod.json @@ -6,12 +6,10 @@ "MixinEntityRenderer", "MixinGuiSpectator", "MixinMinecraft", - "MixinNetHandlerPlayClient", "MixinPlayerControllerMP", "MixinRender", "MixinRenderArrow", "MixinRendererLivingEntity", - "MixinRenderGlobal", "MixinRenderItem", "MixinRenderManager", "MixinTileEntityEndPortalRenderer",