From cbd96d57f4c9a36e0884c0a0cc856cfd14e7ee35 Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Sat, 3 Jan 2015 23:25:21 +0100 Subject: [PATCH] First Commit --- .gitignore | 9 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 + gradlew.bat | 90 + .../eu/crushedpixel/replaymod/ReplayMod.java | 76 + .../replaymod/chat/ChatMessageRequests.java | 95 + .../replaymod/entities/CameraEntity.java | 208 + .../replaymod/events/GuiEventHandler.java | 48 + .../replaymod/events/GuiReplayOverlay.java | 810 +++ .../replaymod/events/MinecraftTicker.java | 442 ++ .../replaymod/events/RecordingHandler.java | 87 + .../replaymod/events/ReplayTickHandler.java | 41 + .../replaymod/gui/GuiCustomMainMenu.java | 139 + .../replaymod/gui/GuiCustomOptions.java | 63 + .../replaymod/gui/GuiExitReplay.java | 65 + .../replaymod/gui/GuiMouseInput.java | 10 + .../replaymod/gui/GuiRenameReplay.java | 102 + .../replaymod/gui/GuiReplayListEntry.java | 92 + .../replaymod/gui/GuiReplayListExtended.java | 203 + .../replaymod/gui/GuiReplayManager.java | 286 + .../replaymod/gui/GuiReplaySettings.java | 133 + .../replaymod/gui/GuiReplaySpeedSlider.java | 189 + .../gui/GuiSizeLimitOptionSlider.java | 185 + .../replaymod/holders/Keyframe.java | 20 + .../replaymod/holders/KeyframeComparator.java | 16 + .../replaymod/holders/Position.java | 57 + .../replaymod/holders/PositionKeyframe.java | 20 + .../replaymod/holders/TimeInfo.java | 100 + .../replaymod/holders/TimeKeyframe.java | 19 + .../replaymod/interpolation/BasicSpline.java | 71 + .../replaymod/interpolation/Cubic.java | 16 + .../interpolation/LinearInterpolation.java | 42 + .../replaymod/interpolation/LinearPoint.java | 29 + .../interpolation/LinearTimestamp.java | 22 + .../replaymod/interpolation/SplinePoint.java | 92 + .../recording/ConnectionEventHandler.java | 158 + .../replaymod/recording/DataListener.java | 184 + .../replaymod/recording/PacketListener.java | 104 + .../replaymod/recording/PacketSerializer.java | 66 + .../replaymod/recording/ReplayMetaData.java | 45 + .../replaymod/reflection/MCPEnvironment.java | 24 + .../replaymod/reflection/MCPNames.java | 269 + .../replaymod/replay/LesserDataWatcher.java | 105 + .../replaymod/replay/OpenEmbeddedChannel.java | 34 + .../replaymod/replay/PacketDeserializer.java | 110 + .../replaymod/replay/PacketInfo.java | 40 + .../replaymod/replay/ReplayHandler.java | 361 + .../replaymod/replay/ReplayProcess.java | 211 + .../replaymod/replay/ReplaySender.java | 528 ++ .../replaymod/settings/ReplaySettings.java | 101 + .../replaymod/unused/DataReciever.java | 30 + .../replaymod/unused/DataWriter.java | 88 + .../replaymod/unused/PacketListener.java | 37 + .../replaymod/unused/PacketReciever.java | 37 + .../replaymod/unused/PacketWriter.java | 101 + .../assets/replaymod/extended_gui.png | Bin 0 -> 1972 bytes .../resources/assets/replaymod/replay_gui.png | Bin 0 -> 2839 bytes src/main/resources/assets/replaymod/steve.png | Bin 0 -> 1619 bytes .../assets/replaymod/timeline_icons.png | Bin 0 -> 1519 bytes src/main/resources/fields.csv | 5870 ++++++++++++++++ src/main/resources/mcmod.info | 16 + src/main/resources/methods.csv | 6038 +++++++++++++++++ src/main/resources/params.csv | 5124 ++++++++++++++ 64 files changed, 23728 insertions(+) create mode 100644 .gitignore create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 src/main/java/eu/crushedpixel/replaymod/ReplayMod.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/chat/ChatMessageRequests.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/events/GuiReplayOverlay.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/events/MinecraftTicker.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/events/RecordingHandler.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/events/ReplayTickHandler.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiCustomMainMenu.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiCustomOptions.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiExitReplay.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiMouseInput.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiRenameReplay.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayListEntry.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayListExtended.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayManager.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySpeedSlider.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/gui/GuiSizeLimitOptionSlider.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/Keyframe.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/KeyframeComparator.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/Position.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/PositionKeyframe.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/TimeInfo.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/TimeKeyframe.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/interpolation/BasicSpline.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/interpolation/Cubic.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/interpolation/LinearInterpolation.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/interpolation/LinearPoint.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/interpolation/LinearTimestamp.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/interpolation/SplinePoint.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/recording/PacketSerializer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/recording/ReplayMetaData.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/reflection/MCPEnvironment.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/reflection/MCPNames.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/replay/LesserDataWatcher.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/replay/OpenEmbeddedChannel.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/replay/PacketDeserializer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/replay/PacketInfo.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/unused/DataReciever.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/unused/DataWriter.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/unused/PacketListener.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/unused/PacketReciever.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/unused/PacketWriter.java create mode 100644 src/main/resources/assets/replaymod/extended_gui.png create mode 100644 src/main/resources/assets/replaymod/replay_gui.png create mode 100644 src/main/resources/assets/replaymod/steve.png create mode 100644 src/main/resources/assets/replaymod/timeline_icons.png create mode 100644 src/main/resources/fields.csv create mode 100644 src/main/resources/mcmod.info create mode 100644 src/main/resources/methods.csv create mode 100644 src/main/resources/params.csv diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f9d6c628 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/* +/*/ +!/src/ +!.gitignore +!gradlew* +!/gradle/ +!/LICENSE.txt +!/README.md +!/COPYING.lesser \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b7612167031001b7b84baf2a959e8ea8ad03c011 GIT binary patch literal 51017 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

N(DARnfPMFG|;(rIGZH1uB1rN;y7UlJ8 znb!RwO#5WS_%cPPlwSw94PTKP64}}J!~E1}ch9``2jvUrB*#@up8L&*`tqgq ztTAS@^33w-5hFZAH&{j)?l0<)9X(d`N|yLk!E=iT%PD-FTJ!|hqyj%RFFP`SaL;|_ z$(+VYhKm)K@Oe~+eZK(6ElQNdfimBrdKjeVl57_7p^|)jrzt|wyRonj$j9Bji!


f)EE4q$t9yObmyG04W0z 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 t|KH7qPwVTc?c*oWQQiM74F8)Y \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java new file mode 100644 index 00000000..8878fba2 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java @@ -0,0 +1,76 @@ +package eu.crushedpixel.replaymod; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.common.config.Property; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.Mod.Instance; +import net.minecraftforge.fml.common.SidedProxy; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import eu.crushedpixel.replaymod.events.GuiEventHandler; +import eu.crushedpixel.replaymod.events.GuiReplayOverlay; +import eu.crushedpixel.replaymod.events.RecordingHandler; +import eu.crushedpixel.replaymod.events.ReplayTickHandler; +import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; +import eu.crushedpixel.replaymod.settings.ReplaySettings; + +@Mod(modid = ReplayMod.MODID, version = ReplayMod.VERSION) +public class ReplayMod +{ + + //TODO: Set ReplayHandler replaying to false when replay is exited + + public static final String MODID = "replaymod"; + public static final String VERSION = "0.0.1"; + + public static GuiReplayOverlay overlay = new GuiReplayOverlay(); + + public static ReplaySettings replaySettings = new ReplaySettings(0, true, true, true, false); + public static Configuration config; + + public static int PLAYER_ID = -1; + + // The instance of your mod that Forge uses. + @Instance(value = "ReplayModID") + public static ReplayMod instance; + + @EventHandler + public void preInit(FMLPreInitializationEvent event) { + config = new Configuration(event.getSuggestedConfigurationFile()); + config.load(); + + Property recServer = config.get("settings", "enableRecordingServer", true, "Defines whether a recording should be started upon joining a server."); + Property recSP = config.get("settings", "enableRecordingSingleplayer", true, "Defines whether a recording should be started upon joining a singleplayer world."); + Property maxFileSize = config.get("settings", "maximumFileSize", 0, "The maximum File size (in MB) of a recording. 0 means unlimited."); + Property showNot = ReplayMod.instance.config.get("settings", "showNotifications", true, "Defines whether notifications should be sent to the player."); + Property linear = ReplayMod.instance.config.get("settings", "forceLinearPath", false, "Defines whether travelling paths should be linear instead of interpolated."); + + replaySettings = new ReplaySettings(maxFileSize.getInt(0), recServer.getBoolean(true), recSP.getBoolean(true), showNot.getBoolean(true), linear.getBoolean(false)); + + config.save(); + } + + @EventHandler + public void init(FMLInitializationEvent event) { + FMLCommonHandler.instance().bus().register(new ConnectionEventHandler()); + MinecraftForge.EVENT_BUS.register(new GuiEventHandler()); + ReplayTickHandler tickHandler = new ReplayTickHandler(); + FMLCommonHandler.instance().bus().register(tickHandler); + MinecraftForge.EVENT_BUS.register(tickHandler); + + RecordingHandler rh = new RecordingHandler(); + FMLCommonHandler.instance().bus().register(rh); + MinecraftForge.EVENT_BUS.register(rh); + } + + @EventHandler + public void postInit(FMLPostInitializationEvent event) { + overlay = new GuiReplayOverlay(); + FMLCommonHandler.instance().bus().register(overlay); + MinecraftForge.EVENT_BUS.register(overlay); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/chat/ChatMessageRequests.java b/src/main/java/eu/crushedpixel/replaymod/chat/ChatMessageRequests.java new file mode 100644 index 00000000..7d98dfad --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/chat/ChatMessageRequests.java @@ -0,0 +1,95 @@ +package eu.crushedpixel.replaymod.chat; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.IChatComponent; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; +import eu.crushedpixel.replaymod.ReplayMod; + +@SideOnly(Side.CLIENT) +public class ChatMessageRequests { + + public enum ChatMessageType { + INFORMATION, WARNING; + } + + private static boolean active = true; + private static boolean alive = true; + + private static Queue requests = new ConcurrentLinkedQueue(); + private static String prefix = "§8[§6Replay Mod§8]§r "; + + private static EntityPlayerSP player = null; + + public static Thread t = new Thread(new Runnable() { + + @Override + public void run() { + while(alive) { + while(active) { + try { + while(player == null) { + if(!alive) { + break; + } + try { + Thread.sleep(100); + player = Minecraft.getMinecraft().thePlayer; + } catch(Exception e) {} + } + + player.addChatComponentMessage(requests.poll()); + Thread.sleep(100); + } catch(Exception e) {} + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }); + + static { + t.start(); + } + + public static void addChatMessage(String message, ChatMessageType type) { + if(ReplayMod.replaySettings.isShowNotifications()) { + message = prefix+toColor(message, type); + ChatComponentText cct = new ChatComponentText(message); + requests.add(cct); + } + } + + private static String toColor(String message, ChatMessageType type) { + if(type == ChatMessageType.INFORMATION) { + return "§2"+message; + } else if(type == ChatMessageType.WARNING) { + return "§c"+message; + } + + return message; + } + + public static void stop() { + active = false; + } + + public static void initialize() { + active = true; + requests.clear(); + if(ReplayMod.replaySettings.isShowNotifications()) { + } else { + System.out.println("Chat messages are disabled"); + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java b/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java new file mode 100644 index 00000000..b5f7b781 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java @@ -0,0 +1,208 @@ +package eu.crushedpixel.replaymod.entities; + +import java.lang.reflect.Field; + +import net.minecraft.client.Minecraft; +import net.minecraft.entity.Entity; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.MathHelper; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import net.minecraft.world.WorldSettings.GameType; + +import org.lwjgl.Sys; + +import eu.crushedpixel.replaymod.holders.Position; +import eu.crushedpixel.replaymod.replay.LesserDataWatcher; +import eu.crushedpixel.replaymod.replay.ReplayHandler; + +public class CameraEntity extends Entity { + + private Vec3 direction; + private double motion; + + private Field drawBlockOutline; + + private static final double SPEED = 10; //2 blocks per second + + private double decay = 4; //decays by 75% per second; + + private long lastCall = 0; + + //frac = time since last tick + public void updateMovement() { + Minecraft mc = Minecraft.getMinecraft(); + if(ReplayHandler.getCameraEntity() != null && mc.thePlayer != null) { + //Aligns the particle rotation + mc.thePlayer.rotationPitch = ReplayHandler.getCameraEntity().rotationPitch; + mc.thePlayer.rotationYaw = ReplayHandler.getCameraEntity().rotationYaw; + + //removes water/suffocation/shadow overlays in screen + mc.thePlayer.posX = 0; + mc.thePlayer.posY = 500; + mc.thePlayer.posZ = 0; + } + + if(direction == null || motion < 0.1) { + lastCall = Sys.getTime(); + return; + } + + long frac = Sys.getTime() - lastCall; + + if(frac == 0) return; + + Vec3 movement = direction.normalize(); + double factor = motion*(frac/1000D); + + moveRelative(movement.xCoord*factor, movement.yCoord*factor, movement.zCoord*factor); + + double decFac = Math.max(0, 1-(decay*(frac/1000D))); + motion *= decFac; + + lastCall = Sys.getTime(); + } + + public void setDirection(float pitch, float yaw) { + this.setRotation(yaw, pitch); + } + + public void setMovement(MoveDirection dir) { + Vec3 oldDir = direction; + + switch(dir) { + case BACKWARD: + direction = this.getVectorForRotation(-rotationPitch, rotationYaw-180); + break; + case DOWN: + direction = this.getVectorForRotation(90, 0); + break; + case FORWARD: + direction = this.getVectorForRotation(rotationPitch, rotationYaw); + break; + case LEFT: + direction = this.getVectorForRotation(0, rotationYaw-90); + break; + case RIGHT: + direction = this.getVectorForRotation(0, rotationYaw+90); + break; + case UP: + direction = this.getVectorForRotation(-90, 0); + break; + } + + if(oldDir != null) direction = direction.normalize().add(new Vec3(oldDir.xCoord*(motion/4f), oldDir.yCoord*(motion/4f), oldDir.zCoord*(motion/4f)).normalize()); + + this.motion = SPEED; + } + + public void moveAbsolute(double x, double y, double z) { + if(ReplayHandler.isReplaying()) return; + this.posX = x; + this.posY = y; + this.posZ = z; + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.lastTickPosX = this.posX; + this.lastTickPosY = this.posY; + this.lastTickPosZ = this.posZ; + } + + public void moveRelative(double x, double y, double z) { + if(ReplayHandler.isReplaying()) return; + this.posX += x; + this.posY += y; + this.posZ += z; + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.lastTickPosX = this.posX; + this.lastTickPosY = this.posY; + this.lastTickPosZ = this.posZ; + } + + public void movePath(Position pos) { + this.posX = pos.getX(); + this.posY = pos.getY(); + this.posZ = pos.getZ(); + this.rotationPitch = pos.getPitch(); + this.rotationYaw = pos.getYaw(); + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.prevRotationPitch = this.rotationPitch; + this.prevRotationYaw = this.rotationYaw; + this.lastTickPosX = this.posX; + this.lastTickPosY = this.posY; + this.lastTickPosZ = this.posZ; + } + + @Override + protected void entityInit() { + this.dataWatcher = new LesserDataWatcher(this); + } + + + public CameraEntity(World worldIn) { + super(worldIn); + } + + @Override + public void setAngles(float yaw, float pitch) + { + 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); + this.prevRotationPitch = this.rotationPitch; + this.prevRotationYaw = this.rotationYaw; + } + + + @Override + public MovingObjectPosition rayTrace(double p_174822_1_, float p_174822_3_) { + return null; + } + + @Override + public boolean canBePushed() { + return false; + } + + @Override + protected void createRunningParticles() {} + + @Override + public boolean canBeCollidedWith() { + return false; + } + @Override + public boolean canRenderOnFire() { + return false; + } + + /* + @Override + public boolean isSpectator() { + return true; + } + */ + + @Override + protected void readEntityFromNBT(NBTTagCompound tagCompund) { + // TODO Auto-generated method stub + + } + + @Override + protected void writeEntityToNBT(NBTTagCompound tagCompound) { + // TODO Auto-generated method stub + + } + + public enum MoveDirection { + UP, DOWN, LEFT, RIGHT, FORWARD, BACKWARD; + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java new file mode 100644 index 00000000..066e28fb --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java @@ -0,0 +1,48 @@ +package eu.crushedpixel.replaymod.events; + +import eu.crushedpixel.replaymod.gui.GuiCustomMainMenu; +import eu.crushedpixel.replaymod.gui.GuiCustomOptions; +import eu.crushedpixel.replaymod.gui.GuiExitReplay; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import net.minecraft.client.gui.GuiChat; +import net.minecraft.client.gui.GuiDisconnected; +import net.minecraft.client.gui.GuiIngameMenu; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiOptions; +import net.minecraft.client.gui.inventory.GuiInventory; +import net.minecraftforge.client.event.GuiOpenEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +public class GuiEventHandler { + + @SubscribeEvent + public void onGui(GuiOpenEvent event) { + if(event.gui instanceof GuiMainMenu) { + event.gui = new GuiCustomMainMenu(); + } else if(event.gui instanceof GuiOptions) { + GuiOptions go = (GuiOptions)event.gui; + GuiCustomOptions gco = new GuiCustomOptions(GuiCustomOptions.getGuiScreen(go), GuiCustomOptions.getGameSettings(go)); + event.gui = gco; + } + + else if(event.gui instanceof GuiChat || event.gui instanceof GuiInventory) { + if(ReplayHandler.replayActive()) { + event.setCanceled(true); + } + } + + else if(event.gui instanceof GuiIngameMenu) { + if(ReplayHandler.replayActive()) { + event.gui = new GuiExitReplay(); + } + } + + else if(event.gui instanceof GuiDisconnected) { + if(!ReplayHandler.replayActive() && System.currentTimeMillis() - ReplayHandler.lastExit < 5000) { + event.setCanceled(true); + } + } + + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/events/GuiReplayOverlay.java b/src/main/java/eu/crushedpixel/replaymod/events/GuiReplayOverlay.java new file mode 100644 index 00000000..033ac2fe --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/events/GuiReplayOverlay.java @@ -0,0 +1,810 @@ +package eu.crushedpixel.replaymod.events; + +import java.awt.Color; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.TimeUnit; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.event.MouseEvent; +import net.minecraftforge.client.event.RenderGameOverlayEvent; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import net.minecraftforge.fml.client.FMLClientHandler; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.InputEvent.KeyInputEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; + +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.Display; +import org.lwjgl.opengl.GL11; + +import eu.crushedpixel.replaymod.entities.CameraEntity; +import eu.crushedpixel.replaymod.entities.CameraEntity.MoveDirection; +import eu.crushedpixel.replaymod.gui.GuiMouseInput; +import eu.crushedpixel.replaymod.gui.GuiReplaySpeedSlider; +import eu.crushedpixel.replaymod.holders.Keyframe; +import eu.crushedpixel.replaymod.holders.Position; +import eu.crushedpixel.replaymod.holders.PositionKeyframe; +import eu.crushedpixel.replaymod.holders.TimeKeyframe; +import eu.crushedpixel.replaymod.reflection.MCPNames; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import eu.crushedpixel.replaymod.replay.ReplayProcess; + +public class GuiReplayOverlay extends Gui { + + private Minecraft mc = Minecraft.getMinecraft(); + + private int sliderX = 35; + private int sliderY = 10; + + private int timelineX = sliderX+100+5; + private int realTimelineX = 10 + 3*25; + private int realTimelineY = 33+10; + + private int realTimePosition = 0; + + private int ppButtonX = 10; + private int ppButtonY = 10; + + private int r_ppButtonX = 10; + private int r_ppButtonY = realTimelineY+1; + + private int place_ButtonX = 35; + private int place_ButtonY = realTimelineY+1; + + private int time_ButtonX = 60; + private int time_ButtonY = realTimelineY+1; + + private long lastSystemTime = System.currentTimeMillis(); + + private ResourceLocation guiLocation = new ResourceLocation("replaymod", "replay_gui.png"); + private ResourceLocation keyframeLocation = new ResourceLocation("replaymod", "extended_gui.png"); + private ResourceLocation timelineLocation = new ResourceLocation("replaymod", "timeline_icons.png"); + + private GuiReplaySpeedSlider speedSlider; + + private boolean mouseDown = false; + + private Field drawBlockOutline; + + public GuiReplayOverlay() { + try { + drawBlockOutline = EntityRenderer.class.getDeclaredField(MCPNames.field("field_175073_D")); + drawBlockOutline.setAccessible(true); + drawBlockOutline.set(Minecraft.getMinecraft().entityRenderer, false); + } catch(Exception e) {} + } + + @SubscribeEvent + public void tick(TickEvent event) { + if(!ReplayHandler.replayActive()) return; + if(ReplayHandler.getCameraEntity() != null) + ReplayHandler.getCameraEntity().updateMovement(); + onMouseMove(new MouseEvent()); + onKeyInput(new KeyInputEvent()); + if(ReplayHandler.isReplaying()) ReplayProcess.tickReplay(); + } + + @SubscribeEvent + public void onRenderWorld(RenderWorldLastEvent event) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, IOException { + if(!ReplayHandler.replayActive()) return; + ReplayHandler.setCameraEntity(ReplayHandler.getCameraEntity()); + if(ReplayHandler.replayActive() && ReplayHandler.isPaused()) { + if(mc != null && mc.thePlayer != null) + MinecraftTicker.runMouseKeyboardTick(mc); + } + } + + public void resetUI() throws Exception { + if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + mc.displayGuiScreen((GuiScreen)null); + } + realTimePosition = 0; + speedSlider = new GuiReplaySpeedSlider(1, sliderX, sliderY, "Speed"); + } + + @SubscribeEvent + public void onRenderGui(RenderGameOverlayEvent.Post event) throws IllegalArgumentException, IllegalAccessException { + if(!ReplayHandler.replayActive()) return; + GL11.glEnable(GL11.GL_BLEND); + drawBlockOutline.set(Minecraft.getMinecraft().entityRenderer, false); + + if(!ReplayHandler.replayActive()) { + return; + } + + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + final int width = sr.getScaledWidth(); + final int height = sr.getScaledHeight(); + + final int mouseX = (Mouse.getX() * width / mc.displayWidth); + final int mouseY = (height - Mouse.getY() * height / mc.displayHeight); + + //Draw Timeline + drawTimeline(timelineX, width - 14, 9); + drawRealTimeline(realTimelineX, width - 14 - 11, realTimelineY, mouseX, mouseY); + + //Play/Pause button + int x = 0; + int y = 0; + + boolean play = !ReplayHandler.isPaused(); + boolean hover = false; + + if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + if(mouseX >= ppButtonX && mouseX <= ppButtonX+20 + && mouseY >= ppButtonY && mouseY <= ppButtonY+20) { + hover = true; + } + } + + if(play) { + y = 20; + } + if(hover) { + x = 20; + } + + mc.renderEngine.bindTexture(guiLocation); + + GlStateManager.resetColor(); + this.drawModalRectWithCustomSizedTexture(ppButtonX, ppButtonY, x, y, 20, 20, 64, 64); + + //GlStateManager.resetColor(); + + if(Mouse.isButtonDown(0) && FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { //clicking the Button + speedSlider.mousePressed(mc, mouseX, mouseY); + if(!mouseDown) { + mouseDown = true; + if(hover) { + boolean paused = !ReplayHandler.isPaused(); + if(paused) { + ReplayHandler.setSpeed(0); + } else { + ReplayHandler.setSpeed(speedSlider.getSliderValue()); + } + + } + + if(mouseX >= timelineX+4 && mouseX <= width - 18 && mouseY >= 11 && mouseY <= 29) { + double tot = (width - 18)-(timelineX+4); + double perc = (mouseX-(timelineX+4))/tot; + double time = perc*(double)ReplayHandler.getReplayLength(); + + if(time < ReplayHandler.getReplayTime()) { + mc.displayGuiScreen((GuiScreen)null); + } + + ReplayHandler.setReplayPos((int)time, false); + } + } + + } else { + speedSlider.mouseReleased(mouseX, mouseY); + mouseDown = false; + } + + + //Place Keyframe Button + hover = false; + x = 0; + y = 0; + + if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + if(mouseX >= place_ButtonX && mouseX <= place_ButtonX+20 + && mouseY >= place_ButtonY && mouseY <= place_ButtonY+20) { + hover = true; + } + } + + if(hover) { + x = 20; + } + + if(ReplayHandler.getSelected() != null && ReplayHandler.getSelected() instanceof PositionKeyframe) { + y += 20; + } + + mc.renderEngine.bindTexture(keyframeLocation); + + if(hover && Mouse.isButtonDown(0) && isClick()) { + if(ReplayHandler.getSelected() == null || !(ReplayHandler.getSelected() instanceof PositionKeyframe)) { + addPlaceKeyframe(); + } else { + ReplayHandler.removeKeyframe(ReplayHandler.getSelected()); + } + } + + GlStateManager.resetColor(); + this.drawModalRectWithCustomSizedTexture(place_ButtonX, place_ButtonY, x, y, 20, 20, 64, 64); + + + //Time Keyframe Button + hover = false; + x = 0; + y = 40; + + boolean timeSelected = false; + if(ReplayHandler.getSelected() != null && ReplayHandler.getSelected() instanceof TimeKeyframe) { + timeSelected = true; + x = 40; + y = 0; + } + + if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + if(mouseX >= time_ButtonX && mouseX <= time_ButtonX+20 + && mouseY >= time_ButtonY && mouseY <= time_ButtonY+20) { + hover = true; + } + } + + if(hover) { + if(timeSelected) { + y = 20; + } else { + x = 20; + } + } + + mc.renderEngine.bindTexture(keyframeLocation); + + if(hover && Mouse.isButtonDown(0) && isClick()) { + if(ReplayHandler.getSelected() == null || !(ReplayHandler.getSelected() instanceof TimeKeyframe)) { + addTimeKeyframe(); + } else { + ReplayHandler.removeKeyframe(ReplayHandler.getSelected()); + } + } + + GlStateManager.resetColor(); + this.drawModalRectWithCustomSizedTexture(time_ButtonX, time_ButtonY, x, y, 20, 20, 64, 64); + + if(mouseX >= (timelineX+4) && mouseX <= width - 18 && mouseY >= 11 && mouseY <= 29) { + double tot = (width - 18)-(timelineX+4); + double perc = (mouseX-(timelineX+4))/tot; + long time = Math.round(perc*(double)ReplayHandler.getReplayLength()); + + String timestamp = (String.format("%02d:%02ds", + TimeUnit.MILLISECONDS.toMinutes(time), + TimeUnit.MILLISECONDS.toSeconds(time) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(time)) + )); + + this.drawCenteredString(mc.fontRendererObj, timestamp, mouseX, mouseY+5, Color.WHITE.getRGB()); + } + + if(mc.inGameHasFocus) { + Mouse.setCursorPosition(width/2, height/2); + } + + speedSlider.drawButton(mc, mouseX, mouseY); + GlStateManager.resetColor(); + + Entity player = ReplayHandler.getCameraEntity(); + if(player != null) { + player.setVelocity(0, 0, 0); + } + + if(!Mouse.isButtonDown(0)) isClick(); + } + + private int tl_begin_x=0; + private int tl_begin_width = 4; + + private int tl_end_x=60; + private int tl_end_width = 4; + + private int tl_middle_x=4; + + private int tl_y=40; + + private void drawTimeline(int minX, int maxX, int y) { + int zero = minX+tl_begin_width; + int full = maxX-tl_end_width; + + GlStateManager.resetColor(); + mc.renderEngine.bindTexture(guiLocation); + this.drawModalRectWithCustomSizedTexture(minX, y, tl_begin_x, tl_y, tl_begin_width, 22, 64, 64); + + for(int i=minX+tl_begin_width; i= slider_min && mouseX <= sl_max && mouseY >= sl_y && mouseY <= sl_y+slider_height || wasSliding)) { + wasSliding = true; + float dx = ((float)Mouse.getDX() * (float)new ScaledResolution(mc, mc.displayWidth, mc.displayHeight).getScaledWidth() / mc.displayWidth); + this.pos_left = Math.min(1f-this.zoom_scale, Math.max(0f, this.pos_left+(dx/(float)tlWidth))); + } + + if(!Mouse.isButtonDown(0)) { + wasSliding = false; + } + + //Timeline Buttons + //+- Buttons + boolean hover = false; + int px = plus_x; + if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + if(mouseX >= maxX+2 && mouseX <= maxX+2+9 + && mouseY >= y+1 && mouseY <= y+1+9) { + hover = true; + } + } + + if(hover) { + px+=9; + } + + this.drawModalRectWithCustomSizedTexture(maxX+2, y+1, px, plus_y, 9, 9, 64, 64); + + if(hover && Mouse.isButtonDown(0)) { + zoomIn(); + } + + hover = false; + int mx = minus_x; + + if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + if(mouseX >= maxX+2 && mouseX <= maxX+2+9 + && mouseY >= y+9+3 && mouseY <= y+9+3+9) { + hover = true; + } + } + + if(hover) { + mx+=9; + } + + this.drawModalRectWithCustomSizedTexture(maxX+2, y+9+3, mx, minus_y, 9, 9, 64, 64); + + if(hover && Mouse.isButtonDown(0)) { + zoomOut(); + } + + //show Time String + if(mouseX >= zero && mouseX <= full && mouseY >= y && mouseY <= y+22) { + long tot = Math.round((double)timelineLength*zoom_scale); + double perc = (mouseX-(realTimelineX+4))/(double)(full-zero); + + long time = Math.round(this.pos_left*(double)timelineLength)+Math.round(perc*(double)tot); + + String timestamp = (String.format("%02d:%02ds", + TimeUnit.MILLISECONDS.toMinutes(time), + TimeUnit.MILLISECONDS.toSeconds(time) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(time)) + )); + this.drawCenteredString(mc.fontRendererObj, timestamp, mouseX, mouseY+5, Color.WHITE.getRGB()); + } + + //draw Markers on timeline + MarkerType mt = MarkerType.getMarkerType(zoom_scale, timelineLength); + + //every x seconds, draw small marker + long left_real = Math.round(pos_left*(double)timelineLength); + long right_real = left_real+(Math.round(zoom_scale*timelineLength)); + long tot = Math.round((double)timelineLength*zoom_scale); + + for(int s=0; s<=timelineLength; s+= mt.getSmallDistance()) { + if(s > right_real) break; + if(s >= left_real) { + //calculate absolute position on screen + long relative = (s) - (left_real); + double perc = ((double)relative/(double)tot); + + long real_width = full-zero; + long rel_x = Math.round(perc*(double)real_width); + + long real_x = zero+rel_x; + + this.drawVerticalLine((int)real_x, y+19-3, y+19, Color.WHITE.getRGB()); + } + } + + //every x seconds, draw big marker + for(int s=0; s<=timelineLength; s+= mt.getDistance()) { + if(s > right_real) break; + if(s >= left_real) { + //calculate absolute position on screen + long relative = s - (left_real); + double perc = ((double)relative/(double)tot); + + long real_width = full-zero; + long rel_x = Math.round(perc*(double)real_width); + + long real_x = zero+rel_x; + + this.drawVerticalLine((int)real_x, y+19-7, y+19, Color.LIGHT_GRAY.getRGB()); + + //write text + int time = s; + String timestamp = (String.format("%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(time), + TimeUnit.MILLISECONDS.toSeconds(time) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(time)) + )); + + this.drawCenteredString(mc.fontRendererObj, timestamp, (int)real_x, y-8, Color.WHITE.getRGB()); + } + } + + //handle Mouse clicks on realTimeLine + if(Mouse.isButtonDown(0) && !wasSliding && mouseX >= minX+tl_begin_width && mouseX <= maxX-tl_end_width && + mouseY >= y && mouseY <= y+22) { + + //calculate real time and set cursor accordingly + int width = (maxX-tl_end_width) - (minX+tl_begin_width); + int rel_x = mouseX-(minX+tl_begin_width); + + float rel_pos = (float)rel_x/(float)width; + + float abs_width = (zoom_scale*(float)timelineLength); + int real_pos = Math.round(left_real+((rel_pos)*abs_width)); + + realTimePosition = real_pos; + + //Keyframe click handling here + if(isClick()) { + //tolerance is 2 pixels multiplied with the timespan of one pixel + int tolerance = 2*Math.round(abs_width/(float)width); + + if(mouseY >= y+9) { + TimeKeyframe close = ReplayHandler.getClosestTimeKeyframeForRealTime(realTimePosition, tolerance); + ReplayHandler.selectKeyframe(close); //can be null, deselects keyframe + } else { + PositionKeyframe close = ReplayHandler.getClosestPlaceKeyframeForRealTime(realTimePosition, tolerance); + ReplayHandler.selectKeyframe(close); //can be null, deselects keyframe + } + } + } + + //Draw Realtime Cursor + if(realTimePosition >= left_real && realTimePosition <= right_real) { + long rel_pos = realTimePosition-left_real; + long rel_width = right_real-left_real; + double perc = (double)rel_pos/(double)rel_width; + + int real_width = (maxX-tl_end_width) - (minX+tl_begin_width); + double rel_x = (float)real_width*perc; + + int real_x = (int)Math.round((minX+tl_begin_width)+rel_x); + mc.renderEngine.bindTexture(this.guiLocation); + + GL11.glEnable(GL11.GL_BLEND); + this.drawModalRectWithCustomSizedTexture(real_x-3, y+3, 44, 0, 8, 16, 64, 64); + //this.drawModalRectWithCustomSizedTexture(real_x, sl_y, u, v, width, height, textureWidth, textureHeight) + } + + + //Draw Keyframe logos + mc.renderEngine.bindTexture(timelineLocation); + for(Keyframe kf : ReplayHandler.getKeyframes()) { + if(kf.getRealTimestamp() > right_real) break; + if(kf.getRealTimestamp() >= left_real) { + int dx = 18; + int dy = 0; + + int ry = y+3; + + if(kf instanceof TimeKeyframe) { + dy = 5; + ry += 5; + } + if(ReplayHandler.isSelected(kf)) { + dx += 5; + } + + long relative = kf.getRealTimestamp() - (left_real); + double perc = ((double)relative/(double)tot); + + long real_width = full-zero; + long rel_x = Math.round(perc*(double)real_width); + + long real_x = zero+rel_x - 2; + + this.drawModalRectWithCustomSizedTexture((int)real_x, ry, dx, dy, 5, 5, 64, 64); + } + } + + //Draw Play/Pause Button + //Play/Pause button + + int dx = 0; + int dy = 0; + + boolean play = ReplayHandler.isReplaying(); + hover = false; + + if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + if(mouseX >= r_ppButtonX && mouseX <= r_ppButtonX+20 + && mouseY >= r_ppButtonY && mouseY <= r_ppButtonY+20) { + hover = true; + } + } + + if(play) { + dy = 20; + } + if(hover) { + dx = 20; + } + + mc.renderEngine.bindTexture(guiLocation); + + GlStateManager.resetColor(); + this.drawModalRectWithCustomSizedTexture(r_ppButtonX, r_ppButtonY, dx, dy, 20, 20, 64, 64); + + //Handling the click on the Replay starter + if(hover && Mouse.isButtonDown(0) && isClick()) { + if(ReplayHandler.isReplaying()) { + ReplayHandler.interruptReplay(); + } else { + ReplayHandler.startPath(); + } + } + + } + + private void addPlaceKeyframe() { + CameraEntity cam = ReplayHandler.getCameraEntity(); + if(cam == null) return; + ReplayHandler.addKeyframe(new PositionKeyframe(realTimePosition, new Position(cam.posX, cam.posY, cam.posZ, cam.rotationPitch, cam.rotationYaw))); + } + + private void addTimeKeyframe() { + ReplayHandler.addKeyframe(new TimeKeyframe(realTimePosition, ReplayHandler.getReplayTime())); + } + + private enum MarkerType { + + ONE_S(1*1000, 100), + FIVE_S(5*1000, 1*1000), + QUARTER_M(15*1000, 3*1000), + HALF_M(30*1000, 5*1000), + ONE_M(60*1000, 10*1000), + FIVE_M(5*60*1000, 50*1000); + + int minimum; + int small_min; + int maximum = 10; + + int getDistance() { + return minimum; + } + + int getSmallDistance() { + return small_min; + } + + MarkerType(int minimum, int small_min) { + this.minimum = minimum; + this.small_min = small_min; + } + + public static MarkerType getMarkerType(float scale, long totalLength) { + long visible = Math.round((double)totalLength*scale); + long seconds = visible; + + for(MarkerType mt : values()) { + if(seconds/mt.getDistance() <= 10) { + return mt; + } + } + + return FIVE_M; + } + } + + private void zoomIn() { + if(!isClick()) return; + this.zoom_scale = Math.max(0.025f, zoom_scale-zoom_steps); + } + + private void zoomOut() { + if(!isClick()) return; + this.zoom_scale = Math.min(1f, zoom_scale+zoom_steps); + this.pos_left = Math.min(pos_left, 1f-zoom_scale); + } + + private boolean mouseDwn = false; + + private boolean isClick() { + if(Mouse.isButtonDown(0)) { + boolean bef = new Boolean(mouseDwn); + mouseDwn = true; + return !bef; + } else { + mouseDwn = false; + return false; + } + } + + @SubscribeEvent + public void onMouseMove(MouseEvent event) { + if(!ReplayHandler.replayActive()) return; + boolean flag = Display.isActive(); + flag = true; + + mc.mcProfiler.startSection("mouse"); + + if (flag && Minecraft.isRunningOnMac && mc.inGameHasFocus && !Mouse.isInsideWindow()) + { + Mouse.setGrabbed(false); + Mouse.setCursorPosition(Display.getWidth() / 2, Display.getHeight() / 2); + Mouse.setGrabbed(true); + } + + if (mc.inGameHasFocus && flag) + { + 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; + } + + ReplayHandler.getCameraEntity().setAngles(f3, f4 * (float)b0); + + } + } + + @SubscribeEvent + public void onKeyInput(KeyInputEvent event) { + if(!ReplayHandler.replayActive()) return; + if(FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) { + return; + } + + KeyBinding[] keyBindings = Minecraft.getMinecraft().gameSettings.keyBindings; + for(KeyBinding kb : keyBindings) { + if(!kb.isKeyDown()) { + continue; + } + try { + + if(kb.getKeyDescription().equals("key.forward")) { + ReplayHandler.getCameraEntity().setMovement(MoveDirection.FORWARD); + continue; + } + + if(kb.getKeyDescription().equals("key.back")) { + ReplayHandler.getCameraEntity().setMovement(MoveDirection.BACKWARD); + continue; + } + + if(kb.getKeyDescription().equals("key.jump")) { + ReplayHandler.getCameraEntity().setMovement(MoveDirection.UP); + continue; + } + + if(kb.getKeyDescription().equals("key.sneak")) { + ReplayHandler.getCameraEntity().setMovement(MoveDirection.DOWN); + continue; + } + + if(kb.getKeyDescription().equals("key.left")) { + ReplayHandler.getCameraEntity().setMovement(MoveDirection.LEFT); + continue; + } + + if(kb.getKeyDescription().equals("key.right")) { + ReplayHandler.getCameraEntity().setMovement(MoveDirection.RIGHT); + continue; + } + + if(kb.getKeyDescription().equals("key.chat")) { + mc.displayGuiScreen(new GuiMouseInput()); + continue; + } + } catch(Exception e) { + e.printStackTrace(); + } + + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/events/MinecraftTicker.java b/src/main/java/eu/crushedpixel/replaymod/events/MinecraftTicker.java new file mode 100644 index 00000000..0358a709 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/events/MinecraftTicker.java @@ -0,0 +1,442 @@ +package eu.crushedpixel.replaymod.events; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiChat; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.inventory.GuiInventory; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.crash.CrashReport; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.network.play.client.C16PacketClientStatus; +import net.minecraft.util.MathHelper; +import net.minecraft.util.ReportedException; + +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.Display; + +import eu.crushedpixel.replaymod.entities.CameraEntity; +import eu.crushedpixel.replaymod.reflection.MCPNames; +import eu.crushedpixel.replaymod.replay.ReplayHandler; + +public class MinecraftTicker { + + private static Field debugCrashKeyPressTime, rightClickDelayTimer, systemTime, leftClickCounter; + private static Method getSystemTime, updateDebugProfilerName, + clickMouse, middleClickMouse, rightClickMouse, sendClickBlockToController; + + private static Minecraft mc = Minecraft.getMinecraft(); + + private static float camPitch, camYaw, smoothCamPartialTicks, + smoothCamFilterX, smoothCamFilterY; + + static { + try { + debugCrashKeyPressTime = Minecraft.class.getDeclaredField(MCPNames.field("field_83002_am")); + debugCrashKeyPressTime.setAccessible(true); + + rightClickDelayTimer = Minecraft.class.getDeclaredField(MCPNames.field("field_71467_ac")); + rightClickDelayTimer.setAccessible(true); + + systemTime = Minecraft.class.getDeclaredField(MCPNames.field("field_71423_H")); + systemTime.setAccessible(true); + + leftClickCounter = Minecraft.class.getDeclaredField(MCPNames.field("field_71429_W")); + leftClickCounter.setAccessible(true); + + getSystemTime = Minecraft.class.getDeclaredMethod(MCPNames.method("func_71386_F")); + getSystemTime.setAccessible(true); + + updateDebugProfilerName = Minecraft.class.getDeclaredMethod(MCPNames.method("func_71383_b"), int.class); + updateDebugProfilerName.setAccessible(true); + + clickMouse = Minecraft.class.getDeclaredMethod(MCPNames.method("func_147116_af")); + clickMouse.setAccessible(true); + + rightClickMouse = Minecraft.class.getDeclaredMethod(MCPNames.method("func_147121_ag")); + rightClickMouse.setAccessible(true); + + middleClickMouse = Minecraft.class.getDeclaredMethod(MCPNames.method("func_147112_ai")); + middleClickMouse.setAccessible(true); + + sendClickBlockToController = Minecraft.class.getDeclaredMethod(MCPNames.method("func_147115_a"), boolean.class); + sendClickBlockToController.setAccessible(true); + + } catch(Exception e) { + e.printStackTrace(); + } + } + + public static void runMouseKeyboardTick(Minecraft mc) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException { + + try { + mc.mcProfiler.endStartSection("mouse"); + int i; + while(Mouse.next()) { + i = Mouse.getEventButton(); + KeyBinding.setKeyBindState(i - 100, Mouse.getEventButtonState()); + + if (Mouse.getEventButtonState()) + { + if (mc.thePlayer.isSpectator() && i == 2) + { + mc.ingameGUI.func_175187_g().func_175261_b(); + } + else + { + KeyBinding.onTick(i - 100); + } + } + + long k = (Long)getSystemTime.invoke(mc) - (Long)systemTime.get(mc); + + if (k <= 200L) + { + int j = Mouse.getEventDWheel(); + + if (j != 0) + { + if (mc.thePlayer.isSpectator()) + { + j = j < 0 ? -1 : 1; + + if (mc.ingameGUI.func_175187_g().func_175262_a()) + { + mc.ingameGUI.func_175187_g().func_175259_b(-j); + } + else + { + float f = MathHelper.clamp_float(mc.thePlayer.capabilities.getFlySpeed() + (float)j * 0.005F, 0.0F, 0.2F); + mc.thePlayer.capabilities.setFlySpeed(f); + } + } + else + { + mc.thePlayer.inventory.changeCurrentItem(j); + } + } + + if (mc.currentScreen == null) + { + if (!mc.inGameHasFocus && Mouse.getEventButtonState()) + { + mc.setIngameFocus(); + } + } + else + { + mc.currentScreen.handleMouseInput(); + } + } + net.minecraftforge.fml.common.FMLCommonHandler.instance().fireMouseInput(); + } + + if ((Integer)leftClickCounter.get(mc) > 0) + { + leftClickCounter.set(mc, (Integer)leftClickCounter.get(mc) - 1); + } + mc.mcProfiler.endStartSection("keyboard"); + + while (Keyboard.next()) + { + i = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); + KeyBinding.setKeyBindState(i, Keyboard.getEventKeyState()); + + if (Keyboard.getEventKeyState()) + { + KeyBinding.onTick(i); + } + + if ((Long)debugCrashKeyPressTime.get(mc) > 0L) + { + if ((Long)getSystemTime.invoke(mc) - (Long)debugCrashKeyPressTime.get(mc) >= 6000L) + { + throw new ReportedException(new CrashReport("Manually triggered debug crash", new Throwable())); + } + + if (!Keyboard.isKeyDown(46) || !Keyboard.isKeyDown(61)) + { + debugCrashKeyPressTime.set(mc, -1); + } + } + else if (Keyboard.isKeyDown(46) && Keyboard.isKeyDown(61)) + { + debugCrashKeyPressTime.set(mc, getSystemTime.invoke(mc)); + } + + mc.dispatchKeypresses(); + + if (Keyboard.getEventKeyState()) + { + if (i == 62 && mc.entityRenderer != null) + { + mc.entityRenderer.switchUseShader(); + } + + if (mc.currentScreen != null) + { + mc.currentScreen.handleKeyboardInput(); + } + else + { + if (i == 1) + { + mc.displayInGameMenu(); + } + + if (i == 32 && Keyboard.isKeyDown(61) && mc.ingameGUI != null) + { + mc.ingameGUI.getChatGUI().clearChatMessages(); + } + + if (i == 31 && Keyboard.isKeyDown(61)) + { + mc.refreshResources(); + } + + if (i == 17 && Keyboard.isKeyDown(61)) + { + ; + } + + if (i == 18 && Keyboard.isKeyDown(61)) + { + ; + } + + if (i == 47 && Keyboard.isKeyDown(61)) + { + ; + } + + if (i == 38 && Keyboard.isKeyDown(61)) + { + ; + } + + if (i == 22 && Keyboard.isKeyDown(61)) + { + ; + } + + if (i == 20 && Keyboard.isKeyDown(61)) + { + mc.refreshResources(); + } + + if (i == 33 && Keyboard.isKeyDown(61)) + { + boolean flag1 = Keyboard.isKeyDown(42) | Keyboard.isKeyDown(54); + mc.gameSettings.setOptionValue(GameSettings.Options.RENDER_DISTANCE, flag1 ? -1 : 1); + } + + if (i == 30 && Keyboard.isKeyDown(61)) + { + mc.renderGlobal.loadRenderers(); + } + + if (i == 35 && Keyboard.isKeyDown(61)) + { + mc.gameSettings.advancedItemTooltips = !mc.gameSettings.advancedItemTooltips; + mc.gameSettings.saveOptions(); + } + + if (i == 48 && Keyboard.isKeyDown(61)) + { + mc.getRenderManager().setDebugBoundingBox(!mc.getRenderManager().isDebugBoundingBox()); + } + + if (i == 25 && Keyboard.isKeyDown(61)) + { + mc.gameSettings.pauseOnLostFocus = !mc.gameSettings.pauseOnLostFocus; + mc.gameSettings.saveOptions(); + } + + if (i == 59) + { + mc.gameSettings.hideGUI = !mc.gameSettings.hideGUI; + } + + if (i == 61) + { + mc.gameSettings.showDebugInfo = !mc.gameSettings.showDebugInfo; + mc.gameSettings.showDebugProfilerChart = GuiScreen.isShiftKeyDown(); + } + + if (mc.gameSettings.keyBindTogglePerspective.isPressed()) + { + ++mc.gameSettings.thirdPersonView; + + if (mc.gameSettings.thirdPersonView > 2) + { + mc.gameSettings.thirdPersonView = 0; + } + + if (mc.gameSettings.thirdPersonView == 0) + { + mc.entityRenderer.loadEntityShader(mc.getRenderViewEntity()); + } + else if (mc.gameSettings.thirdPersonView == 1) + { + mc.entityRenderer.loadEntityShader((Entity)null); + } + } + + if (mc.gameSettings.keyBindSmoothCamera.isPressed()) + { + mc.gameSettings.smoothCamera = !mc.gameSettings.smoothCamera; + } + } + + if (mc.gameSettings.showDebugInfo && mc.gameSettings.showDebugProfilerChart) + { + if (i == 11) + { + updateDebugProfilerName.invoke(mc, 0); + } + + for (int l = 0; l < 9; ++l) + { + if (i == 2 + l) + { + updateDebugProfilerName.invoke(mc, l+1); + } + } + } + } + net.minecraftforge.fml.common.FMLCommonHandler.instance().fireKeyInput(); + } + + for (i = 0; i < 9; ++i) + { + if (mc.gameSettings.keyBindsHotbar[i].isPressed()) + { + if (mc.thePlayer.isSpectator()) + { + mc.ingameGUI.func_175187_g().func_175260_a(i); + } + else + { + mc.thePlayer.inventory.currentItem = i; + } + } + } + + boolean flag = mc.gameSettings.chatVisibility != EntityPlayer.EnumChatVisibility.HIDDEN; + + while (mc.gameSettings.keyBindInventory.isPressed()) + { + if (mc.playerController.isRidingHorse()) + { + mc.thePlayer.sendHorseInventory(); + } + else + { + mc.getNetHandler().addToSendQueue(new C16PacketClientStatus(C16PacketClientStatus.EnumState.OPEN_INVENTORY_ACHIEVEMENT)); + mc.displayGuiScreen(new GuiInventory(mc.thePlayer)); + } + } + + while (mc.gameSettings.keyBindDrop.isPressed()) + { + if (!mc.thePlayer.isSpectator()) + { + mc.thePlayer.dropOneItem(GuiScreen.isCtrlKeyDown()); + } + } + + while (mc.gameSettings.keyBindChat.isPressed() && flag) + { + mc.displayGuiScreen(new GuiChat()); + } + + if (mc.currentScreen == null && mc.gameSettings.keyBindCommand.isPressed() && flag) + { + mc.displayGuiScreen(new GuiChat("/")); + } + + if (mc.thePlayer != null && mc.thePlayer.isUsingItem()) + { + if (!mc.gameSettings.keyBindUseItem.isKeyDown()) + { + mc.playerController.onStoppedUsingItem(mc.thePlayer); + } + + label435: + + while (true) + { + if (!mc.gameSettings.keyBindAttack.isPressed()) + { + while (mc.gameSettings.keyBindUseItem.isPressed()) + { + ; + } + + while (true) + { + if (mc.gameSettings.keyBindPickBlock.isPressed()) + { + continue; + } + + break label435; + } + } + } + } + else + { + while (mc.gameSettings.keyBindAttack.isPressed()) + { + if(mc != null) + try { + clickMouse.invoke(mc); + } catch(Exception e) {} + + } + + while (mc.gameSettings.keyBindUseItem.isPressed()) + { + if(mc != null) + try { + rightClickMouse.invoke(mc); + } catch(Exception e) {} + } + + while (mc.gameSettings.keyBindPickBlock.isPressed()) + { + if(mc != null) + try { + middleClickMouse.invoke(mc); + } catch(Exception e) {} + } + } + + if (mc.gameSettings.keyBindUseItem.isKeyDown() && (Integer)rightClickDelayTimer.get(mc) == 0 && !mc.thePlayer.isUsingItem()) + { + if(mc != null) + try { + rightClickMouse.invoke(mc); + } catch(Exception e) {} + } + + if(mc != null) + try { + sendClickBlockToController.invoke(mc, mc.currentScreen == null && mc.gameSettings.keyBindAttack.isKeyDown() && mc.inGameHasFocus); + } catch(Exception e) {} + + if(mc != null) + systemTime.set(mc, getSystemTime.invoke(mc)); + } catch(Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/events/RecordingHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/RecordingHandler.java new file mode 100644 index 00000000..b7f4cf53 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/events/RecordingHandler.java @@ -0,0 +1,87 @@ +package eu.crushedpixel.replaymod.events; + +import java.io.IOException; +import java.util.UUID; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.network.Packet; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.play.server.S0CPacketSpawnPlayer; +import net.minecraft.network.play.server.S14PacketEntity.S17PacketEntityLookMove; +import net.minecraft.network.play.server.S18PacketEntityTeleport; +import net.minecraft.util.MathHelper; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent.PlayerTickEvent; +import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; + +public class RecordingHandler { + + private Minecraft mc = Minecraft.getMinecraft(); + + private int entityID = Integer.MAX_VALUE; + + @SubscribeEvent + public void onPlayerJoin(EntityJoinWorldEvent e) throws IOException { + if(e.entity != mc.thePlayer) return; + if(!ConnectionEventHandler.isRecording()) return; + + //Packet packet = new S0CPacketSpawnPlayer((EntityPlayer)e.entity); + Packet packet = new S0CPacketSpawnPlayer(); + + ByteBuf bb = Unpooled.buffer(); + PacketBuffer pb = new PacketBuffer(bb); + + pb.writeVarIntToBuffer(entityID); + //pb.writeUuid((e.entity).getUniqueID()); + pb.writeUuid(UUID.randomUUID()); + + pb.writeInt(MathHelper.floor_double(e.entity.posX * 32.0D)); + pb.writeInt(MathHelper.floor_double(e.entity.posY * 32.0D)); + pb.writeInt(MathHelper.floor_double(e.entity.posZ * 32.0D)); + pb.writeByte((byte)((int)(((EntityPlayer)e.entity).rotationYaw * 256.0F / 360.0F))); + pb.writeByte((byte)((int)(((EntityPlayer)e.entity).rotationPitch * 256.0F / 360.0F))); + + ItemStack itemstack = ((EntityPlayer)e.entity).inventory.getCurrentItem(); + pb.writeInt(itemstack == null ? 0 : Item.getIdFromItem(itemstack.getItem())); + ((EntityPlayer)e.entity).getDataWatcher().writeTo(pb); + + packet.readPacketData(pb); + + ConnectionEventHandler.insertPacket(packet); + } + + @SubscribeEvent + public void onPlayerTick(PlayerTickEvent e) { + if(e.player != mc.thePlayer) return; + if(!ConnectionEventHandler.isRecording()) return; + + double dx = e.player.prevPosX - e.player.posX; + double dy = e.player.prevPosY - e.player.posY; + double dz = e.player.prevPosZ - e.player.posZ; + + Packet packet; + if(Math.abs(dx) > 4.0 || Math.abs(dy) > 4.0 || Math.abs(dz) > 4.0) { + packet = new S18PacketEntityTeleport(e.player); + } else { + byte oldYaw = (byte)((int)(e.player.prevRotationYaw * 256.0F / 360.0F)); + byte newYaw = (byte)((int)(e.player.rotationYaw * 256.0F / 360.0F)); + byte oldPitch = (byte)((int)(e.player.prevRotationPitch * 256.0F / 360.0F)); + byte newPitch = (byte)((int)(e.player.rotationPitch * 256.0F / 360.0F)); + + byte dPitch = (byte)(newPitch-oldPitch); + byte dYaw = (byte)(newYaw-oldYaw); + + packet = new S17PacketEntityLookMove(entityID, + (byte)Math.round(dx*32), (byte)Math.round(dy*32), (byte)Math.round(dz*32), + dYaw, dPitch, e.player.onGround); + } + + ConnectionEventHandler.insertPacket(packet); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/events/ReplayTickHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/ReplayTickHandler.java new file mode 100644 index 00000000..eb2b91d4 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/events/ReplayTickHandler.java @@ -0,0 +1,41 @@ +package eu.crushedpixel.replaymod.events; + +import java.lang.reflect.Field; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.entity.Entity; +import net.minecraft.world.WorldSettings.GameType; +import net.minecraftforge.client.event.FOVUpdateEvent; +import net.minecraftforge.client.event.RenderWorldEvent; +import net.minecraftforge.event.entity.player.AchievementEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.entity.player.PlayerFlyableFallEvent; +import net.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.fml.common.eventhandler.Event; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent.PlayerTickEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent.ServerTickEvent; +import eu.crushedpixel.replaymod.reflection.MCPNames; +import eu.crushedpixel.replaymod.replay.ReplayHandler; + +public class ReplayTickHandler { + + private Minecraft mc = Minecraft.getMinecraft(); + + @SubscribeEvent + public void onPlayer(PlayerEvent event) { + if(ReplayHandler.replayActive() && event.isCancelable()) { + event.setCanceled(true); + } + } + + + @SubscribeEvent + public void onFovChange(FOVUpdateEvent event) { + if(ReplayHandler.replayActive()) { + event.newfov = 1f; + } + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiCustomMainMenu.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiCustomMainMenu.java new file mode 100644 index 00000000..c1de01ce --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiCustomMainMenu.java @@ -0,0 +1,139 @@ +package eu.crushedpixel.replaymod.gui; + +import io.netty.channel.SimpleChannelInboundHandler; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Calendar; +import java.util.Date; + +import eu.crushedpixel.replaymod.reflection.MCPNames; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiButtonLanguage; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiSelectWorld; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.resources.I18n; +import net.minecraft.network.Packet; + +public class GuiCustomMainMenu extends GuiMainMenu { + + private static final int REPLAY_MANAGER_ID = 9001; + + @Override + public void initGui() { + Class clazz = GuiMainMenu.class; + + int i1 = this.height / 4 + 48; + this.buttonList.add(new GuiButton(REPLAY_MANAGER_ID, this.width / 2 - 100, i1 + 2*24, I18n.format("Replay Manager", new Object[0]))); + + try { + Field viewportTexture = clazz.getDeclaredField(MCPNames.field("field_73977_n")); + viewportTexture.setAccessible(true); + viewportTexture.set(this, new DynamicTexture(256, 256)); + + Field field_110351_G = clazz.getDeclaredField(MCPNames.field("field_110351_G")); + field_110351_G.setAccessible(true); + field_110351_G.set(this, this.mc.getTextureManager().getDynamicTextureLocation("background", (DynamicTexture)viewportTexture.get(this))); + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + + Field splashText = clazz.getDeclaredField(MCPNames.field("field_73975_c")); + splashText.setAccessible(true); + + if (calendar.get(2) + 1 == 11 && calendar.get(5) == 9) + { + splashText.set(this, "Happy birthday, ez!"); + } + else if (calendar.get(2) + 1 == 6 && calendar.get(5) == 1) + { + splashText.set(this, "Happy birthday, Notch!"); + } + else if (calendar.get(2) + 1 == 12 && calendar.get(5) == 24) + { + splashText.set(this, "Merry X-Mas!"); + } + else if (calendar.get(2) + 1 == 1 && calendar.get(5) == 1) + { + splashText.set(this, "Happy new year!"); + } + else if (calendar.get(2) + 1 == 10 && calendar.get(5) == 31) + { + splashText.set(this, "OOoooOOOoooo! Spooky!"); + } + + boolean flag = true; + int i = this.height / 4 + 48; + + if (this.mc.isDemo()) + { + Method addDemoButtons = clazz.getDeclaredMethod(MCPNames.method("func_73972_b"), int.class, int.class); + addDemoButtons.setAccessible(true); + addDemoButtons.invoke(this, i, 24); + } + else + { + Method addSingleplayerMultiplayerButtons = clazz.getDeclaredMethod(MCPNames.method("func_73969_a"), int.class, int.class); + addSingleplayerMultiplayerButtons.setAccessible(true); + addSingleplayerMultiplayerButtons.invoke(this, i - 24, 24); + } + + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, i + 72 + 12, 98, 20, I18n.format("menu.options", new Object[0]))); + this.buttonList.add(new GuiButton(4, this.width / 2 + 2, i + 72 + 12, 98, 20, I18n.format("menu.quit", new Object[0]))); + this.buttonList.add(new GuiButtonLanguage(5, this.width / 2 - 124, i + 72 + 12)); + + Field threadLock = clazz.getDeclaredField(MCPNames.field("field_104025_t")); + threadLock.setAccessible(true); + Object object = threadLock.get(this); + + Field field_92023_s = clazz.getDeclaredField(MCPNames.field("field_92023_s")); + field_92023_s.setAccessible(true); + + Field openGLWarning1 = clazz.getDeclaredField(MCPNames.field("openGLWarning1")); + openGLWarning1.setAccessible(true); + + Field openGLWarning2 = clazz.getDeclaredField(MCPNames.field("openGLWarning2")); + openGLWarning2.setAccessible(true); + + Field field_92024_r = clazz.getDeclaredField(MCPNames.field("field_92024_r")); + field_92024_r.setAccessible(true); + + Field field_92022_t = clazz.getDeclaredField(MCPNames.field("field_92022_t")); + field_92022_t.setAccessible(true); + + Field field_92021_u = clazz.getDeclaredField(MCPNames.field("field_92021_u")); + field_92021_u.setAccessible(true); + + Field field_92020_v = clazz.getDeclaredField(MCPNames.field("field_92020_v")); + field_92020_v.setAccessible(true); + + Field field_92019_w = clazz.getDeclaredField(MCPNames.field("field_92019_w")); + field_92019_w.setAccessible(true); + + synchronized (object) + { + field_92023_s.set(this, this.fontRendererObj.getStringWidth((String)openGLWarning1.get(this))); + field_92024_r.set(this, this.fontRendererObj.getStringWidth((String)openGLWarning2.get(this))); + int j = Math.max((Integer)(field_92023_s.get(this)), (Integer)(field_92024_r.get(this))); + field_92022_t.set(this, (this.width - j) / 2); + field_92021_u.set(this, ((GuiButton)this.buttonList.get(0)).yPosition - 24); + field_92020_v.set(this, (Integer)field_92022_t.get(this) + j); + field_92019_w.set(this, (Integer)field_92021_u.get(this) + 24); + } + + } catch(Exception e) { + e.printStackTrace(); + } + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + if(button.id == REPLAY_MANAGER_ID) { + this.mc.displayGuiScreen(new GuiReplayManager()); + } + super.actionPerformed(button); + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiCustomOptions.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiCustomOptions.java new file mode 100644 index 00000000..9c6044d2 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiCustomOptions.java @@ -0,0 +1,63 @@ +package eu.crushedpixel.replaymod.gui; + +import java.io.IOException; +import java.lang.reflect.Field; + +import com.mojang.realmsclient.util.Pair; + +import eu.crushedpixel.replaymod.reflection.MCPNames; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiCustomizeSkin; +import net.minecraft.client.gui.GuiOptionButton; +import net.minecraft.client.gui.GuiOptions; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.settings.GameSettings; + +public class GuiCustomOptions extends GuiOptions { + + public static GuiScreen getGuiScreen(GuiOptions go) { + try { + Class clazz = GuiOptions.class; + Field guiScreen = clazz.getDeclaredField(MCPNames.field(MCPNames.field("field_146441_g"))); + guiScreen.setAccessible(true); + Object obj = guiScreen.get(go); + return (GuiScreen)obj; + } catch(Exception e) { + e.printStackTrace(); + return null; + } + } + + public static GameSettings getGameSettings(GuiOptions go) { + try { + Class clazz = GuiOptions.class; + Field gameSettings = clazz.getDeclaredField(MCPNames.field(MCPNames.field("field_146443_h"))); + gameSettings.setAccessible(true); + Object obj = gameSettings.get(go); + return (GameSettings)obj; + } catch(Exception e) { + e.printStackTrace(); + return null; + } + } + + public GuiCustomOptions(GuiScreen guiScreen, GameSettings gameSettings) { + super(guiScreen, gameSettings); + } + + @Override + public void initGui() { + super.initGui(); + buttonList.add(new GuiButton(9001, this.width / 2 - 155, this.height / 6 + 48 - 6 - 24, 310, 20, "Replay Mod Settings...")); + } + + @Override + public void actionPerformed(GuiButton button) throws IOException { + super.actionPerformed(button); + if (button.enabled && button.id == 9001) { //Replay Mod Settings... + this.mc.displayGuiScreen(new GuiReplaySettings(this)); + } + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiExitReplay.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiExitReplay.java new file mode 100644 index 00000000..cc227a1c --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiExitReplay.java @@ -0,0 +1,65 @@ +package eu.crushedpixel.replaymod.gui; + +import java.io.IOException; + +import eu.crushedpixel.replaymod.replay.ReplayHandler; +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.multiplayer.WorldClient; +import net.minecraft.client.resources.I18n; + +public class GuiExitReplay extends GuiIngameMenu { + + @Override + public void initGui() + { + this.buttonList.clear(); + byte b0 = -16; + boolean flag = true; + this.buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 4 + 120 + b0, I18n.format("menu.returnToMenu", new Object[0]))); + + if (!this.mc.isIntegratedServerRunning()) + { + ((GuiButton)this.buttonList.get(0)).displayString = I18n.format("Exit Replay", new Object[0]); + } + + this.buttonList.add(new GuiButton(4, this.width / 2 - 100, this.height / 4 + 24 + b0, I18n.format("menu.returnToGame", new Object[0]))); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 4 + 96 + b0, 98, 20, I18n.format("menu.options", new Object[0]))); + this.buttonList.add(new GuiButton(12, this.width / 2 + 2, this.height / 4 + 96 + b0, 98, 20, I18n.format("fml.menu.modoptions"))); + GuiButton guibutton; + this.buttonList.add(guibutton = new GuiButton(7, this.width / 2 - 100, this.height / 4 + 72 + b0, 200, 20, I18n.format("menu.shareToLan", new Object[0]))); + this.buttonList.add(new GuiButton(5, this.width / 2 - 100, this.height / 4 + 48 + b0, 98, 20, I18n.format("gui.achievements", new Object[0]))); + this.buttonList.add(new GuiButton(6, this.width / 2 + 2, this.height / 4 + 48 + b0, 98, 20, I18n.format("gui.stats", new Object[0]))); + guibutton.enabled = this.mc.isSingleplayer() && !this.mc.getIntegratedServer().getPublic(); + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException + { + if(button.id == 1) { + button.enabled = false; + + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + ReplayHandler.endReplay(); + ReplayHandler.setSpeed(1f); + + ReplayHandler.lastExit = System.currentTimeMillis(); + mc.theWorld.sendQuittingDisconnectingPacket(); + mc.displayGuiScreen(new GuiMainMenu()); + //mc.loadWorld((WorldClient)null); + } + }); + + t.run(); + + + } else { + super.actionPerformed(button); + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiMouseInput.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiMouseInput.java new file mode 100644 index 00000000..6d2d6dc2 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiMouseInput.java @@ -0,0 +1,10 @@ +package eu.crushedpixel.replaymod.gui; + +import org.lwjgl.input.Keyboard; + +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiTextField; + +public class GuiMouseInput extends GuiScreen { + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenameReplay.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenameReplay.java new file mode 100644 index 00000000..df8707ce --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenameReplay.java @@ -0,0 +1,102 @@ +package eu.crushedpixel.replaymod.gui; + +import java.io.File; +import java.io.IOException; + +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.apache.commons.io.FilenameUtils; +import org.lwjgl.input.Keyboard; + +import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; + +public class GuiRenameReplay extends GuiScreen +{ + private GuiScreen field_146585_a; + private GuiTextField field_146583_f; + private static final String __OBFID = "CL_00000709"; + + private File file; + + public GuiRenameReplay(GuiScreen parent, File file) + { + this.field_146585_a = parent; + this.file = file; + } + + public void updateScreen() + { + this.field_146583_f.updateCursorCounter(); + } + + public void initGui() + { + Keyboard.enableRepeatEvents(true); + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 4 + 96 + 12, I18n.format("Rename", new Object[0]))); + this.buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 4 + 120 + 12, I18n.format("Cancel", new Object[0]))); + String s = FilenameUtils.getBaseName(file.getAbsolutePath()); + this.field_146583_f = new GuiTextField(2, this.fontRendererObj, this.width / 2 - 100, 60, 200, 20); + this.field_146583_f.setFocused(true); + this.field_146583_f.setText(s); + } + + public void onGuiClosed() + { + Keyboard.enableRepeatEvents(false); + } + + protected void actionPerformed(GuiButton button) throws IOException + { + if (button.enabled) + { + if (button.id == 1) + { + this.mc.displayGuiScreen(this.field_146585_a); + } + else if (button.id == 0) + { + File folder = new File("./replay_recordings/"); + folder.mkdirs(); + File initRenamed = new File(folder, this.field_146583_f.getText().trim()+ConnectionEventHandler.ZIP_FILE_EXTENSION.replaceAll("[^a-zA-Z0-9\\.\\-]", "_")); + File renamed = initRenamed; + int i=1; + while(renamed.isFile()) { + renamed = new File(initRenamed.getAbsolutePath()+"_"+i); + i++; + } + file.renameTo(renamed); + this.mc.displayGuiScreen(this.field_146585_a); + } + } + } + + protected void keyTyped(char typedChar, int keyCode) throws IOException + { + this.field_146583_f.textboxKeyTyped(typedChar, keyCode); + ((GuiButton)this.buttonList.get(0)).enabled = this.field_146583_f.getText().trim().length() > 0; + + if (keyCode == 28 || keyCode == 156) + { + this.actionPerformed((GuiButton)this.buttonList.get(0)); + } + } + + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException + { + super.mouseClicked(mouseX, mouseY, mouseButton); + this.field_146583_f.mouseClicked(mouseX, mouseY, mouseButton); + } + + public void drawScreen(int mouseX, int mouseY, float partialTicks) + { + this.drawDefaultBackground(); + this.drawCenteredString(this.fontRendererObj, I18n.format("Rename World", new Object[0]), this.width / 2, 20, 16777215); + this.drawString(this.fontRendererObj, I18n.format("Replay Name", new Object[0]), this.width / 2 - 100, 47, 10526880); + this.field_146583_f.drawTextBox(); + super.drawScreen(mouseX, mouseY, partialTicks); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayListEntry.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayListEntry.java new file mode 100644 index 00000000..b218bd57 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayListEntry.java @@ -0,0 +1,92 @@ +package eu.crushedpixel.replaymod.gui; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiListExtended.IGuiListEntry; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import eu.crushedpixel.replaymod.recording.ReplayMetaData; + +public class GuiReplayListEntry implements IGuiListEntry { + + private Minecraft minecraft = Minecraft.getMinecraft(); + private final DateFormat dateFormat = new SimpleDateFormat(); + + private ReplayMetaData metaData; + private String fileName; + + + + public ReplayMetaData getMetaData() { + return metaData; + } + + public void setMetaData(ReplayMetaData metaData) { + this.metaData = metaData; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + private boolean selected = false; + private GuiReplayListExtended parent; + + public GuiReplayListEntry(GuiReplayListExtended parent, String fileName, ReplayMetaData metaData) { + this.metaData = metaData; + this.fileName = fileName; + this.parent = parent; + } + + @Override + public void drawEntry(int slotIndex, int x, int y, int listWidth, int slotHeight, int mouseX, int mouseY, boolean isSelected) + { + minecraft.fontRendererObj.drawString(fileName, x + 3, y + 1, 16777215); + + List list = new ArrayList(); + list.add(metaData.getServerName()+" ("+dateFormat.format(new Date(metaData.getDate()))+")"); + + list.add(String.format("%02dm%02ds", + TimeUnit.MILLISECONDS.toMinutes(metaData.getDuration()), + TimeUnit.MILLISECONDS.toSeconds(metaData.getDuration()) - + TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(metaData.getDuration())) + )); + + for (int l1 = 0; l1 < Math.min(list.size(), 2); ++l1) { + minecraft.fontRendererObj.drawString((String)list.get(l1), x + 3, y + 12 + minecraft.fontRendererObj.FONT_HEIGHT * l1, 8421504); + } + } + + @Override + public void setSelected(int p_178011_1_, int p_178011_2_, int p_178011_3_) {} + + @Override + public boolean mousePressed(int p_148278_1_, int p_148278_2_, + int p_148278_3_, int p_148278_4_, int p_148278_5_, int p_148278_6_) { + for(int slot = 0; slot this.bottom || k1 + l1 < this.top) + { + this.func_178040_a(j1, p_148120_1_, k1); + } + + if (this.showSelectionBox && selected == j1) + { + int i2 = this.left + (this.width / 2 - this.getListWidth() / 2); + int j2 = this.left + this.width / 2 + this.getListWidth() / 2; + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.disableTexture2D(); + worldrenderer.startDrawingQuads(); + worldrenderer.setColorOpaque_I(8421504); + worldrenderer.addVertexWithUV((double)i2, (double)(k1 + l1 + 2), 0.0D, 0.0D, 1.0D); + worldrenderer.addVertexWithUV((double)j2, (double)(k1 + l1 + 2), 0.0D, 1.0D, 1.0D); + worldrenderer.addVertexWithUV((double)j2, (double)(k1 - 2), 0.0D, 1.0D, 0.0D); + worldrenderer.addVertexWithUV((double)i2, (double)(k1 - 2), 0.0D, 0.0D, 0.0D); + worldrenderer.setColorOpaque_I(0); + worldrenderer.addVertexWithUV((double)(i2 + 1), (double)(k1 + l1 + 1), 0.0D, 0.0D, 1.0D); + worldrenderer.addVertexWithUV((double)(j2 - 1), (double)(k1 + l1 + 1), 0.0D, 1.0D, 1.0D); + worldrenderer.addVertexWithUV((double)(j2 - 1), (double)(k1 - 1), 0.0D, 1.0D, 0.0D); + worldrenderer.addVertexWithUV((double)(i2 + 1), (double)(k1 - 1), 0.0D, 0.0D, 0.0D); + tessellator.draw(); + GlStateManager.enableTexture2D(); + } + + this.drawSlot(j1, p_148120_1_, k1, l1, p_148120_3_, p_148120_4_); + } + } + + + private List entries = new ArrayList(); + + public void clearEntries() { + entries = new ArrayList(); + } + + public void addEntry(String fileName, ReplayMetaData metaData) { + entries.add(new GuiReplayListEntry(this, fileName, metaData)); + } + + @Override + public GuiReplayListEntry getListEntry(int index) { + return entries.get(index); + } + + @Override + protected int getSize() { + return entries.size(); + } + + + @Override + public void handleMouseInput() + { + if (this.isMouseYWithinSlotBounds(this.mouseY)) + { + if (Mouse.isButtonDown(0)) + { + if (this.initialClickY == -1.0F) + { + int i2 = this.getScrollBarX(); + int i1 = i2 + 6; + + boolean flag = true; + + if (this.mouseY >= this.top && this.mouseY <= this.bottom && this.mouseX <= i1) + { + int i = this.width / 2 - this.getListWidth() / 2; + int j = this.width / 2 + this.getListWidth() / 2; + int k = this.mouseY - this.top - this.headerPadding + (int)this.amountScrolled - 4; + int l = k / this.slotHeight; + + if (this.mouseX >= i && this.mouseX <= j && l >= 0 && k >= 0 && l < this.getSize()) + { + boolean flag1 = l == this.selectedElement && Minecraft.getSystemTime() - this.lastClicked < 250L; + this.elementClicked(l, flag1, this.mouseX, this.mouseY); + this.selectedElement = l; + this.lastClicked = Minecraft.getSystemTime(); + } + else if (this.mouseX >= i && this.mouseX <= j && k < 0) + { + this.func_148132_a(this.mouseX - i, this.mouseY - this.top + (int)this.amountScrolled - 4); + flag = false; + } + + if (this.mouseX >= i2 && this.mouseX <= i1) + { + this.scrollMultiplier = -1.0F; + int j1 = this.func_148135_f(); + + if (j1 < 1) + { + j1 = 1; + } + + int k1 = (int)((float)((this.bottom - this.top) * (this.bottom - this.top)) / (float)this.getContentHeight()); + k1 = MathHelper.clamp_int(k1, 32, this.bottom - this.top - 8); + this.scrollMultiplier /= (float)(this.bottom - this.top - k1) / (float)j1; + } + else + { + this.scrollMultiplier = 1.0F; + } + + if (flag) + { + this.initialClickY = (float)this.mouseY; + } + else + { + this.initialClickY = -2.0F; + } + } + else + { + this.initialClickY = -2.0F; + } + } + else if (this.initialClickY >= 0.0F) + { + this.amountScrolled -= ((float)this.mouseY - this.initialClickY) * this.scrollMultiplier; + this.initialClickY = (float)this.mouseY; + } + } + else + { + this.initialClickY = -1.0F; + } + + int l1 = Mouse.getEventDWheel(); + + if (l1 != 0) + { + if (l1 > 0) + { + l1 = -1; + } + else if (l1 < 0) + { + l1 = 1; + } + + this.amountScrolled += (float)(l1 * this.slotHeight / 2); + } + } + } + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayManager.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayManager.java new file mode 100644 index 00000000..67b31eeb --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplayManager.java @@ -0,0 +1,286 @@ +package eu.crushedpixel.replaymod.gui; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.client.network.NetHandlerLoginClient; +import net.minecraft.client.resources.I18n; +import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.handshake.client.C00Handshake; +import net.minecraft.network.login.client.C00PacketLoginStart; +import net.minecraft.util.Util; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.commons.io.FilenameUtils; +import org.lwjgl.Sys; +import org.lwjgl.input.Keyboard; + +import com.google.gson.Gson; +import com.mojang.realmsclient.util.Pair; + +import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; +import eu.crushedpixel.replaymod.recording.ReplayMetaData; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import eu.crushedpixel.replaymod.replay.ReplaySender; + +public class GuiReplayManager extends GuiScreen implements GuiYesNoCallback { + + private GuiScreen parentScreen; + private GuiButton btnEditServer; + private GuiButton btnSelectServer; + private GuiButton btnDeleteServer; + private String hoveringText; + private boolean initialized; + private GuiReplayListExtended replayGuiList; + private List> replayFileList = new ArrayList>(); + private GuiButton loadButton, folderButton, renameButton, deleteButton, cancelButton, settingsButton; + + private static Gson gson = new Gson(); + private boolean replaying = false; + + private static final int LOAD_BUTTON_ID = 9001; + private static final int FOLDER_BUTTON_ID = 9002; + private static final int RENAME_BUTTON_ID = 9003; + private static final int DELETE_BUTTON_ID = 9004; + private static final int SETTINGS_BUTTON_ID = 9005; + private static final int CANCEL_BUTTON_ID = 9006; + + private boolean delete_file = false; + + private void reloadFiles() { + replayGuiList.clearEntries(); + replayFileList = new ArrayList>(); + + File folder = new File("./replay_recordings/"); + folder.mkdirs(); + + for(File file : folder.listFiles()) { + if(("."+FilenameUtils.getExtension(file.getAbsolutePath())).equals(ConnectionEventHandler.ZIP_FILE_EXTENSION)) { + try { + ZipFile archive = new ZipFile(file); + ZipArchiveEntry recfile = archive.getEntry("recording"+ConnectionEventHandler.TEMP_FILE_EXTENSION); + ZipArchiveEntry metadata = archive.getEntry("metaData"+ConnectionEventHandler.JSON_FILE_EXTENSION); + + InputStream is = archive.getInputStream(metadata); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + + String json = br.readLine(); + + ReplayMetaData metaData = gson.fromJson(json, ReplayMetaData.class); + + replayFileList.add(Pair.of(file, metaData)); + + archive.close(); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + Collections.sort(replayFileList, new FileAgeComparator()); + + for(Pair p : replayFileList) { + replayGuiList.addEntry(FilenameUtils.getBaseName(p.first().getName()), p.second()); + } + } + + public class FileAgeComparator implements Comparator> { + + @Override + public int compare(Pair o1, Pair o2) { + return (int) (o2.second().getDate() - o1.second().getDate()); + } + + } + + public void initGui() + { + replayGuiList = new GuiReplayListExtended(this, this.mc, this.width, this.height, 32, this.height - 64, 36); + Keyboard.enableRepeatEvents(true); + this.buttonList.clear(); + + if (!this.initialized) { + this.initialized = true; + } + else { + this.replayGuiList.setDimensions(this.width, this.height, 32, this.height - 64); + } + + reloadFiles(); + this.createButtons(); + } + + private void createButtons() { + this.buttonList.add(loadButton = new GuiButton(LOAD_BUTTON_ID, this.width / 2 - 154, this.height - 52, 150, 20, I18n.format("Load Replay", new Object[0]))); + this.buttonList.add(folderButton = new GuiButton(FOLDER_BUTTON_ID, this.width / 2 + 4, this.height - 52, 150, 20, I18n.format("Open Replay Folder...", new Object[0]))); + this.buttonList.add(renameButton = new GuiButton(RENAME_BUTTON_ID, this.width / 2 - 154, this.height - 28, 72, 20, I18n.format("Rename", new Object[0]))); + this.buttonList.add(deleteButton = new GuiButton(DELETE_BUTTON_ID, this.width / 2 - 76, this.height - 28, 72, 20, I18n.format("Delete", new Object[0]))); + this.buttonList.add(settingsButton = new GuiButton(SETTINGS_BUTTON_ID, this.width / 2 + 4, this.height - 28, 72, 20, I18n.format("Settings", new Object[0]))); + this.buttonList.add(cancelButton = new GuiButton(CANCEL_BUTTON_ID, this.width / 2 + 4 + 78, this.height - 28, 72, 20, I18n.format("Cancel", new Object[0]))); + setButtonsEnabled(false); + } + + public void handleMouseInput() throws IOException + { + super.handleMouseInput(); + this.replayGuiList.handleMouseInput(); + } + + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException + { + super.mouseClicked(mouseX, mouseY, mouseButton); + this.replayGuiList.mouseClicked(mouseX, mouseY, mouseButton); + } + + protected void mouseReleased(int mouseX, int mouseY, int state) + { + super.mouseReleased(mouseX, mouseY, state); + this.replayGuiList.mouseReleased(mouseX, mouseY, state); + } + + public void drawScreen(int mouseX, int mouseY, float partialTicks) + { + this.hoveringText = null; + this.drawDefaultBackground(); + this.replayGuiList.drawScreen(mouseX, mouseY, partialTicks); + this.drawCenteredString(this.fontRendererObj, I18n.format("Replay Manager", new Object[0]), this.width / 2, 20, 16777215); + super.drawScreen(mouseX, mouseY, partialTicks); + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException + { + if (button.enabled) { + if(button.id == LOAD_BUTTON_ID) { + loadReplay(replayGuiList.selected); + } + else if (button.id == CANCEL_BUTTON_ID) + { + mc.displayGuiScreen(parentScreen); + } + else if (button.id == DELETE_BUTTON_ID) + { + String s = replayGuiList.getListEntry(replayGuiList.selected).getFileName(); + + if (s != null) + { + delete_file = true; + GuiYesNo guiyesno = getYesNoGui(this, s, 1); + this.mc.displayGuiScreen(guiyesno); + } + } + else if(button.id == SETTINGS_BUTTON_ID) { + this.mc.displayGuiScreen(new GuiReplaySettings(this)); + } + else if(button.id == RENAME_BUTTON_ID) + { + File file = replayFileList.get(replayGuiList.selected).first(); + this.mc.displayGuiScreen(new GuiRenameReplay(this, file)); + } + else if(button.id == FOLDER_BUTTON_ID) + { + File file1 = new File("./replay_recordings/"); + file1.mkdirs(); + String s = file1.getAbsolutePath(); + + if (Util.getOSType() == Util.EnumOS.OSX) + { + try + { + Runtime.getRuntime().exec(new String[] {"/usr/bin/open", s}); + return; + } + catch (IOException ioexception1) {} + } + else if (Util.getOSType() == Util.EnumOS.WINDOWS) + { + String s1 = String.format("cmd.exe /C start \"Open file\" \"%s\"", new Object[] {s}); + + try + { + Runtime.getRuntime().exec(s1); + return; + } + catch (IOException ioexception) {} + } + + boolean flag = false; + + try + { + Class oclass = Class.forName("java.awt.Desktop"); + Object object = oclass.getMethod("getDesktop", new Class[0]).invoke((Object)null, new Object[0]); + oclass.getMethod("browse", new Class[] {URI.class}).invoke(object, new Object[] {file1.toURI()}); + } + catch (Throwable throwable) + { + flag = true; + } + + if (flag) + { + Sys.openURL("file://" + s); + } + } + } + } + + public void confirmClicked(boolean result, int id) { + if (this.delete_file) + { + this.delete_file = false; + + if (result) + { + replayFileList.get(replayGuiList.selected).first().delete(); + replayFileList.remove(replayGuiList.selected); + } + + this.mc.displayGuiScreen(this); + } + } + + public static GuiYesNo getYesNoGui(GuiYesNoCallback p_152129_0_, String file, int p_152129_2_) + { + String s1 = I18n.format("Are you sure you want to delete this replay?", new Object[0]); + String s2 = "\'" + file + "\' " + I18n.format("will be lost forever! (A long time!)", new Object[0]); + String s3 = I18n.format("Delete", new Object[0]); + String s4 = I18n.format("Cancel", new Object[0]); + GuiYesNo guiyesno = new GuiYesNo(p_152129_0_, s1, s2, s3, s4, p_152129_2_); + return guiyesno; + } + + public void setButtonsEnabled(boolean b) { + loadButton.enabled = b; + renameButton.enabled = b; + deleteButton.enabled = b; + } + + public void loadReplay(int id) + { + mc.displayGuiScreen((GuiScreen)null); + + try { + ReplayHandler.startReplay(replayFileList.get(id).first()); + } catch(Exception e) { + e.printStackTrace(); + } + + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java new file mode 100644 index 00000000..a1457f4d --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySettings.java @@ -0,0 +1,133 @@ +package eu.crushedpixel.replaymod.gui; + +import java.awt.Color; +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; + +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiOptionButton; +import net.minecraft.client.gui.GuiOptionSlider; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import net.minecraftforge.fml.client.FMLClientHandler; +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.settings.ReplaySettings; + +public class GuiReplaySettings extends GuiScreen { + + private GuiScreen parentGuiScreen; + protected String screenTitle = "Replay Mod Settings"; + + private static final int MAXSIZE_SLIDER_ID = 9003; + private static final int RECORDSERVER_ID = 9004; + private static final int RECORDSP_ID = 9005; + private static final int SEND_CHAT = 9006; + private static final int FORCE_LINEAR = 9007; + + private GuiButton recordServerButton, recordSPButton, sendChatButton, linearButton; + + public GuiReplaySettings(GuiScreen parentGuiScreen) + { + this.parentGuiScreen = parentGuiScreen; + } + + public void initGui() { + this.screenTitle = I18n.format("Replay Mod Settings", new Object[0]); + this.buttonList.clear(); + this.buttonList.add(new GuiButton(200, this.width / 2 - 100, this.height - 27, I18n.format("gui.done", new Object[0]))); + + Map aoptions = ReplayMod.replaySettings.getOptions(); + + int k = 0; + int i = 0; + for (Entry e : aoptions.entrySet()) { + if(e.getKey().equals("Maximum File Size")) { + float minValue = -1; + float maxValue = 10000; + float valueSteps = 1; + + int val = (Integer)e.getValue(); + this.buttonList.add(new GuiSizeLimitOptionSlider(MAXSIZE_SLIDER_ID, this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), 0, 39, 1, val, e.getKey())); + + } else if(e.getKey().equals("Enable Notifications")) { + sendChatButton = new GuiButton(SEND_CHAT, this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), 150, 20, "Enable Notifications: "+onOff((Boolean)e.getValue())); + this.buttonList.add(sendChatButton); + } else if(e.getKey().equals("Record Server")) { + recordServerButton = new GuiButton(RECORDSERVER_ID, this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), 150, 20, "Record Server: "+onOff((Boolean)e.getValue())); + this.buttonList.add(recordServerButton); + } else if(e.getKey().equals("Record Singleplayer")) { + recordSPButton = new GuiButton(RECORDSP_ID, this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), 150, 20, "Record Singleplayer: "+onOff((Boolean)e.getValue())); + this.buttonList.add(recordSPButton); + } else if(e.getKey().equals("Force Linear Movement")) { + linearButton = new GuiButton(FORCE_LINEAR, this.width / 2 - 155 + i % 2 * 160, this.height / 6 + 24 * (i >> 1), 150, 20, "Camera Path: "+linearOnOff((Boolean)e.getValue())); + this.buttonList.add(linearButton); + } + + ++i; + ++k; + } + + if (i % 2 == 1) + { + ++i; + } + } + + private String onOff(boolean on) { + return on ? "ON" : "OFF"; + } + + private String linearOnOff(boolean on) { + return on ? "Linear" : "Cubic"; + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) + { + this.drawDefaultBackground(); + this.drawCenteredString(this.fontRendererObj, "Replay Mod Settings", this.width / 2, 20, 16777215); + if (FMLClientHandler.instance().getClient().thePlayer != null) { + this.drawCenteredString(this.fontRendererObj, "WARNING: These settings are going to be", this.width / 2, 180, Color.RED.getRGB()); + this.drawCenteredString(this.fontRendererObj, "applied the next time you join a world.", this.width / 2, 190, Color.RED.getRGB()); + } + super.drawScreen(mouseX, mouseY, partialTicks); + } + + + protected void actionPerformed(GuiButton button) throws IOException + { + if (button.enabled) + { + switch(button.id) { + case 200: + this.mc.displayGuiScreen(this.parentGuiScreen); + break; + case RECORDSERVER_ID: + boolean enabled = ReplayMod.replaySettings.isEnableRecordingServer(); + enabled = !enabled; + recordServerButton.displayString = "Record Server: "+onOff(enabled); + ReplayMod.replaySettings.setEnableRecordingServer(enabled); + break; + case RECORDSP_ID: + enabled = ReplayMod.replaySettings.isEnableRecordingSingleplayer(); + enabled = !enabled; + recordSPButton.displayString = "Record Singleplayer: "+onOff(enabled); + ReplayMod.replaySettings.setEnableRecordingSingleplayer(enabled); + break; + case SEND_CHAT: + enabled = ReplayMod.replaySettings.isShowNotifications(); + enabled = !enabled; + sendChatButton.displayString = "Enable Notifications: "+onOff(enabled); + ReplayMod.replaySettings.setShowNotifications(enabled); + break; + case FORCE_LINEAR: + enabled = ReplayMod.replaySettings.isLinearMovement(); + enabled = !enabled; + linearButton.displayString = "Camera Path: "+linearOnOff(enabled); + ReplayMod.replaySettings.setLinearMovement(enabled); + break; + } + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySpeedSlider.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySpeedSlider.java new file mode 100644 index 00000000..bb32ec4e --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiReplaySpeedSlider.java @@ -0,0 +1,189 @@ +package eu.crushedpixel.replaymod.gui; + +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.util.MathHelper; +import net.minecraftforge.fml.client.FMLClientHandler; + +public class GuiReplaySpeedSlider extends GuiButton { + + private float sliderValue; + + private float valueStep, valueMin, valueMax; + private String displayKey; + private boolean dragging = false; + + public GuiReplaySpeedSlider(int buttonId, int p_i45017_2_, int p_i45017_3_, String displayKey) + { + super(buttonId, p_i45017_2_, p_i45017_3_, 150, 20, ""); + sliderValue = (9f/38f); + + this.width = 100; + this.valueMin = 1; + this.valueMax = 38; + this.valueStep = 1; + this.displayString = displayKey+": 1x"; + this.displayKey = displayKey; + + Minecraft.getMinecraft().getTextureManager().bindTexture(buttonTextures); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + this.drawTexturedModalRect(this.xPosition + (int)(sliderValue * (float)(this.width - 8)), this.yPosition, 0, 66, 4, 20); + this.drawTexturedModalRect(this.xPosition + (int)(sliderValue * (float)(this.width - 8)) + 4, this.yPosition, 196, 66, 4, 20); + } + + @Override + protected int getHoverState(boolean mouseOver) { + return 0; + } + + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) + { + if (this.visible) + { + try { + FontRenderer fontrenderer = mc.fontRendererObj; + mc.getTextureManager().bindTexture(buttonTextures); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + this.hovered = mouseX >= this.xPosition && mouseY >= this.yPosition && mouseX < this.xPosition + this.width && mouseY < this.yPosition + this.height; + int k = this.getHoverState(this.hovered); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(770, 771, 1, 0); + GlStateManager.blendFunc(770, 771); + this.drawTexturedModalRect(this.xPosition, this.yPosition, 0, 46 + k * 20, this.width / 2, this.height); + this.drawTexturedModalRect(this.xPosition + this.width / 2, this.yPosition, 200 - this.width / 2, 46 + k * 20, this.width / 2, this.height); + this.mouseDragged(mc, mouseX, mouseY); + int l = 14737632; + + if (packedFGColour != 0) + { + l = packedFGColour; + } + else if (!this.enabled) + { + l = 10526880; + } + else if (this.hovered && FMLClientHandler.instance().isGUIOpen(GuiMouseInput.class)) + { + l = 16777120; + } + + this.drawCenteredString(fontrenderer, this.displayString, this.xPosition + this.width / 2, this.yPosition + (this.height - 8) / 2, l); + } catch(Exception e) {} + } + } + + private String translate(float f) { + return f+"x"; + } + + public static float convertScaleRet(float value) { + if(value <= 1) { + return Math.round(value*10); + } + float steps = value-1; + return Math.round(steps/0.25f); + } + + public static float convertScale(float value) { + if(value == 10) { + return 1; + } + if(value <= 9) { + return value/10f; + } + return 1+(0.25f*(value-10)); + } + + + public double getSliderValue() { + return convertScale(normalizedToReal(sliderValue)); + } + + @Override + protected void mouseDragged(Minecraft mc, int mouseX, int mouseY) { + if (this.visible) { + try { + if(this.dragging) { + sliderValue = (float)(mouseX - (this.xPosition + 4)) / (float)(this.width - 8); + sliderValue = MathHelper.clamp_float(sliderValue, 0.0F, 1.0F); + float f = denormalizeValue(sliderValue); + sliderValue = normalizeValue(f); + if(ReplayHandler.getSpeed() != 0) { + ReplayHandler.setSpeed(convertScale(normalizedToReal(sliderValue))); + } + this.displayString = displayKey+": "+translate(convertScale(normalizedToReal(sliderValue))); + } + + mc.getTextureManager().bindTexture(buttonTextures); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + this.drawTexturedModalRect(this.xPosition + (int)(sliderValue * (float)(this.width - 8)), this.yPosition, 0, 66, 4, 20); + this.drawTexturedModalRect(this.xPosition + (int)(sliderValue * (float)(this.width - 8)) + 4, this.yPosition, 196, 66, 4, 20); + } catch(Exception e) {} + } + } + + public float normalizeValue(float p_148266_1_) + { + return MathHelper.clamp_float((this.snapToStepClamp(p_148266_1_) - this.valueMin) / (this.valueMax - this.valueMin), 0.0F, 1.0F); + } + + public float realToNormalized(float value) { + float min = 0 - valueMin; + float max = valueMax + min; + + return value/(max) - min; + } + + public float denormalizeValue(float p_148262_1_) + { + return this.snapToStepClamp(this.valueMin + (this.valueMax - this.valueMin) * MathHelper.clamp_float(p_148262_1_, 0.0F, 1.0F)); + } + + public float snapToStepClamp(float p_148268_1_) + { + p_148268_1_ = this.snapToStep(p_148268_1_); + return MathHelper.clamp_float(p_148268_1_, this.valueMin, this.valueMax); + } + + protected float snapToStep(float p_148264_1_) + { + if (this.valueStep > 0.0F) + { + p_148264_1_ = this.valueStep * (float)Math.round(p_148264_1_ / this.valueStep); + } + + return p_148264_1_; + } + + public float normalizedToReal(float value) { + float min = 0 - valueMin; + float max = valueMax + min; + + return Math.round(value*(max) - min); + } + + public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) + { + if (super.mousePressed(mc, mouseX, mouseY)) + { + this.dragging = true; + return true; + } + else + { + return false; + } + } + + public void mouseReleased(int mouseX, int mouseY) + { + this.dragging = false; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiSizeLimitOptionSlider.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiSizeLimitOptionSlider.java new file mode 100644 index 00000000..9f8406b8 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiSizeLimitOptionSlider.java @@ -0,0 +1,185 @@ +package eu.crushedpixel.replaymod.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.util.MathHelper; +import eu.crushedpixel.replaymod.ReplayMod; + +public class GuiSizeLimitOptionSlider extends GuiButton { + + public GuiSizeLimitOptionSlider(int buttonId, int p_i45017_2_, int p_i45017_3_, float valueMin, float valueMax, float valueStep, int sliderValue, String displayKey) + { + super(buttonId, p_i45017_2_, p_i45017_3_, 150, 20, ""); + this.valueMin = valueMin; + this.valueMax = valueMax; + this.valueStep = valueStep; + this.sliderValue = sliderValue; + this.displayString = displayKey+": "+translate(sliderValue); + this.displayKey = displayKey; + + float val = realToNormalized(convertScaleRet(sliderValue)); + + + this.sliderValue = val; + } + + private float sliderValue; + public boolean dragging; + private GameSettings.Options options; + + private float valueStep, valueMin, valueMax; + private String displayKey; + + + protected int getHoverState(boolean mouseOver) { + return 0; + } + + + @Override + protected void mouseDragged(Minecraft mc, int mouseX, int mouseY) { + if (this.visible) { + if (this.dragging) { + sliderValue = (float)(mouseX - (this.xPosition + 4)) / (float)(this.width - 8); + sliderValue = MathHelper.clamp_float(sliderValue, 0.0F, 1.0F); + float f = denormalizeValue(sliderValue); + sliderValue = normalizeValue(f); + this.displayString = displayKey+": "+translate(convertScale(normalizedToReal(sliderValue))); + ReplayMod.replaySettings.setMaximumFileSize(convertScale(normalizedToReal(sliderValue))); + } + + mc.getTextureManager().bindTexture(buttonTextures); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + this.drawTexturedModalRect(this.xPosition + (int)(sliderValue * (float)(this.width - 8)), this.yPosition, 0, 66, 4, 20); + this.drawTexturedModalRect(this.xPosition + (int)(sliderValue * (float)(this.width - 8)) + 4, this.yPosition, 196, 66, 4, 20); + } + } + + public static String translate(int value) { + if(value == 0) { + return "Unlimited"; + } else { + return convertToStringRepresentation(value); + } + } + + public static int convertScale(float value) { + if(value == 0) { + return 0; + } else { + if(value > 20) { + value = 1024+((value-20)*1024); + } else { + value = value*50; + } + if(value == 1000) { + value = 1024; + } + return Math.round(value); + } + } + + public static int convertScaleRet(float value) { + if(value == 0) { + return 0; + } else { + if(value >= 1024) { + value = 20+((value-1024)/1024); + } else { + value = value/50; + } + if(value == 1024) { + value = 1000; + } + return Math.round(value); + } + } + + public static String convertToStringRepresentation(final long value){ + long M = 1; + long G = 1024; + long T = G * 1024; + + final long[] dividers = new long[] { T, G, M}; + final String[] units = new String[] { "TB", "GB", "MB"}; + if(value < 1) + throw new IllegalArgumentException("Invalid file size: " + value); + String result = null; + for(int i = 0; i < dividers.length; i++){ + final long divider = dividers[i]; + if(value >= divider){ + result = format(value, divider, units[i]); + break; + } + } + return result; + } + + private static String format(final long value, + final long divider, + final String unit){ + final double result = + divider > 1 ? (double) value / (double) divider : (double) value; + return String.format("%.0f %s", Double.valueOf(result), unit); + } + + public float normalizedToReal(float value) { + float min = 0 - valueMin; + float max = valueMax + min; + + return Math.round(value*(max) - min); + } + + public float realToNormalized(float value) { + float min = 0 - valueMin; + float max = valueMax + min; + + return value/(max) - min; + } + + public float normalizeValue(float p_148266_1_) + { + return MathHelper.clamp_float((this.snapToStepClamp(p_148266_1_) - this.valueMin) / (this.valueMax - this.valueMin), 0.0F, 1.0F); + } + + public float denormalizeValue(float p_148262_1_) + { + return this.snapToStepClamp(this.valueMin + (this.valueMax - this.valueMin) * MathHelper.clamp_float(p_148262_1_, 0.0F, 1.0F)); + } + + public float snapToStepClamp(float p_148268_1_) + { + p_148268_1_ = this.snapToStep(p_148268_1_); + return MathHelper.clamp_float(p_148268_1_, this.valueMin, this.valueMax); + } + + protected float snapToStep(float p_148264_1_) + { + if (this.valueStep > 0.0F) + { + p_148264_1_ = this.valueStep * (float)Math.round(p_148264_1_ / this.valueStep); + } + + return p_148264_1_; + } + + public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) + { + if (super.mousePressed(mc, mouseX, mouseY)) + { + this.dragging = true; + return true; + } + else + { + return false; + } + } + + public void mouseReleased(int mouseX, int mouseY) + { + this.dragging = false; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/Keyframe.java b/src/main/java/eu/crushedpixel/replaymod/holders/Keyframe.java new file mode 100644 index 00000000..a0f7645e --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/holders/Keyframe.java @@ -0,0 +1,20 @@ +package eu.crushedpixel.replaymod.holders; + +public class Keyframe { + + private int realTimestamp; + + public Keyframe(int realTimestamp) { + this.realTimestamp = realTimestamp; + } + + public int getRealTimestamp() { + return realTimestamp; + } + + public void setRealTimestamp(int realTimestamp) { + this.realTimestamp = realTimestamp; + } + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/KeyframeComparator.java b/src/main/java/eu/crushedpixel/replaymod/holders/KeyframeComparator.java new file mode 100644 index 00000000..309f4e7d --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/holders/KeyframeComparator.java @@ -0,0 +1,16 @@ +package eu.crushedpixel.replaymod.holders; + +import java.util.Comparator; + +import eu.crushedpixel.replaymod.replay.ReplayHandler; + +public class KeyframeComparator implements Comparator { + + @Override + public int compare(Keyframe o1, Keyframe o2) { + if(ReplayHandler.isSelected(o1)) return 1; + if(ReplayHandler.isSelected(o2)) return -1; + return o1.getRealTimestamp()-o2.getRealTimestamp(); + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/Position.java b/src/main/java/eu/crushedpixel/replaymod/holders/Position.java new file mode 100644 index 00000000..0a02ce82 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/holders/Position.java @@ -0,0 +1,57 @@ +package eu.crushedpixel.replaymod.holders; + +public class Position { + + private double x, y, z; + private float pitch, yaw; + + public Position(double x, double y, double z, float pitch, float yaw) { + this.x = x; + this.y = y; + this.z = z; + this.pitch = pitch; + this.yaw = yaw; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public float getPitch() { + return pitch; + } + + public void setPitch(float pitch) { + this.pitch = pitch; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/PositionKeyframe.java b/src/main/java/eu/crushedpixel/replaymod/holders/PositionKeyframe.java new file mode 100644 index 00000000..a4b1ea90 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/holders/PositionKeyframe.java @@ -0,0 +1,20 @@ +package eu.crushedpixel.replaymod.holders; + +public class PositionKeyframe extends Keyframe { + + private Position position; + + public PositionKeyframe(int realTime, Position position) { + super(realTime); + this.position = position; + } + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/TimeInfo.java b/src/main/java/eu/crushedpixel/replaymod/holders/TimeInfo.java new file mode 100644 index 00000000..a1583a9f --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/holders/TimeInfo.java @@ -0,0 +1,100 @@ +package eu.crushedpixel.replaymod.holders; + +/** + * Copyright (c) 2014 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. + **/ +public final class TimeInfo { + + @Override + public String toString() { + return start+" | "+speedSince+" | "+speed+" | "+jumpTo; + } + + public static TimeInfo create() { + long now = System.currentTimeMillis(); + return new TimeInfo(now, now, 1, -1); + } + + public TimeInfo(long start, long speedSince, double speed, long jumpTo) { + this.start = start; + this.speedSince = speedSince; + this.speed = speed; + this.jumpTo = jumpTo; + } + + private final long start; + private final long speedSince; + private final double speed; + private final long jumpTo; + + public long getActualStartTime(long now) { + long realTimePassed = now - speedSince; + long ingameTimePassed = (long) (realTimePassed * this.speed); + return start + realTimePassed - ingameTimePassed; + } + + public long getInGameTimePassed(long now) { + long realTimePassed = now - speedSince; + long ingameTimePassed = (long) (realTimePassed * this.speed); + return speedSince - start + ingameTimePassed; + } + + public TimeInfo updateSpeed(long now, double speed) { + if (isJumping()) { + return new TimeInfo(now-jumpTo, now, speed, -1); + } else { + if (this.speed == speed) { + return this; + } + long start; + if (this.speed == 1) { + start = this.start; + } else { + start = getActualStartTime(now); + } + return new TimeInfo(start, now, speed, -1); + } + } + + public boolean isJumping() { + return jumpTo != -1; + } + + public TimeInfo jumpTo(long jumpTo) { + return new TimeInfo(start, speedSince, speed, jumpTo); + } + + public long getStart() { + return start; + } + + public long getSpeedSince() { + return speedSince; + } + + public double getSpeed() { + return speed; + } + + public long getJumpTo() { + return jumpTo; + } +} \ No newline at end of file diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/TimeKeyframe.java b/src/main/java/eu/crushedpixel/replaymod/holders/TimeKeyframe.java new file mode 100644 index 00000000..bb527812 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/holders/TimeKeyframe.java @@ -0,0 +1,19 @@ +package eu.crushedpixel.replaymod.holders; + +public class TimeKeyframe extends Keyframe { + + private int timestamp; + + public TimeKeyframe(int realTime, int timestamp) { + super(realTime); + this.timestamp = timestamp; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/BasicSpline.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/BasicSpline.java new file mode 100644 index 00000000..58c9dab0 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/BasicSpline.java @@ -0,0 +1,71 @@ +package eu.crushedpixel.replaymod.interpolation; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; + +public abstract class BasicSpline { + public void calcNaturalCubic(List valueCollection, Field val, Collection cubicCollection) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + int num = valueCollection.size()-1; + + double[] gamma = new double[num+1]; + double[] delta = new double[num+1]; + double[] D = new double[num+1]; + + int i; + /* + We solve the equation + [2 1 ] [D[0]] [3(x[1] - x[0]) ] + |1 4 1 | |D[1]| |3(x[2] - x[0]) | + | 1 4 1 | | . | = | . | + | ..... | | . | | . | + | 1 4 1| | . | |3(x[n] - x[n-2])| + [ 1 2] [D[n]] [3(x[n] - x[n-1])] + + by using row operations to convert the matrix to upper triangular + and then back sustitution. The D[i] are the derivatives at the knots. + */ + gamma[0] = 1.0f / 2.0f; + for(i=1; i< num; i++) { + gamma[i] = 1.0f/(4.0f - gamma[i-1]); + } + gamma[num] = 1.0f/(2.0f - gamma[num-1]); + + Double p0 = val.getDouble(valueCollection.get(0)); + Double p1 = val.getDouble(valueCollection.get(1)); + + delta[0] = 3.0f * (p1 - p0) * gamma[0]; + for(i=1; i< num; i++) { + p0 = val.getDouble(valueCollection.get(i-1)); + p1 = val.getDouble(valueCollection.get(i+1)); + delta[i] = (3.0f * (p1 - p0) - delta[i - 1]) * gamma[i]; + } + p0 = val.getDouble(valueCollection.get(num-1)); + p1 = val.getDouble(valueCollection.get(num)); + + delta[num] = (3.0f * (p1 - p0) - delta[num - 1]) * gamma[num]; + + D[num] = delta[num]; + for(i=num-1; i >= 0; i--) { + D[i] = delta[i] - gamma[i] * D[i+1]; + } + + //now compute the coefficients of the cubics + cubicCollection.clear(); + + for(i=0; i { + + protected List points = new ArrayList(); + + public abstract K getPoint(float position); + + public void addPoint(K point) { + points.add(point); + } + + public void clearPoints() { + points = new ArrayList(); + } + + protected Pair> getCurrentPoints(float position) { + if(points.size() == 0) return null; + float pos = position * (points.size()-1); + int cubicNum = Math.round(pos); + float cubicPos = (pos - cubicNum); + + if(cubicNum == points.size()-1) { + cubicNum--; + cubicPos++; + } + + if(cubicNum < 0) { + return new Pair>(cubicPos, new Pair(points.get(cubicNum+1), points.get(cubicNum+1))); + } + return new Pair>(cubicPos, new Pair(points.get(cubicNum), points.get(cubicNum+1))); + } + + protected double getInterpolatedValue(double val1, double val2, float perc) { + return val1+((val2-val1)*perc); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearPoint.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearPoint.java new file mode 100644 index 00000000..262ae07b --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearPoint.java @@ -0,0 +1,29 @@ +package eu.crushedpixel.replaymod.interpolation; + +import akka.japi.Pair; +import eu.crushedpixel.replaymod.holders.Position; + +public class LinearPoint extends LinearInterpolation { + + @Override + public Position getPoint(float position) { + Pair> pair = getCurrentPoints(position); + if(pair == null) return null; + + float perc = pair.first(); + + Position first = pair.second().first(); + Position second = pair.second().second(); + + double x = getInterpolatedValue(first.getX(), second.getX(), perc); + double y = getInterpolatedValue(first.getY(), second.getY(), perc); + double z = getInterpolatedValue(first.getZ(), second.getZ(), perc); + + float pitch = (float)getInterpolatedValue(first.getPitch(), second.getPitch(), perc); + float yaw = (float)getInterpolatedValue(first.getYaw(), second.getYaw(), perc); + + Position inter = new Position(x, y, z, pitch, yaw); + return inter; + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearTimestamp.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearTimestamp.java new file mode 100644 index 00000000..5bc08eec --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearTimestamp.java @@ -0,0 +1,22 @@ +package eu.crushedpixel.replaymod.interpolation; + +import akka.japi.Pair; +import eu.crushedpixel.replaymod.holders.Position; + +public class LinearTimestamp extends LinearInterpolation { + + @Override + public Integer getPoint(float position) { + Pair> pair = getCurrentPoints(position); + if(pair == null) return null; + + float perc = pair.first(); + + int first = pair.second().first(); + int second = pair.second().second(); + + int val = (int)getInterpolatedValue(first, second, perc); + + return val; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/SplinePoint.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/SplinePoint.java new file mode 100644 index 00000000..d851a86c --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/SplinePoint.java @@ -0,0 +1,92 @@ +package eu.crushedpixel.replaymod.interpolation; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Vector; + +import com.sun.javafx.geom.Vec3d; + +import eu.crushedpixel.replaymod.holders.Position; + +public class SplinePoint extends BasicSpline{ + private Vector points; + + private Vector xCubics; + private Vector yCubics; + private Vector zCubics; + private Vector pitchCubics; + private Vector yawCubics; + + private Field vectorX; + private Field vectorY; + private Field vectorZ; + private Field vectorPitch; + private Field vectorYaw; + + private static final Object[] EMPTYOBJ = new Object[] { }; + + public SplinePoint() { + this.points = new Vector(); + + this.xCubics = new Vector(); + this.yCubics = new Vector(); + this.zCubics = new Vector(); + this.pitchCubics = new Vector(); + this.yawCubics = new Vector(); + + try { + vectorX = Position.class.getDeclaredField("x"); + vectorY = Position.class.getDeclaredField("y"); + vectorZ = Position.class.getDeclaredField("z"); + vectorPitch = Position.class.getDeclaredField("pitch"); + vectorYaw = Position.class.getDeclaredField("yaw"); + vectorX.setAccessible(true); + vectorY.setAccessible(true); + vectorZ.setAccessible(true); + vectorPitch.setAccessible(true); + vectorYaw.setAccessible(true); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + } + + public void addPoint(Position point) { + this.points.add(point); + } + + public Vector getPoints() { + return points; + } + + public void calcSpline() { + try { + calcNaturalCubic(points, vectorX, xCubics); + calcNaturalCubic(points, vectorY, yCubics); + calcNaturalCubic(points, vectorZ, zCubics); + calcNaturalCubic(points, vectorPitch, pitchCubics); + calcNaturalCubic(points, vectorYaw, yawCubics); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + + public Position getPoint(float position) { + position = position * xCubics.size(); + int cubicNum = (int)Math.min(xCubics.size()-1, position); + float cubicPos = (position - cubicNum); + + return new Position(xCubics.get(cubicNum).eval(cubicPos), + yCubics.get(cubicNum).eval(cubicPos), + zCubics.get(cubicNum).eval(cubicPos), + (float)pitchCubics.get(cubicNum).eval(cubicPos), + (float)yawCubics.get(cubicNum).eval(cubicPos)); + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java b/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java new file mode 100644 index 00000000..5a039990 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java @@ -0,0 +1,158 @@ +package eu.crushedpixel.replaymod.recording; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; + +import java.io.File; +import java.net.InetSocketAddress; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; + +import net.minecraft.network.NetworkManager; +import net.minecraft.network.Packet; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.network.FMLNetworkEvent.ClientConnectedToServerEvent; +import net.minecraftforge.fml.common.network.FMLNetworkEvent.ClientDisconnectionFromServerEvent; +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.chat.ChatMessageRequests; +import eu.crushedpixel.replaymod.chat.ChatMessageRequests.ChatMessageType; + +public class ConnectionEventHandler { + + private static final String decoderKey = "decoder"; + 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); + public static final String TEMP_FILE_EXTENSION = ".tmcpr"; + public static final String JSON_FILE_EXTENSION = ".json"; + public static final String ZIP_FILE_EXTENSION = ".mcpr"; + + private File currentFile; + private String fileName; + + private static PacketListener packetListener = null; + + private static boolean isRecording = false; + + public static boolean isRecording() { + return isRecording; + } + + public static void insertPacket(Packet packet) { + if(!isRecording || packetListener == null) { + System.out.println("Invalid attempt to insert Packet!"); + return; + } + try { + packetListener.channelRead(null, packet); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @SubscribeEvent + public void onConnectedToServerEvent(ClientConnectedToServerEvent event) { + System.out.println("Connected to server"); + + ChatMessageRequests.initialize(); + + try { + if(event.isLocal) { + if(!ReplayMod.replaySettings.isEnableRecordingSingleplayer()) { + System.out.println("Singleplayer Recording is disabled"); + return; + } + } else { + if(!ReplayMod.replaySettings.isEnableRecordingServer()) { + System.out.println("Multiplayer Recording is disabled"); + return; + } + } + NetworkManager nm = event.manager; + String worldName = ""; + if(!event.isLocal) { + worldName = ((InetSocketAddress)nm.getRemoteAddress()).getHostName(); + } + Channel channel = nm.channel(); + ChannelPipeline pipeline = channel.pipeline(); + + List channelHandlerKeys = new ArrayList(); + Iterator> it = pipeline.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + channelHandlerKeys.add(entry.getKey()); + } + + File folder = new File("./replay_recordings/"); + folder.mkdirs(); + + fileName = sdf.format(Calendar.getInstance().getTime()); + currentFile = new File(folder, fileName+TEMP_FILE_EXTENSION); + + currentFile.createNewFile(); + + int maxFileSize = ReplayMod.replaySettings.getMaximumFileSize(); + + PacketListener insert = null; + + if(event.isLocal) { + pipeline.addBefore(packetHandlerKey, "replay_recorder", insert = new PacketListener(currentFile, fileName, worldName, System.currentTimeMillis(), maxFileSize)); + ChatMessageRequests.addChatMessage("Recording started!", ChatMessageType.INFORMATION); + isRecording = true; + + } else { + //pipeline.addBefore(decoderKey, "replay_recorder", insert = new DataListener(currentFile, fileName, worldName, System.currentTimeMillis(), maxFileSize)); + pipeline.addBefore(packetHandlerKey, "replay_recorder", insert = new PacketListener(currentFile, fileName, worldName, System.currentTimeMillis(), maxFileSize)); + ChatMessageRequests.addChatMessage("Recording started!", ChatMessageType.INFORMATION); + isRecording = true; + } + + final PacketListener listener = insert; + + if(insert != null && event.isLocal) { + new Thread(new Runnable() { + + @Override + public void run() { + String worldName = null; + while(worldName == null) { + try { + worldName = MinecraftServer.getServer().getWorldName(); + listener.setWorldName(worldName); + return; + + } catch(Exception e) { + try { + Thread.sleep(100); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + } + + } + }).start(); + } + + packetListener = listener; + + } catch(Exception e) { + ChatMessageRequests.addChatMessage("Failed to start recording!", ChatMessageType.WARNING); + e.printStackTrace(); + } + } + + @SubscribeEvent + public void onDisconnectedFromServerEvent(ClientDisconnectionFromServerEvent event) { + System.out.println("Disconnected from server"); + isRecording = false; + packetListener = null; + ChatMessageRequests.stop(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java new file mode 100644 index 00000000..80d92668 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java @@ -0,0 +1,184 @@ +package eu.crushedpixel.replaymod.recording; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import net.minecraft.network.Packet; + +import com.google.gson.Gson; + +import eu.crushedpixel.replaymod.chat.ChatMessageRequests; +import eu.crushedpixel.replaymod.chat.ChatMessageRequests.ChatMessageType; + +public class DataListener extends ChannelInboundHandlerAdapter { + + protected File file; + protected long startTime; + protected long maxSize; + protected long totalBytes = 0; + protected DataOutputStream out; + protected String name; + protected String worldName; + + protected long lastSentPacket = 0; + + protected boolean alive = true; + protected boolean isPacketListener = false; + + private boolean writeJoinPacket = false; + + private Gson gson = new Gson(); + + public void setWorldName(String worldName) { + this.worldName = worldName; + System.out.println(worldName); + } + + public void insertPacket(Packet p) { + + } + + public DataListener(File file, String name, String worldName, long startTime, int maxSize) throws FileNotFoundException { + this.file = file; + this.startTime = startTime; + this.maxSize = maxSize*1024*1024; + this.name = name; + this.worldName = worldName; + + System.out.println(worldName); + + FileOutputStream fos = new FileOutputStream(file); + BufferedOutputStream bos = new BufferedOutputStream(fos); + out = new DataOutputStream(bos); + + writeJoinPacket = true; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if(!alive || isPacketListener) { + lastSentPacket = System.currentTimeMillis(); + super.channelRead(ctx, msg); + return; + } + + /* + if(writeJoinPacket) { + writeJoinPacket = false; + S01PacketJoinGame p = new S01PacketJoinGame(); + + int timestamp = 0; + + PacketSerializer ps = new PacketSerializer(EnumPacketDirection.CLIENTBOUND); + ByteBuf bb = Unpooled.buffer(); + + PacketBuffer pb = new PacketBuffer(bb); + pb.writeInt(new Random().nextInt(1000)); + pb.writeByte(3); + pb.writeByte(-1); + pb.writeByte(0); + pb.writeByte(100); + pb.writeString("default"); + pb.writeBoolean(false); + + p.readPacketData(pb); + + ByteBuf b2 = Unpooled.buffer(); + ps.encode(ctx, p, b2); + + b2.readerIndex(0); + + byte[] array = new byte[b2.readableBytes()]; + b2.readBytes(array); + + b2.readerIndex(0); + + out.writeInt(timestamp); + out.writeInt(array.length); + out.write(array); + out.flush(); + } + */ + + int timestamp = (int)(System.currentTimeMillis() - startTime); + + ByteBuf byteBuf = (ByteBuf)msg; + + byteBuf.readerIndex(0); + byte[] array = new byte[byteBuf.readableBytes()]; + byteBuf.readBytes(array); + + totalBytes += (array.length + (2*4)); //two Integer values and the Packet size + if(totalBytes >= maxSize && maxSize > 0) { + ChatMessageRequests.addChatMessage("Maximum file size exceeded", ChatMessageType.WARNING); + ChatMessageRequests.addChatMessage("The Recording has been stopped", ChatMessageType.WARNING); + alive = false; + } + + byteBuf.readerIndex(0); + + out.writeInt(timestamp); + out.writeInt(array.length); + out.write(array); + out.flush(); + + super.channelRead(ctx, msg); + } + + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + out.flush(); + out.close(); + + byte[] buffer = new byte[1024]; + + try { + ReplayMetaData metaData = new ReplayMetaData(isPacketListener, worldName, (int) (lastSentPacket - startTime), startTime); + String json = gson.toJson(metaData); + + File folder = new File("./replay_recordings/"); + folder.mkdirs(); + + File archive = new File(folder, name+ConnectionEventHandler.ZIP_FILE_EXTENSION); + archive.createNewFile(); + + FileOutputStream fos = new FileOutputStream(archive); + ZipOutputStream zos = new ZipOutputStream(fos); + + zos.putNextEntry(new ZipEntry("metaData.json")); + PrintWriter pw = new PrintWriter(zos); + pw.write(json); + pw.flush(); + zos.closeEntry(); + + zos.putNextEntry(new ZipEntry("recording"+ConnectionEventHandler.TEMP_FILE_EXTENSION)); + FileInputStream fis = new FileInputStream(file); + int len; + while((len = fis.read(buffer)) > 0) { + zos.write(buffer, 0, len); + } + + fis.close(); + zos.closeEntry(); + + zos.close(); + + file.delete(); + } catch(Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java new file mode 100644 index 00000000..76ce3ee6 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java @@ -0,0 +1,104 @@ +package eu.crushedpixel.replaymod.recording; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; + +import java.io.File; +import java.io.FileNotFoundException; +import java.lang.reflect.Field; + +import net.minecraft.entity.DataWatcher; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.Packet; +import net.minecraft.network.play.server.S0CPacketSpawnPlayer; +import net.minecraft.network.play.server.S0FPacketSpawnMob; +import eu.crushedpixel.replaymod.chat.ChatMessageRequests; +import eu.crushedpixel.replaymod.chat.ChatMessageRequests.ChatMessageType; +import eu.crushedpixel.replaymod.reflection.MCPNames; + +public class PacketListener extends DataListener { + + public PacketListener(File file, String name, String worldName, long startTime, int maxSize) throws FileNotFoundException { + super(file, name, worldName, startTime, maxSize); + isPacketListener = true; + } + + private ChannelHandlerContext context = null; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if(ctx == null) { + if(context == null) { + return; + } else { + ctx = context; + } + } + this.context = ctx; + if(!alive) { + super.channelRead(ctx, msg); + return; + } + if(msg instanceof Packet) { + try { + Packet packet = (Packet)msg; + + int timestamp = (int)(System.currentTimeMillis() - startTime); + + //Converts the packet back to a ByteBuffer for correct saving + + PacketSerializer ps = new PacketSerializer(EnumPacketDirection.CLIENTBOUND); + + ByteBuf bb = Unpooled.buffer(); + if(packet instanceof S0FPacketSpawnMob) { + Field field_149043_l = S0FPacketSpawnMob.class.getDeclaredField(MCPNames.field("field_149043_l")); + field_149043_l.setAccessible(true); + DataWatcher l = (DataWatcher)field_149043_l.get(packet); + DataWatcher dw = new DataWatcher(null); + if(l == null) { + field_149043_l.set(packet, dw); + } + } + + if(packet instanceof S0CPacketSpawnPlayer) { + Field field_149043_l = S0CPacketSpawnPlayer.class.getDeclaredField(MCPNames.field("field_148960_i")); + field_149043_l.setAccessible(true); + DataWatcher l = (DataWatcher)field_149043_l.get(packet); + DataWatcher dw = new DataWatcher(null); + if(l == null) { + field_149043_l.set(packet, dw); + } + } + + ps.encode(ctx, packet, bb); + + bb.readerIndex(0); + byte[] array = new byte[bb.readableBytes()]; + bb.readBytes(array); + + + totalBytes += (array.length + (2*4)); //two Integer values and the Packet size + if(totalBytes >= maxSize && maxSize > 0) { + ChatMessageRequests.addChatMessage("Maximum file size exceeded", ChatMessageType.WARNING); + ChatMessageRequests.addChatMessage("The Recording has been stopped", ChatMessageType.WARNING); + alive = false; + } + + bb.readerIndex(0); + + out.writeInt(timestamp); //Timestamp + out.writeInt(array.length); //Lenght + out.write(array); + out.flush(); + + } catch(Exception e) { + e.printStackTrace(); + } + + } + + super.channelRead(ctx, msg); + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/PacketSerializer.java b/src/main/java/eu/crushedpixel/replaymod/recording/PacketSerializer.java new file mode 100644 index 00000000..59d6b55f --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/recording/PacketSerializer.java @@ -0,0 +1,66 @@ +package eu.crushedpixel.replaymod.recording; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; + +import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.Packet; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.play.server.S0CPacketSpawnPlayer; +import net.minecraft.util.MessageSerializer; + +public class PacketSerializer extends MessageSerializer { + + public PacketSerializer(EnumPacketDirection direction) { + super(direction); + } + + @Override + public void encode(ChannelHandlerContext p_encode_1_, Packet p_encode_2_, ByteBuf p_encode_3_) throws IOException { + //super.encode(p_encode_1_, p_encode_2_, p_encode_3_); + + Integer integer = ((EnumConnectionState)p_encode_1_.channel().attr(NetworkManager.attrKeyConnectionState).get()).getPacketId(EnumPacketDirection.CLIENTBOUND, p_encode_2_); + + if (integer == null) + { + return; + } + else + { + PacketBuffer packetbuffer = new PacketBuffer(p_encode_3_); + packetbuffer.writeVarIntToBuffer(integer.intValue()); + + try + { + if (p_encode_2_ instanceof S0CPacketSpawnPlayer) + { + //p_encode_2_ = p_encode_2_; FML: Kill warning + } + + p_encode_2_.writePacketData(packetbuffer); + } + catch (Throwable throwable) + { + throwable.printStackTrace(); + } + } + } + + public static ByteBuf toByteBuf(byte[] bytes) throws IOException, ClassNotFoundException { + ByteBuf bb; + bb = Unpooled.buffer(bytes.length); + bb.writeBytes(bytes); + + return bb; + } + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/ReplayMetaData.java b/src/main/java/eu/crushedpixel/replaymod/recording/ReplayMetaData.java new file mode 100644 index 00000000..a7b5f7a6 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/recording/ReplayMetaData.java @@ -0,0 +1,45 @@ +package eu.crushedpixel.replaymod.recording; + +import java.util.Date; +import java.util.List; + +public class ReplayMetaData { + + private boolean singleplayer; + private String serverName; + private int duration; + private long date; + + + public ReplayMetaData(boolean singleplayer, String serverName, + int duration, long date) { + this.singleplayer = singleplayer; + this.serverName = serverName; + this.duration = duration; + this.date = date; + } + public boolean isSingleplayer() { + return singleplayer; + } + public void setSingleplayer(boolean singleplayer) { + this.singleplayer = singleplayer; + } + public String getServerName() { + return serverName; + } + public void setServer_name(String serverName) { + this.serverName = serverName; + } + public int getDuration() { + return duration; + } + public void setDuration(int duration) { + this.duration = duration; + } + public long getDate() { + return date; + } + public void setDate(long date) { + this.date = date; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/reflection/MCPEnvironment.java b/src/main/java/eu/crushedpixel/replaymod/reflection/MCPEnvironment.java new file mode 100644 index 00000000..2b54ec31 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/reflection/MCPEnvironment.java @@ -0,0 +1,24 @@ +package eu.crushedpixel.replaymod.reflection; + +import java.lang.reflect.Field; + +import net.minecraft.client.gui.GuiMainMenu; + +public class MCPEnvironment { + + boolean eclipse = true; + + public MCPEnvironment() { + eclipse = true; + Class clazz = GuiMainMenu.class; + try { + Field viewportTexture = clazz.getDeclaredField("viewportTexture"); + } catch(Exception e) { + eclipse = false; + } + } + + public boolean isMCPEnvironment() { + return eclipse; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/reflection/MCPNames.java b/src/main/java/eu/crushedpixel/replaymod/reflection/MCPNames.java new file mode 100644 index 00000000..7b9b3110 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/reflection/MCPNames.java @@ -0,0 +1,269 @@ +package eu.crushedpixel.replaymod.reflection; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; + +import com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.io.Files; +import com.google.common.io.LineProcessor; + +/** + *

A helper class for working with obfuscated field names.

+ *

In the development environment the mappings file will automatically loaded. You can provide the location of a custom mappings file by + * providing the system property {@code sevencommons.mappingsFile}.

+ * @author diesieben07 + * @author CrushedPixel + */ +public final class MCPNames { + + private static final Map fields; + private static final Map methods; + private static final MCPEnvironment env = new MCPEnvironment(); + + static { + if (use()) { + String mappingsDir = "./../build/unpacked/mappings/"; + ResourceLocation fieldsLocation = new ResourceLocation("assets/replaymod/fields.csv"); + ResourceLocation methodsLocation = new ResourceLocation("assets/replaymod/methods.csv"); + + InputStream fieldsIs = MCPNames.class.getClassLoader().getResourceAsStream("fields.csv"); + InputStream methodsIs = MCPNames.class.getClassLoader().getResourceAsStream("methods.csv"); + + fields = readMappings(fieldsIs); + methods = readMappings(methodsIs); + + } else { + methods = fields = null; + } + } + + /** + *

Whether the code is running in a development environment or not.

+ * @return true if the code is running in development mode (use MCP instead of SRG names) + */ + public static boolean use() { + return env.isMCPEnvironment(); + } + + /** + *

Get the correct name for the given SRG field based on the context.

+ * @param srg the SRG name for a field + * @return the input if the code is running outside of development mode or the matching MCP name otherwise + */ + public static String field(String srg) { + if (use()) { + String mcp = fields.get(srg); + if (mcp == null) { + // no mapping + return srg; + } + return mcp; + } else { + return srg; + } + } + + /** + *

Get the correct name for the given SRG method based on the context.

+ * @param srg the SRG name for a method + * @return the input if the code is running outside of development mode or the matching MCP name otherwise + */ + public static String method(String srg) { + if (use()) { + String mcp = methods.get(srg); + if (mcp == null) { + // no mapping + return srg; + } + return mcp; + } else { + return srg; + } + } + + private static Map readMappings(InputStream is) { + try { + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + + MCPFileParser fileParser = new MCPFileParser(); + + while(br.ready()) { + fileParser.processLine(br.readLine()); + } + + return fileParser.getResult(); + } catch (IOException e) { + throw new RuntimeException("Couldn't read SRG->MCP mappings", e); + } + } + + private static class MCPFileParser implements LineProcessor> { + + private static final Splitter splitter = Splitter.on(',').trimResults(); + private final Map map = Maps.newHashMap(); + private boolean foundFirst; + + @Override + public boolean processLine(String line) throws IOException { + if (!foundFirst) { + foundFirst = true; + return true; + } + + Iterator splitted = splitter.split(line).iterator(); + try { + String srg = splitted.next(); + String mcp = splitted.next(); + if (!map.containsKey(srg)) { + map.put(srg, mcp); + } + } catch (NoSuchElementException e) { + throw new IOException("Invalid Mappings file!", e); + } + + return true; + } + + @Override + public Map getResult() { + return ImmutableMap.copyOf(map); + } + } + + public static final String M_SPAWN_BABY = "func_75388_i"; + + public static final String F_TARGET_MATE = "field_75391_e"; + + public static final String F_THE_ANIMAL = "field_75390_d"; + + public static final String M_CLONE_PLAYER = "func_71049_a"; + + public static final String M_CONVERT_TO_VILLAGER = "func_82232_p"; + + public static final String M_SET_WORLD_AND_RESOLUTION = "func_73872_a"; + + public static final String F_BUTTON_LIST = "field_73887_h"; + + public static final String F_TAG_LIST = "field_74747_a"; + + public static final String F_TAG_MAP = "field_74784_a"; + + public static final String F_FOV_MODIFIER_HAND_PREV = "field_78506_S"; + + public static final String F_FOV_MODIFIER_HAND = "field_78507_R"; + + public static final String F_TRACKED_ENTITY_IDS = "field_72794_c"; + + public static final String F_MAP_TEXTURE_OBJECTS = "field_110585_a"; + + public static final String F_MY_ENTITY = "field_73132_a"; + + public static final String M_TRY_START_WATCHING_THIS = "func_73117_b"; + + public static final String M_ON_UPDATE = "func_70071_h_"; + + public static final String M_UPDATE_ENTITY = "func_70316_g"; + + public static final String M_DETECT_AND_SEND_CHANGES = "func_75142_b"; + + public static final String F_IS_REMOTE = "field_72995_K"; + + public static final String F_WORLD_OBJ_TILEENTITY = "field_70331_k"; + + public static final String F_WORLD_OBJ_ENTITY = "field_70170_p"; + + public static final String F_TIMER = "field_71428_T"; + + public static final String F_PACKET_CLASS_TO_ID_MAP = "field_73291_a"; + + public static final String M_SEND_PACKET_TO_PLAYER = "func_72567_b"; + + public static final String M_REMOVE_ENTITY = "func_72900_e"; + + public static final String M_WRITE_ENTITY_TO_NBT = "func_70014_b"; + + public static final String M_READ_ENTITY_FROM_NBT = "func_70037_a"; + + public static final String M_WRITE_TO_NBT_TILEENTITY = "func_70310_b"; + + public static final String M_READ_FROM_NBT_TILEENTITY = "func_70307_a"; + + public static final String F_ITEM_DAMAGE = "field_77991_e"; + + public static final String M_REGISTER_EXT_PROPS = "registerExtendedProperties"; + + public static final String M_READ_PACKET_DATA = "func_73267_a"; + + public static final String M_WRITE_PACKET_DATA = "func_73273_a"; + + public static final String M_GET_PACKET_SIZE = "func_73284_a"; + + public static final String F_UNLOCALIZED_NAME_BLOCK = "field_71968_b"; + + public static final String M_SET_HAS_SUBTYPES = "func_77627_a"; + + public static final String F_ICON_STRING = "field_111218_cA"; + + public static final String F_UNLOCALIZED_NAME_ITEM = "field_77774_bZ"; + + public static final String F_TEXTURE_NAME_BLOCK = "field_111026_f"; + + public static final String M_ACTION_PERFORMED = "func_73875_a"; + + public static final String F_Z_LEVEL = "field_73735_i"; + + public static final String M_ADD_SLOT_TO_CONTAINER = "func_75146_a"; + + public static final String M_MERGE_ITEM_STACK = "func_75135_a"; + + public static final String F_CRAFTERS = "field_75149_d"; + + public static final String M_GET_ICON_STRING = "func_111208_A"; + + public static final String M_GET_TEXTURE_NAME = "func_111023_E"; + + public static final String M_NBT_WRITE = "func_74734_a"; + + public static final String M_NBT_LOAD = "func_74735_a"; + + public static final String F_NBT_STRING_DATA = "field_74751_a"; + public static final String F_NBT_BYTE_DATA = "field_74756_a"; + public static final String F_NBT_SHORT_DATA = "field_74752_a"; + public static final String F_NBT_INT_DATA = "field_74748_a"; + public static final String F_NBT_LONG_DATA = "field_74753_a"; + public static final String F_NBT_FLOAT_DATA = "field_74750_a"; + public static final String F_NBT_DOUBLE_DATA = "field_74755_a"; + + public static final String M_SET_TAG = "func_74782_a"; + + public static final String M_NBT_GET_ID = "func_74732_a"; + + public static final String M_ITEMSTACK_WRITE_NBT = "func_77955_b"; + public static final String M_LOAD_ITEMSTACK_FROM_NBT = "func_77949_a"; + + public static final String M_ADD_CRAFTING_TO_CRAFTERS = "func_75132_a"; + + public static final String M_CHECK_HOTBAR_KEYS = "func_82319_a"; + + public static final String M_HANDLE_MOUSE_CLICK = "func_74191_a"; + + public static final String F_GUICONTAINER_THE_SLOT = "field_82320_o"; + + private MCPNames() { } + +} \ No newline at end of file diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/LesserDataWatcher.java b/src/main/java/eu/crushedpixel/replaymod/replay/LesserDataWatcher.java new file mode 100644 index 00000000..c7ce1561 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/replay/LesserDataWatcher.java @@ -0,0 +1,105 @@ +package eu.crushedpixel.replaymod.replay; + +import java.io.IOException; +import java.util.List; + +import scala.actors.threadpool.Arrays; +import net.minecraft.entity.DataWatcher; +import net.minecraft.entity.Entity; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketBuffer; +import net.minecraft.util.Rotations; + +public class LesserDataWatcher extends DataWatcher { + + public LesserDataWatcher(Entity owner) { + super(owner); + } + + @Override + public void addObject(int id, Object object) { + } + + @Override + public void addObjectByDataType(int id, int type) { + } + + @Override + public byte getWatchableObjectByte(int id) { + return 0; + } + + @Override + public short getWatchableObjectShort(int id) { + return 0; + } + + @Override + public int getWatchableObjectInt(int id) { + return 0; + } + + @Override + public float getWatchableObjectFloat(int id) { + return 0; + } + + @Override + public String getWatchableObjectString(int id) { + return null; + } + + @Override + public ItemStack getWatchableObjectItemStack(int id) { + return null; + } + + @Override + public Rotations getWatchableObjectRotations(int id) { + return null; + } + + @Override + public void updateObject(int id, Object newData) { + } + + @Override + public void setObjectWatched(int id) { + } + + @Override + public boolean hasObjectChanged() { + return false; + } + + @Override + public List getChanged() { + return null; + } + + @Override + public void writeTo(PacketBuffer buffer) throws IOException { + } + + @Override + public List getAllWatched() { + return null; + } + + @Override + public void updateWatchedObjectsFromList(List p_75687_1_) { + } + + @Override + public boolean getIsBlank() { + return true; + } + + @Override + public void func_111144_e() { + } + + + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/OpenEmbeddedChannel.java b/src/main/java/eu/crushedpixel/replaymod/replay/OpenEmbeddedChannel.java new file mode 100644 index 00000000..360568c3 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/replay/OpenEmbeddedChannel.java @@ -0,0 +1,34 @@ +package eu.crushedpixel.replaymod.replay; + +import net.minecraft.network.NetworkManager; +import io.netty.channel.ChannelFuture; +import io.netty.channel.embedded.EmbeddedChannel; + +public class OpenEmbeddedChannel extends EmbeddedChannel { + + private boolean ignoreClose = false; + + public OpenEmbeddedChannel(NetworkManager networkManager) { + super(networkManager); + } + + @Override + public boolean finish() { + System.out.println("wanted to finish"); + ignoreClose = true; + return super.finish(); + } + + @Override + public ChannelFuture close() { + if(ignoreClose) { + ignoreClose = false; + return null; + } + return pipeline().close(); + } + + public ChannelFuture manualClose() { + return pipeline().close(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/PacketDeserializer.java b/src/main/java/eu/crushedpixel/replaymod/replay/PacketDeserializer.java new file mode 100644 index 00000000..0e0a1f6b --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/replay/PacketDeserializer.java @@ -0,0 +1,110 @@ +package eu.crushedpixel.replaymod.replay; + +import gnu.trove.iterator.TIntObjectIterator; +import gnu.trove.map.TIntObjectMap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.Packet; +import net.minecraft.network.PacketBuffer; +import net.minecraft.util.MessageDeserializer; + +import com.google.common.collect.BiMap; + +import eu.crushedpixel.replaymod.reflection.MCPNames; + +public class PacketDeserializer extends MessageDeserializer { + + private final EnumPacketDirection direction; + private Field directionMaps; + private EnumConnectionState state; + + public PacketDeserializer(EnumPacketDirection direction) { + super(direction); + this.direction = direction; + try { + directionMaps = EnumConnectionState.class.getDeclaredField(MCPNames.field("field_179247_h")); + directionMaps.setAccessible(true); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } + } + + public Packet getPacket(EnumPacketDirection direction, int packetId) throws InstantiationException, IllegalAccessException + { + Map map = ((Map)directionMaps.get(state)); + BiMap biMap = ((BiMap)map.get(direction)); + if(biMap == null) { + System.out.println("BiMap is null!"); + } + Class oclass = (Class)biMap.get(Integer.valueOf(packetId)); + return oclass == null ? null : (Packet)oclass.newInstance(); + } + + public void setEnumConnectionState(EnumConnectionState state) { + this.state = state; + } + + @Override + public void decode(ChannelHandlerContext p_decode_1_, + ByteBuf p_decode_2_, List p_decode_3_) throws IOException, + InstantiationException, IllegalAccessException { + + if (p_decode_2_.readableBytes() != 0) + { + PacketBuffer packetbuffer = new PacketBuffer(p_decode_2_); + int i = packetbuffer.readVarIntFromBuffer(); + + Field state_by_id = null; + try { + state_by_id = EnumConnectionState.class.getDeclaredField("STATES_BY_ID"); //TODO: Localize + state_by_id.setAccessible(true); + state = (EnumConnectionState)((TIntObjectMap)state_by_id.get(null)).get(i); + TIntObjectMap map = (TIntObjectMap)state_by_id.get(null); + TIntObjectIterator it = map.iterator(); + while(it.hasNext()) { + it.advance(); + System.out.println(it.key() +" | "+it.value().getClass()); + } + } catch(Exception e) { + e.printStackTrace(); + } + + if(state == null) { + System.out.println("state is null"); + } + //Packet packet = getPacket(this.direction, i); + Packet packet = state.getPacket(EnumPacketDirection.CLIENTBOUND, i); + + if (packet == null) + { + throw new IOException("Bad packet id " + i); + } + else + { + packet.readPacketData(packetbuffer); + + if (packetbuffer.readableBytes() > 0) + { + throw new IOException("Packet " + ((EnumConnectionState)p_decode_1_.channel().attr(NetworkManager.attrKeyConnectionState).get()).getId() + "/" + i + " (" + packet.getClass().getSimpleName() + ") was larger than I expected, found " + packetbuffer.readableBytes() + " bytes extra whilst reading packet " + i); + } + else + { + p_decode_3_.add(packet); + } + } + } + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/PacketInfo.java b/src/main/java/eu/crushedpixel/replaymod/replay/PacketInfo.java new file mode 100644 index 00000000..edc7375a --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/replay/PacketInfo.java @@ -0,0 +1,40 @@ +package eu.crushedpixel.replaymod.replay; + +import net.minecraft.network.EnumConnectionState; + +public class PacketInfo { + + private byte[] bytes; + private EnumConnectionState connectionState; + private int packetID; + + + + public PacketInfo(byte[] bytes, EnumConnectionState connectionState, + int packetID) { + super(); + this.bytes = bytes; + this.connectionState = connectionState; + this.packetID = packetID; + } + public byte[] getBytes() { + return bytes; + } + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + public EnumConnectionState getConnectionState() { + return connectionState; + } + public void setConnectionState(EnumConnectionState connectionState) { + this.connectionState = connectionState; + } + public int getPacketID() { + return packetID; + } + public void setPacketID(int packetID) { + this.packetID = packetID; + } + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java new file mode 100644 index 00000000..2d756418 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -0,0 +1,361 @@ +package eu.crushedpixel.replaymod.replay; + +import io.netty.channel.ChannelPipeline; +import io.netty.channel.embedded.EmbeddedChannel; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.play.INetHandlerPlayClient; + +import com.mojang.authlib.GameProfile; + +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.chat.ChatMessageRequests; +import eu.crushedpixel.replaymod.entities.CameraEntity; +import eu.crushedpixel.replaymod.holders.Keyframe; +import eu.crushedpixel.replaymod.holders.KeyframeComparator; +import eu.crushedpixel.replaymod.holders.PositionKeyframe; +import eu.crushedpixel.replaymod.holders.TimeKeyframe; + +public class ReplayHandler { + + private static NetworkManager networkManager; + private static Minecraft mc = Minecraft.getMinecraft(); + private static ReplaySender replaySender; + private static OpenEmbeddedChannel channel; + + private static Keyframe selectedKeyframe; + + private static boolean isReplaying = false; + + private static CameraEntity cameraEntity; + + private static List keyframes = new ArrayList(); + + private static boolean replayActive = false; + + public static long lastExit = 0; + + public static void setReplaying(boolean replaying) { + isReplaying = replaying; + } + + public static void startPath() { + ReplayProcess.startReplayProcess(); + } + + public static void interruptReplay() { + ReplayProcess.stopReplayProcess(false); + } + + public static boolean isReplaying() { + return isReplaying; + } + + public static void setCameraEntity(CameraEntity entity) { + if(entity == null) return; + cameraEntity = entity; + mc.setRenderViewEntity(cameraEntity); + } + + public static CameraEntity getCameraEntity() { + return cameraEntity; + } + + public static int getReplayTime() { + if(replaySender != null) { + return (int)replaySender.currentTimeStamp(); + } + + return 0; + } + + public static void sortKeyframes() { + Collections.sort(keyframes, new KeyframeComparator()); + } + + public static void addKeyframe(Keyframe keyframe) { + keyframes.add(keyframe); + selectKeyframe(keyframe); + } + + public static void removeKeyframe(Keyframe keyframe) { + keyframes.remove(keyframe); + if(keyframe == selectedKeyframe) { + selectKeyframe(null); + } else { + sortKeyframes(); + } + } + + public static int getKeyframeIndex(TimeKeyframe timeKeyframe) { + int index = 0; + for(Keyframe kf : keyframes) { + if(kf == timeKeyframe) return index; + else if(kf instanceof TimeKeyframe) index++; + } + return -1; + } + + public static int getKeyframeIndex(PositionKeyframe posKeyframe) { + int index = 0; + for(Keyframe kf : keyframes) { + if(kf == posKeyframe) return index; + else if(kf instanceof PositionKeyframe) index++; + } + return -1; + } + + public static int getPosKeyframeCount() { + int size = 0; + for(Keyframe kf : keyframes) { + if(kf instanceof PositionKeyframe) size++; + } + return size; + } + + public static int getTimeKeyframeCount() { + int size = 0; + for(Keyframe kf : keyframes) { + if(kf instanceof TimeKeyframe) size++; + } + return size; + } + + public static TimeKeyframe getClosestTimeKeyframeForRealTime(int realTime, int tolerance) { + List found = new ArrayList(); + for(Keyframe kf : keyframes) { + if(!(kf instanceof TimeKeyframe)) continue; + if(Math.abs(kf.getRealTimestamp()-realTime) <= tolerance) { + found.add((TimeKeyframe)kf); + } + } + + TimeKeyframe closest = null; + + for(TimeKeyframe kf : found) { + if(closest == null || Math.abs(closest.getTimestamp()-realTime) > Math.abs(kf.getRealTimestamp()-realTime)) { + closest = kf; + } + } + return closest; + } + + public static PositionKeyframe getClosestPlaceKeyframeForRealTime(int realTime, int tolerance) { + List found = new ArrayList(); + for(Keyframe kf : keyframes) { + if(!(kf instanceof PositionKeyframe)) continue; + if(Math.abs(kf.getRealTimestamp()-realTime) <= tolerance) { + found.add((PositionKeyframe)kf); + } + } + + PositionKeyframe closest = null; + + for(PositionKeyframe kf : found) { + if(closest == null || Math.abs(closest.getRealTimestamp()-realTime) > Math.abs(kf.getRealTimestamp()-realTime)) { + closest = kf; + } + } + return closest; + } + + public static PositionKeyframe getPreviousPositionKeyframe(int realTime) { + if(keyframes.isEmpty()) return null; + List found = new ArrayList(); + for(Keyframe kf : keyframes) { + if(!(kf instanceof PositionKeyframe)) continue; + if(kf.getRealTimestamp() < realTime) { + found.add((PositionKeyframe)kf); + } + } + + if(found.size() > 0) + return found.get(found.size()-1); //last element is nearest + else return null; + } + + public static PositionKeyframe getNextPositionKeyframe(int realTime) { + if(keyframes.isEmpty()) return null; + for(Keyframe kf : keyframes) { + if(!(kf instanceof PositionKeyframe)) continue; + if(kf.getRealTimestamp() >= realTime) { + return (PositionKeyframe)kf; //first found element is next + } + } + return null; + } + + public static TimeKeyframe getPreviousTimeKeyframe(int realTime) { + if(keyframes.isEmpty()) return null; + List found = new ArrayList(); + for(Keyframe kf : keyframes) { + if(!(kf instanceof TimeKeyframe)) continue; + if(kf.getRealTimestamp() < realTime) { + found.add((TimeKeyframe)kf); + } + } + + if(found.size() > 0) + return found.get(found.size()-1); //last element is nearest + else return null; + } + + public static TimeKeyframe getNextTimeKeyframe(int realTime) { + if(keyframes.isEmpty()) return null; + for(Keyframe kf : keyframes) { + if(!(kf instanceof TimeKeyframe)) continue; + if(kf.getRealTimestamp() >= realTime) { + return (TimeKeyframe)kf; //first found element is next + } + } + return null; + } + + public static List getKeyframes() { + return new ArrayList(keyframes); + } + + public static void resetKeyframes() { + keyframes = new ArrayList(); + selectKeyframe(null); + } + + public static void setReplayPos(int pos, boolean force) { + if(replaySender != null) { + replaySender.jumpToTime(pos, force); + } + } + + public static boolean isHurrying() { + if(replaySender != null) { + return replaySender.isHurrying(); + } + return false; + } + + public static int getReplayLength() { + if(replaySender != null) { + return replaySender.replayLength(); + } + + return 1; + } + + public static boolean isSelected(Keyframe kf) { + return kf == selectedKeyframe; + } + + public static void selectKeyframe(Keyframe kf) { + selectedKeyframe = kf; + sortKeyframes(); + } + + public static boolean replayActive() { + return replayActive; + } + + public static boolean isPaused() { + if(replaySender != null) { + return replaySender.paused(); + } + return true; + } + + public static void setSpeed(double d) { + if(replaySender != null) { + replaySender.setReplaySpeed(d); + } + } + + public static double getSpeed() { + if(replaySender != null) { + return replaySender.getReplaySpeed(); + } + return 0; + } + + public static void startReplay(File file) throws NoSuchMethodException, SecurityException, NoSuchFieldException { + + ChatMessageRequests.initialize(); + mc.ingameGUI.getChatGUI().clearChatMessages(); + resetKeyframes(); + + if(replaySender != null) { + replaySender.terminateReplay(); + } + + if(channel != null) { + channel.close(); + } + + networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND); + INetHandlerPlayClient pc = new NetHandlerPlayClient(mc, (GuiScreen)null, networkManager, new GameProfile(UUID.randomUUID(), "Player")); + networkManager.setNetHandler(pc); + + channel = new OpenEmbeddedChannel(networkManager); + + replaySender = new ReplaySender(file, networkManager); + channel.pipeline().addFirst(replaySender); + channel.pipeline().fireChannelActive(); + + try { + ReplayMod.overlay.resetUI(); + } catch(Exception e) {} + + replayActive = true; + } + + public static void restartReplay() { + //mc.setRenderViewEntity(mc.thePlayer); + mc.ingameGUI.getChatGUI().clearChatMessages(); + + if(channel != null) { + channel.close(); + } + + networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND); + INetHandlerPlayClient pc = new NetHandlerPlayClient(mc, (GuiScreen)null, networkManager, new GameProfile(UUID.randomUUID(), "Player")); + networkManager.setNetHandler(pc); + + EmbeddedChannel channel = new OpenEmbeddedChannel(networkManager); + + channel.pipeline().addFirst(replaySender); + channel.pipeline().fireChannelActive(); + + ChannelPipeline pipeline = networkManager.channel().pipeline(); + + try { + ReplayMod.overlay.resetUI(); + } catch(Exception e) {} + + replayActive = true; + } + + public static void endReplay() { + if(replaySender != null) { + replaySender.terminateReplay(); + } + + resetKeyframes(); + + if(channel != null) { + channel.close(); + } + + replayActive = false; + } + + public static Keyframe getSelected() { + return selectedKeyframe; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java new file mode 100644 index 00000000..db9cf6a0 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java @@ -0,0 +1,211 @@ +package eu.crushedpixel.replaymod.replay; + +import net.minecraft.client.Minecraft; +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.chat.ChatMessageRequests; +import eu.crushedpixel.replaymod.chat.ChatMessageRequests.ChatMessageType; +import eu.crushedpixel.replaymod.holders.Keyframe; +import eu.crushedpixel.replaymod.holders.Position; +import eu.crushedpixel.replaymod.holders.PositionKeyframe; +import eu.crushedpixel.replaymod.holders.TimeKeyframe; +import eu.crushedpixel.replaymod.interpolation.LinearPoint; +import eu.crushedpixel.replaymod.interpolation.LinearTimestamp; +import eu.crushedpixel.replaymod.interpolation.SplinePoint; + +public class ReplayProcess { + + private static Minecraft mc = Minecraft.getMinecraft(); + + private static long startRealTime; + private static int lastRealReplayTime; + private static long lastRealTime = 0; + + private static boolean linear = false; + + private static Position lastPosition = null; + private static int lastTimestamp = -1; + + private static SplinePoint motionSpline = null; + private static LinearPoint motionLinear = null; + private static LinearTimestamp timeLinear = null; + + private static double previousReplaySpeed = 0; + + public static void startReplayProcess() { + ChatMessageRequests.initialize(); + if(ReplayHandler.getKeyframes().isEmpty()) { + ChatMessageRequests.addChatMessage("No keyframes set!", ChatMessageType.WARNING); + return; + } + startRealTime = System.currentTimeMillis(); + lastRealTime = startRealTime; + lastRealReplayTime = 0; + lastTimestamp = -1; + lastPosition = null; + motionSpline = null; + timeLinear = null; + linear = ReplayMod.replaySettings.isLinearMovement(); + ReplayHandler.sortKeyframes(); + ReplayHandler.setReplaying(true); + previousReplaySpeed = ReplayHandler.getSpeed(); + TimeKeyframe tf = ReplayHandler.getNextTimeKeyframe(-1); + if(tf != null) { + int ts = tf.getTimestamp(); + ReplayHandler.setReplayPos(ts, true); + } + ChatMessageRequests.addChatMessage("Replay started!", ChatMessageType.INFORMATION); + } + + public static void stopReplayProcess(boolean finished) { + if(finished) ChatMessageRequests.addChatMessage("Replay finished!", ChatMessageType.INFORMATION); + else ChatMessageRequests.addChatMessage("Replay stopped!", ChatMessageType.INFORMATION); + ReplayHandler.setReplaying(false); + ReplayHandler.setSpeed(previousReplaySpeed); + ReplayHandler.setSpeed(0); + } + + public static void tickReplay() { + if(!ReplayHandler.isReplaying()) return; + if(ReplayHandler.isHurrying()) { + lastRealTime = System.currentTimeMillis(); + System.out.println("rethurrn"); + return; + } + + if(!linear && motionSpline == null) { + //set up spline path + motionSpline = new SplinePoint(); + for(Keyframe kf : ReplayHandler.getKeyframes()) { + if(kf instanceof PositionKeyframe) { + PositionKeyframe pkf = (PositionKeyframe)kf; + Position pos = pkf.getPosition(); + motionSpline.addPoint(pos); + } + } + if(motionSpline.getPoints().size() < 3) linear = true; + else motionSpline.calcSpline(); + + } + if(linear && motionLinear == null) { + //set up linear path + motionLinear = new LinearPoint(); + for(Keyframe kf : ReplayHandler.getKeyframes()) { + if(kf instanceof PositionKeyframe) { + PositionKeyframe pkf = (PositionKeyframe)kf; + Position pos = pkf.getPosition(); + motionLinear.addPoint(pos); + } + } + } + if(timeLinear == null) { + timeLinear = new LinearTimestamp(); + for(Keyframe kf : ReplayHandler.getKeyframes()) { + if(kf instanceof TimeKeyframe) { + timeLinear.addPoint(((TimeKeyframe)kf).getTimestamp()); + } + } + + /* + for(float x = 0; x <= 1f; x+=0.1) { + //timeLinear.getPoint(x); + System.out.println(x+" | "+timeLinear.getPoint(x)); + } + */ + //System.out.println(timeLinear.getPoint(0)); + } + + long curTime = System.currentTimeMillis(); + long timeStep = curTime - lastRealTime; + + int curRealReplayTime = (int)(lastRealReplayTime + timeStep); + + PositionKeyframe lastPos = ReplayHandler.getPreviousPositionKeyframe(curRealReplayTime); + PositionKeyframe nextPos = ReplayHandler.getNextPositionKeyframe(curRealReplayTime); + + int lastPosStamp = 0; + int nextPosStamp = 0; + + if(nextPos != null || lastPos != null) { + if(nextPos != null) { + nextPosStamp = nextPos.getRealTimestamp(); + } else { + nextPosStamp = lastPos.getRealTimestamp(); + } + + if(lastPos != null) { + lastPosStamp = lastPos.getRealTimestamp(); + } else { + lastPosStamp = nextPos.getRealTimestamp(); + } + } + + TimeKeyframe lastTime = ReplayHandler.getPreviousTimeKeyframe(curRealReplayTime); + TimeKeyframe nextTime = ReplayHandler.getNextTimeKeyframe(curRealReplayTime); + + int lastTimeStamp = 0; + int nextTimeStamp = 0; + + double curSpeed = 0f; + + if(nextTime != null || lastTime != null) { + 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)((nextTime.getTimestamp()-lastTime.getTimestamp())))/((double)((nextTimeStamp-lastTimeStamp))); + } + } + + int currentDiff = nextPosStamp - lastPosStamp; + int current = curRealReplayTime - lastPosStamp; + + float currentStepPerc = (float)current/(float)currentDiff; //The percentage of the travelled path between the current positions + if(Float.isInfinite(currentStepPerc)) currentStepPerc = 0; + + float splinePos = ((float)ReplayHandler.getKeyframeIndex(lastPos) + currentStepPerc)/(float)(ReplayHandler.getPosKeyframeCount()-1); + float timePos = ((float)ReplayHandler.getKeyframeIndex(lastTime) + currentStepPerc)/(float)(ReplayHandler.getTimeKeyframeCount()-1); + + Position pos = null; + if(!linear) { + pos = motionSpline.getPoint(Math.max(0, Math.min(1, splinePos))); + } else { + pos = motionLinear.getPoint(Math.max(0, Math.min(1, splinePos))); + } + + //int curPos = timeLinear.getPoint(Math.max(0, Math.min(1, timePos))); + + //set replay speed + if(pos != null) ReplayHandler.getCameraEntity().movePath(pos); + //System.out.println(curSpeed+" | "+curPos); + //ReplayHandler.setSpeed(curSpeed); + //System.out.println("sent "+curPos); + //ReplayHandler.setReplayPos(curPos, timePos == 0); + + + //calculate replay speed + + /* + int ts = timeLinear.getPoint(Math.max(0, Math.min(1, splinePos))); + if(ts != lastTimestamp) { + lastTimestamp = ts; + } + */ + + //splinePos = (index of last entry + add) / total entries + + + lastRealReplayTime = curRealReplayTime; + lastRealTime = curTime; + + if(splinePos >= 1) stopReplayProcess(true); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java new file mode 100644 index 00000000..7d15c84a --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java @@ -0,0 +1,528 @@ +package eu.crushedpixel.replaymod.replay; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiDownloadTerrain; +import net.minecraft.client.particle.EffectRenderer; +import net.minecraft.entity.Entity; +import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.Packet; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.play.server.S01PacketJoinGame; +import net.minecraft.network.play.server.S02PacketChat; +import net.minecraft.network.play.server.S06PacketUpdateHealth; +import net.minecraft.network.play.server.S08PacketPlayerPosLook; +import net.minecraft.network.play.server.S0BPacketAnimation; +import net.minecraft.network.play.server.S0CPacketSpawnPlayer; +import net.minecraft.network.play.server.S14PacketEntity; +import net.minecraft.network.play.server.S18PacketEntityTeleport; +import net.minecraft.network.play.server.S1CPacketEntityMetadata; +import net.minecraft.network.play.server.S1DPacketEntityEffect; +import net.minecraft.network.play.server.S28PacketEffect; +import net.minecraft.network.play.server.S29PacketSoundEffect; +import net.minecraft.network.play.server.S2BPacketChangeGameState; +import net.minecraft.network.play.server.S2DPacketOpenWindow; +import net.minecraft.network.play.server.S2EPacketCloseWindow; +import net.minecraft.network.play.server.S2FPacketSetSlot; +import net.minecraft.network.play.server.S30PacketWindowItems; +import net.minecraft.network.play.server.S36PacketSignEditorOpen; +import net.minecraft.network.play.server.S37PacketStatistics; +import net.minecraft.network.play.server.S39PacketPlayerAbilities; +import net.minecraft.network.play.server.S43PacketCamera; +import net.minecraft.network.play.server.S45PacketTitle; +import net.minecraft.network.play.server.S14PacketEntity.S17PacketEntityLookMove; +import net.minecraft.util.Timer; +import net.minecraft.world.EnumDifficulty; +import net.minecraft.world.WorldSettings.GameType; +import net.minecraft.world.WorldType; +import net.minecraftforge.fml.client.FMLClientHandler; +import net.minecraftforge.fml.common.FMLCommonHandler; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.commons.io.FilenameUtils; + +import com.google.gson.Gson; + +import eu.crushedpixel.replaymod.entities.CameraEntity; +import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; +import eu.crushedpixel.replaymod.recording.ReplayMetaData; +import eu.crushedpixel.replaymod.reflection.MCPNames; + +@Sharable +public class ReplaySender extends ChannelInboundHandlerAdapter { + + private long currentTimeStamp; + private boolean hurryToTimestamp; + private long desiredTimeStamp; + private long lastTimeStamp, lastPacketSent; + + private long toleratedTimeStamp; + + private File replayFile; + private PacketDeserializer ds = new PacketDeserializer(EnumPacketDirection.SERVERBOUND); + private boolean active = true; + private ZipFile archive; + private DataInputStream dis; + private ChannelHandlerContext ctx = null; + + private boolean startFromBeginning = true; + + private NetworkManager networkManager; + private boolean terminate = false; + + private float defaultReplaySpeed = 0.5f; + + private int packetAt = 0; + private double replaySpeed = 1f; + private long lastTime = 0, lastTimestamp = 0; + + private Field joinPacketEntityId, joinPacketWorldType, + joinPacketDimension, joinPacketDifficulty, joinPacketMaxPlayers; + + private Field effectPacketEntityId; + private Field metadataPacketEntityId, metadataPacketList; + + private Field animationPacketEntityId; + private Field entityDataWatcher; + + private Field chatPacketPosition; + + private Minecraft mc = Minecraft.getMinecraft(); + private Field mcTimer; + + private long now = System.currentTimeMillis(); + + private int replayLength = 0; + + private EffectRenderer old = mc.effectRenderer; + + private ZipArchiveEntry replayEntry; + + public boolean isHurrying() { + return hurryToTimestamp; + } + + public long currentTimeStamp() { + return currentTimeStamp; + } + + public int replayLength() { + return replayLength; + } + + public void terminateReplay() { + terminate = true; + try { + channelInactive(ctx); + ctx.channel().pipeline().close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void jumpToTime(int millis, boolean forceReload) { + setReplaySpeed(replaySpeed); + + if((millis < currentTimeStamp && !isHurrying()) || forceReload) { + if(forceReload) System.out.println("forced"); + if(ReplayHandler.isReplaying()) { + if(forceReload) { + startFromBeginning = true; + } + else { + startFromBeginning = false; + desiredTimeStamp = millis; + hurryToTimestamp = false; + return; + } + } else { + startFromBeginning = true; + } + } + + desiredTimeStamp = millis; + hurryToTimestamp = true; + } + + public void setReplaySpeed(final double d) { + if(d != 0) this.replaySpeed = d; + + try { + Timer timer = (Timer)mcTimer.get(mc); + timer.timerSpeed = (float)d; + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public ReplaySender(final File replayFile, NetworkManager nm) { + try { + joinPacketEntityId = S01PacketJoinGame.class.getDeclaredField(MCPNames.field("field_149206_a")); + joinPacketEntityId.setAccessible(true); + + joinPacketDifficulty = S01PacketJoinGame.class.getDeclaredField(MCPNames.field("field_149203_e")); + joinPacketDifficulty.setAccessible(true); + + joinPacketDimension = S01PacketJoinGame.class.getDeclaredField(MCPNames.field("field_149202_d")); + joinPacketDimension.setAccessible(true); + + joinPacketMaxPlayers = S01PacketJoinGame.class.getDeclaredField(MCPNames.field("field_149200_f")); + joinPacketMaxPlayers.setAccessible(true); + + joinPacketWorldType = S01PacketJoinGame.class.getDeclaredField(MCPNames.field("field_149201_g")); + joinPacketWorldType.setAccessible(true); + + mcTimer = Minecraft.class.getDeclaredField(MCPNames.field("field_71428_T")); + mcTimer.setAccessible(true); + + effectPacketEntityId = S1DPacketEntityEffect.class.getDeclaredField(MCPNames.field("field_149434_a")); + effectPacketEntityId.setAccessible(true); + + metadataPacketEntityId = S1CPacketEntityMetadata.class.getDeclaredField(MCPNames.field("field_149379_a")); + metadataPacketEntityId.setAccessible(true); + + metadataPacketList = S1CPacketEntityMetadata.class.getDeclaredField(MCPNames.field("field_149378_b")); + metadataPacketList.setAccessible(true); + + animationPacketEntityId = S0BPacketAnimation.class.getDeclaredField(MCPNames.field("field_148981_a")); + animationPacketEntityId.setAccessible(true); + + entityDataWatcher = Entity.class.getDeclaredField(MCPNames.field("field_70180_af")); + entityDataWatcher.setAccessible(true); + + chatPacketPosition = S02PacketChat.class.getDeclaredField(MCPNames.field("field_179842_b")); + chatPacketPosition.setAccessible(true); + + } catch (Exception e) { + e.printStackTrace(); + } + + this.replayFile = replayFile; + this.networkManager = nm; + if(("."+FilenameUtils.getExtension(replayFile.getAbsolutePath())).equals(ConnectionEventHandler.ZIP_FILE_EXTENSION)) { + try { + archive = new ZipFile(replayFile); + replayEntry = archive.getEntry("recording"+ConnectionEventHandler.TEMP_FILE_EXTENSION); + + ZipArchiveEntry metadata = archive.getEntry("metaData"+ConnectionEventHandler.JSON_FILE_EXTENSION); + InputStream is = archive.getInputStream(metadata); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + + String json = br.readLine(); + + ReplayMetaData metaData = new Gson().fromJson(json, ReplayMetaData.class); + + this.replayLength = metaData.getDuration(); + + sender.start(); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + private Thread sender = new Thread(new Runnable() { + + @Override + public void run() { + + try { + dis = new DataInputStream(archive.getInputStream(replayEntry)); + } catch(Exception e) { + e.printStackTrace(); + } + + try { + while(ctx == null && !terminate) { + Thread.sleep(10); + } + while(!terminate) { + if(startFromBeginning) { + currentTimeStamp = 0; + dis = new DataInputStream(archive.getInputStream(replayEntry)); + startFromBeginning = false; + ReplayHandler.restartReplay(); + } + while(!startFromBeginning && (!paused() || FMLClientHandler.instance().isGUIOpen(GuiDownloadTerrain.class))) { + try { + + /* + * LOGIC: + * While behind desired timestamp, only send packets + * until desired timestamp is reached, + * then increase desired timestamp by 1/20th of a second + * + * Desired timestamp is divided through stretch factor. + * + * If hurrying, don't wait for correct timing. + */ + + if(!hurryToTimestamp && ReplayHandler.isReplaying()) { + continue; + } else if(ReplayHandler.isReplaying()) { + System.out.println("nocont "+currentTimeStamp+" | "+((Timer)mcTimer.get(mc)).timerSpeed); + } + + int timestamp = dis.readInt(); + + currentTimeStamp = timestamp; + + if(!ReplayHandler.isReplaying() && !hurryToTimestamp && !FMLClientHandler.instance().isGUIOpen(GuiDownloadTerrain.class)) { + int timeWait = (int)Math.round((currentTimeStamp - lastTimeStamp)/replaySpeed); + long timeDiff = System.currentTimeMillis() - lastPacketSent; + lastPacketSent = System.currentTimeMillis(); + Thread.sleep(Math.max(0, timeWait-timeDiff)); + } + + int bytes = dis.readInt(); + byte[] bb = new byte[bytes]; + dis.readFully(bb); + + ReplaySender.this.channelRead(ctx, bb); + + lastTimeStamp = currentTimeStamp; + + if(hurryToTimestamp && currentTimeStamp >= desiredTimeStamp && !startFromBeginning) { + toleratedTimeStamp = currentTimeStamp; + hurryToTimestamp = false; + System.out.println("stop hurrying"); + } + + } catch(EOFException eof) { + setReplaySpeed(0); + } + } + } + } catch(Exception e) { + e.printStackTrace(); + } + } + }); + + private List packetClasses = new ArrayList(); + + private static Field field_149074_a; //TODO: REMOVE + static { + try { + field_149074_a = S14PacketEntity.class.getDeclaredField(MCPNames.field("field_149074_a")); + field_149074_a.setAccessible(true); + } catch(Exception e) { + e.printStackTrace(); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) + throws Exception { + if(terminate) { + return; + } + + if(msg instanceof Packet) { + super.channelRead(ctx, msg); + return; + } + byte[] ba = (byte[])msg; + + try { + ByteBuf bb = Unpooled.buffer(ba.length); + PacketBuffer pb = new PacketBuffer(bb); + + pb.writeBytes(ba); + + int i = pb.readVarIntFromBuffer(); + + Packet p = EnumConnectionState.PLAY.getPacket(EnumPacketDirection.CLIENTBOUND, i); + + if(hurryToTimestamp) { //If hurrying, ignore some packets + if(p instanceof S45PacketTitle || + p instanceof S29PacketSoundEffect) return; + } + + if(p instanceof S28PacketEffect) { + return; + } + + if(p instanceof S2BPacketChangeGameState) { //TODO: Test with rain + return; + } + + if(p instanceof S06PacketUpdateHealth) { + return; + } + + if(p instanceof S2DPacketOpenWindow) { + return; + } + + if(p instanceof S2EPacketCloseWindow) { + return; + } + + if(p instanceof S2FPacketSetSlot) { + return; + } + + if(p instanceof S30PacketWindowItems) { + return; + } + + if(p instanceof S36PacketSignEditorOpen) { + return; + } + + if(p instanceof S02PacketChat) { + byte pos = (Byte)chatPacketPosition.get(p); + if(pos == 1) { //Ignores command block output sent + return; + } + } + + if(p instanceof S1CPacketEntityMetadata) { + int entityId = (Integer)metadataPacketEntityId.get(p); + } + + if(p instanceof S0BPacketAnimation) { + int entityId = (Integer)animationPacketEntityId.get(p); + if(entityId == 0) { + return; + } + } + + if(p instanceof S1DPacketEntityEffect) { + int entityId = (Integer)effectPacketEntityId.get(p); + if(entityId == 0) { + return; + } + } + + if(p instanceof S37PacketStatistics) { + return; + } + + try { + p.readPacketData(pb); + + if(p instanceof S0CPacketSpawnPlayer) { + S0CPacketSpawnPlayer sp = (S0CPacketSpawnPlayer)p; + System.out.println("PACKET SPAWN PLAYER ----------"); + System.out.println("ENTITY ID: "+sp.func_148943_d()); + System.out.println("UUID: "+sp.func_179819_c()); + System.out.println("X: "+sp.func_148942_f()); + System.out.println("Y: "+sp.func_148949_g()); + System.out.println("Z: "+sp.func_148946_h()); + System.out.println("YAW: "+sp.func_148941_i()); + System.out.println("PITCH: "+sp.func_148945_j()); + System.out.println("ITEM: "+sp.func_148947_k()); + System.out.println("PACKET END -------------------"); + } + + if(p instanceof S01PacketJoinGame) { + + int entId = (Integer)joinPacketEntityId.get(p); + entId = -1; + int dimension = (Integer)joinPacketDimension.get(p); + EnumDifficulty difficulty = (EnumDifficulty)joinPacketDifficulty.get(p); + int maxPlayers = (Integer)joinPacketMaxPlayers.get(p); + WorldType worldType = (WorldType)joinPacketWorldType.get(p); + + p = new S01PacketJoinGame(entId, GameType.SPECTATOR, false, dimension, + difficulty, maxPlayers, worldType, false); + } + + if(p instanceof S39PacketPlayerAbilities) { + return; + } + + if(p instanceof S08PacketPlayerPosLook) { + final S08PacketPlayerPosLook ppl = (S08PacketPlayerPosLook)p; + + if(ReplayHandler.isReplaying()) return; + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + while(mc.theWorld == null) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + + Entity ent = ReplayHandler.getCameraEntity(); + if(ent == null || !(ent instanceof CameraEntity)) ent = new CameraEntity(mc.theWorld); + CameraEntity cent = (CameraEntity)ent; + cent.moveAbsolute(ppl.func_148932_c(), ppl.func_148928_d(), ppl.func_148933_e()); + + ReplayHandler.setCameraEntity(cent); + } + }); + + t.start(); + } + + if(p instanceof S43PacketCamera) { + return; + } + if(ReplayHandler.isReplaying()) + System.out.println("packet arrived"); + super.channelRead(ctx, p); + } catch(Exception e) { + System.out.println(p.getClass()); + e.printStackTrace(); + } + + } catch(Exception e) { + //e.printStackTrace(); + } + + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + this.ctx = ctx; + networkManager.channel().attr(networkManager.attrKeyConnectionState).set(EnumConnectionState.PLAY); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + archive.close(); + super.channelInactive(ctx); + } + + public boolean paused() { + try { + return ((Timer)mcTimer.get(mc)).timerSpeed == 0; + } catch(Exception e) {} + return true; + } + + public double getReplaySpeed() { + if(!paused()) return replaySpeed; + else return 0; + //return timeInfo.get().getSpeed(); + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java b/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java new file mode 100644 index 00000000..097b6ee4 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/settings/ReplaySettings.java @@ -0,0 +1,101 @@ +package eu.crushedpixel.replaymod.settings; + +import java.util.LinkedHashMap; +import java.util.Map; + +import net.minecraftforge.common.config.Property; +import eu.crushedpixel.replaymod.ReplayMod; + +public class ReplaySettings { + + private int maximumFileSize = 0; + private boolean enableRecordingServer = true; + private boolean enableRecordingSingleplayer = true; + private boolean showNotifications = true; + private boolean forceLinearPath = false; + + public ReplaySettings(int maximumFileSize, boolean enableRecordingServer, + boolean enableRecordingSingleplayer, boolean showNotifications, boolean forceLinearPath) { + this.maximumFileSize = maximumFileSize; + this.enableRecordingServer = enableRecordingServer; + this.enableRecordingSingleplayer = enableRecordingSingleplayer; + this.showNotifications = showNotifications; + this.forceLinearPath = forceLinearPath; + } + + public int getMaximumFileSize() { + return maximumFileSize; + } + public void setMaximumFileSize(int maximumFileSize) { + this.maximumFileSize = maximumFileSize; + rewriteSettings(); + } + public boolean isEnableRecordingServer() { + return enableRecordingServer; + } + public void setEnableRecordingServer(boolean enableRecordingServer) { + this.enableRecordingServer = enableRecordingServer; + rewriteSettings(); + } + public boolean isEnableRecordingSingleplayer() { + return enableRecordingSingleplayer; + } + public void setEnableRecordingSingleplayer(boolean enableRecordingSingleplayer) { + this.enableRecordingSingleplayer = enableRecordingSingleplayer; + rewriteSettings(); + } + public boolean isShowNotifications() { + return showNotifications; + } + public void setShowNotifications(boolean showNotifications) { + this.showNotifications = showNotifications; + rewriteSettings(); + } + public boolean isLinearMovement() { + return forceLinearPath; + } + public void setLinearMovement(boolean linear) { + this.forceLinearPath = linear; + rewriteSettings(); + } + + public Map getOptions() { + Map map = new LinkedHashMap(); + + map.put("Enable Notifications", showNotifications); + map.put("Maximum File Size", maximumFileSize); + map.put("Record Server", enableRecordingServer); + map.put("Record Singleplayer", enableRecordingSingleplayer); + map.put("Force Linear Movement", forceLinearPath); + + return map; + } + + public void setOptions(Map map) { + try { + + maximumFileSize = (Integer)map.get("Maximum File Size"); + showNotifications = (Boolean)map.get("Enable Notifications"); + enableRecordingServer = (Boolean)map.get("Record Server"); + enableRecordingSingleplayer = (Boolean)map.get("Record Singleplayer"); + forceLinearPath = (Boolean)map.get("Force Linear Movement"); + + rewriteSettings(); + } catch(Exception e) { + e.printStackTrace(); + } + } + + public void rewriteSettings() { + ReplayMod.instance.config.load(); + + ReplayMod.instance.config.removeCategory(ReplayMod.instance.config.getCategory("settings")); + Property recServer = ReplayMod.instance.config.get("settings", "enableRecordingServer", enableRecordingServer, "Defines whether a recording should be started upon joining a server."); + Property recSP = ReplayMod.instance.config.get("settings", "enableRecordingSingleplayer", enableRecordingSingleplayer, "Defines whether a recording should be started upon joining a singleplayer world."); + Property maxFileSize = ReplayMod.instance.config.get("settings", "maximumFileSize", maximumFileSize, "The maximum File size (in MB) of a recording. 0 means unlimited."); + Property showNot = ReplayMod.instance.config.get("settings", "showNotifications", showNotifications, "Defines whether notifications should be sent to the player."); + Property linear = ReplayMod.instance.config.get("settings", "forceLinearPath", forceLinearPath, "Defines whether travelling paths should be linear instead of interpolated."); + + ReplayMod.instance.config.save(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/unused/DataReciever.java b/src/main/java/eu/crushedpixel/replaymod/unused/DataReciever.java new file mode 100644 index 00000000..9460684e --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/unused/DataReciever.java @@ -0,0 +1,30 @@ +package eu.crushedpixel.replaymod.unused; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.Packet; + +public class DataReciever { + + private byte[] array; + private int timestamp; + + public DataReciever(byte[] array, int timestamp) { + this.array = array; + this.timestamp = timestamp; + } + + public byte[] getByteArray() { + return array; + } + public void setByteArray(byte[] array) { + this.array = array; + } + public int getTimestamp() { + return timestamp; + } + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/unused/DataWriter.java b/src/main/java/eu/crushedpixel/replaymod/unused/DataWriter.java new file mode 100644 index 00000000..2c9087d0 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/unused/DataWriter.java @@ -0,0 +1,88 @@ +package eu.crushedpixel.replaymod.unused; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.io.DataOutputStream; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentLinkedQueue; + +import net.minecraft.network.PacketBuffer; + +public class DataWriter { + + private boolean active = true; + + private ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); + + Thread outputThread = new Thread(new Runnable() { + + @Override + public void run() { + + HashMap counts = new HashMap(); + + while(active) { + DataReciever dataReciever = queue.poll(); + if(dataReciever != null) { + //write the ByteBuf to the given OutputStream + + byte[] array = dataReciever.getByteArray(); + + if(array != null) { + try { + + stream.writeInt(dataReciever.getTimestamp()); //Timestamp + stream.writeInt(array.length); //Lenght + + stream.write(array); //Content + stream.flush(); + } catch(Exception e) { + e.printStackTrace(); + } + } + + } else { + try { + //let the Thread sleep for 1/2 second and queue up new Packets + Thread.sleep(500L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + try { + stream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + for(Entry entries : counts.entrySet()) { + System.out.println(entries.getKey()+ "| "+entries.getValue()); + } + + } + }); + + public ConcurrentLinkedQueue getQueue() { + return queue; + } + + public void setQueue(ConcurrentLinkedQueue queue) { + this.queue = queue; + } + + private DataOutputStream stream; + + public DataWriter(DataOutputStream stream) { + this.stream = stream; + outputThread.start(); + } + + public void requestFinish() { + active = false; + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/unused/PacketListener.java b/src/main/java/eu/crushedpixel/replaymod/unused/PacketListener.java new file mode 100644 index 00000000..2b5e6573 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/unused/PacketListener.java @@ -0,0 +1,37 @@ +package eu.crushedpixel.replaymod.unused; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.Packet; +import net.minecraft.network.PacketBuffer; + +public class PacketListener extends ChannelInboundHandlerAdapter { + + private PacketWriter packetWriter; + private long startTime; + + public PacketListener(PacketWriter packetWriter, long startTime) { + this.packetWriter = packetWriter; + this.startTime = startTime; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + super.channelRead(ctx, msg); + + if(msg instanceof Packet) { + Packet packet = (Packet)msg; + int packetID = ((EnumConnectionState)ctx.channel() + .attr(NetworkManager.attrKeyConnectionState).get()).getPacketId(EnumPacketDirection.CLIENTBOUND, packet); + int timestamp = (int) (System.currentTimeMillis() - startTime); + PacketReciever reciever = new PacketReciever(packet, (short)packetID, timestamp); + packetWriter.getQueue().add(reciever); + } + } + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/unused/PacketReciever.java b/src/main/java/eu/crushedpixel/replaymod/unused/PacketReciever.java new file mode 100644 index 00000000..a08fcbb4 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/unused/PacketReciever.java @@ -0,0 +1,37 @@ +package eu.crushedpixel.replaymod.unused; + +import net.minecraft.network.Packet; + +public class PacketReciever { + + private Packet packet; + private short packetID; + private int timestamp; + + public PacketReciever(Packet packet, short packetID, int timestamp) { + this.packet = packet; + this.packetID = packetID; + this.timestamp = timestamp; + } + + public Packet getPacket() { + return packet; + } + public void setPacket(Packet packet) { + this.packet = packet; + } + public short getPacketID() { + return packetID; + } + public void setPacketID(short packetID) { + this.packetID = packetID; + } + public int getTimestamp() { + return timestamp; + } + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/unused/PacketWriter.java b/src/main/java/eu/crushedpixel/replaymod/unused/PacketWriter.java new file mode 100644 index 00000000..65c33da6 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/unused/PacketWriter.java @@ -0,0 +1,101 @@ +package eu.crushedpixel.replaymod.unused; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.io.DataOutputStream; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentLinkedQueue; + +import net.minecraft.network.PacketBuffer; + +public class PacketWriter { + + private boolean active = true; + + private ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); + + Thread outputThread = new Thread(new Runnable() { + + @Override + public void run() { + + HashMap counts = new HashMap(); + + while(active) { + PacketReciever packetReciever = queue.poll(); + if(packetReciever != null) { + ByteBuf bb = Unpooled.buffer(); + PacketBuffer packetBuffer = new PacketBuffer(bb); + //write the Packet to the given OutputStream + + if(packetReciever.getPacket() != null) { + try { + packetReciever.getPacket().writePacketData(packetBuffer); + + bb.readerIndex(0); + byte[] array = new byte[bb.readableBytes()]; + bb.readBytes(array); + + stream.writeInt(packetReciever.getTimestamp()); //Timestamp + stream.writeInt(array.length); //Lenght + + if(counts.containsKey(packetReciever.getPacket().getClass())) { + counts.put(packetReciever.getPacket().getClass(), counts.get(packetReciever.getPacket().getClass())+array.length); + } else { + counts.put(packetReciever.getPacket().getClass(), array.length); + } + + stream.writeShort(packetReciever.getPacketID()); //Packet ID + stream.write(array); //Content + stream.flush(); + } catch(Exception e) { + System.out.println(packetReciever.getPacket().getClass()); + e.printStackTrace(); + } + } + + } else { + try { + //let the Thread sleep for 1/2 second and queue up new Packets + Thread.sleep(500L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + try { + stream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + for(Entry entries : counts.entrySet()) { + System.out.println(entries.getKey()+ "| "+entries.getValue()); + } + + } + }); + + public ConcurrentLinkedQueue getQueue() { + return queue; + } + + public void setQueue(ConcurrentLinkedQueue queue) { + this.queue = queue; + } + + private DataOutputStream stream; + + public PacketWriter(DataOutputStream stream) { + this.stream = stream; + outputThread.start(); + } + + public void requestFinish() { + active = false; + } + +} diff --git a/src/main/resources/assets/replaymod/extended_gui.png b/src/main/resources/assets/replaymod/extended_gui.png new file mode 100644 index 0000000000000000000000000000000000000000..68bb39250a9c2ad226c7a438dc29e6f84ec6f80d GIT binary patch literal 1972 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 R|3Cl$002ovPDHLkV1oGY^{xN_ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/replaymod/timeline_icons.png b/src/main/resources/assets/replaymod/timeline_icons.png new file mode 100644 index 0000000000000000000000000000000000000000..a71e93551ef8db3b0505af13a25232fb0aba0377 GIT binary patch literal 1519 zcmV0pBkT=iJ%};loJ$s!Qpt0S z5o5(-=#~Jn(>{V;vna}55A0@IPb2pWp4jM z;L4RNc<;G!<3S%wC9@ZbT@pFeLVpFMk4 zW~)xd`|@)oqL%{%AbL4qKaas(3F~r#b`r_ zfud*=Mm_G&CkD_;BC4GWwQl{D4+xREZOyMaLa3d|>GQiD69}w)B*c(-!PE#|*`jm6 zk>rS>`NjawhsL7krrCOYNvgOcvFysX?o_y9XL5%4fi?tkB``))6t2lTMjMPQy~Y@o zgo;G0&5w1!h{~U;CMjP0{tM?{KdcIp3~MbbyCl!bZcnK)f=|0iLWCd#vzYUnJ7uPG zf?x6bY2f_%X~Ok(iOMDf6$$eKJ<0xbgH||{El?OR8X*KRRtfUpfH0r8^1a8td_{g< zAmwX`9H%wfIb_fT>FORx(PWUDH*cW6(ONfcI(1I?D>ytnOq1aJw6$d2GZX@318=FTyr}vFjmmo)OOwsPg@d&M z#uk=XWp^Ju&WEj=?kZm!tJ|a0RX)?E=jR)Y+I%1{l?pzRWvzmC+4<4|>qcL0#N?*u z5gw)5BWnKZoXNoi)mGo_#(jJK*X~_s^-WN+G_cfaU`=AXCrvNSFP2NZ^Qi%hF&Ld8 zRr(gXouJCBZ+^Zk+u;|BS8HqDJDh9XdO2U>N!>g~hf(b->bBkWk4u2%a@8JX5kg0P zT@(1Q-IJ!1392%j2Do?c9*2jAL>CeI+`RWfc3?|c@pu~_q@k+;&V}Y z%4&(K