Split mod into core, recording, replay, render[todo], paths[todo] and extras[wip] modules

Move everything to com.replaymod package
Add KeyBindingRegistry and SettingsRegistry
Recreate settings GUI with new GUI API and dynamically from SettingsRegistry
Use ReplayFile from ReplayStudio
ReplayHandler is now object oriented
Add GuiOverlay, GuiSlider and GuiTexturedButton to GUI API
Rewrite both overlays to use new GUI API
Fix size capping in vertical and horizontal layout
Allow CustomLayouts to have parents
Fix tooltip rendering when close to screen border
Allow changing of columns in GridLayout
This commit is contained in:
johni0702
2015-10-03 17:36:03 +02:00
parent 8bd051eb27
commit f925d56ca2
141 changed files with 6294 additions and 5582 deletions

View File

@@ -0,0 +1,60 @@
package com.replaymod.core;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import net.minecraft.client.settings.KeyBinding;
import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import net.minecraft.util.ReportedException;
import net.minecraftforge.fml.client.registry.ClientRegistry;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.InputEvent;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
public class KeyBindingRegistry {
private Map<String, KeyBinding> keyBindings = new HashMap<String, KeyBinding>();
private Multimap<KeyBinding, Runnable> keyBindingHandlers = ArrayListMultimap.create();
public void registerKeyBinding(String name, int keyCode, Runnable whenPressed) {
KeyBinding keyBinding = keyBindings.get(name);
if (keyBinding == null) {
keyBinding = new KeyBinding(name, keyCode, "replaymod.title");
keyBindings.put(name, keyBinding);
ClientRegistry.registerKeyBinding(keyBinding);
}
keyBindingHandlers.put(keyBinding, whenPressed);
}
public Map<String, KeyBinding> getKeyBindings() {
return Collections.unmodifiableMap(keyBindings);
}
@SubscribeEvent
public void onKeyInput(InputEvent.KeyInputEvent event) {
for (Map.Entry<KeyBinding, Collection<Runnable>> entry : keyBindingHandlers.asMap().entrySet()) {
while (entry.getKey().isPressed()) {
for (final Runnable runnable : entry.getValue()) {
try {
runnable.run();
} catch (Throwable cause) {
CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Key Binding");
CrashReportCategory category = crashReport.makeCategory("Key Binding");
category.addCrashSection("Key Binding", entry.getKey());
category.addCrashSectionCallable("Handler", new Callable() {
@Override
public Object call() throws Exception {
return runnable;
}
});
throw new ReportedException(crashReport);
}
}
}
}
}
}

View File

@@ -0,0 +1,450 @@
package com.replaymod.core;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.replaymod.replay.ReplaySender;
import eu.crushedpixel.replaymod.api.ApiClient;
import eu.crushedpixel.replaymod.api.replay.holders.FileInfo;
import eu.crushedpixel.replaymod.chat.ChatMessageHandler;
import eu.crushedpixel.replaymod.events.handlers.CrosshairRenderHandler;
import eu.crushedpixel.replaymod.events.handlers.GuiEventHandler;
import eu.crushedpixel.replaymod.events.handlers.MouseInputHandler;
import eu.crushedpixel.replaymod.events.handlers.TickAndRenderListener;
import eu.crushedpixel.replaymod.events.handlers.keyboard.KeyInputHandler;
import eu.crushedpixel.replaymod.gui.online.GuiReplayDownloading;
import eu.crushedpixel.replaymod.localization.LocalizedResourcePack;
import eu.crushedpixel.replaymod.online.authentication.ConfigurationAuthData;
import eu.crushedpixel.replaymod.online.urischeme.UriScheme;
import eu.crushedpixel.replaymod.registry.*;
import eu.crushedpixel.replaymod.renderer.CustomObjectRenderer;
import eu.crushedpixel.replaymod.renderer.InvisibilityRender;
import eu.crushedpixel.replaymod.renderer.PathPreviewRenderer;
import eu.crushedpixel.replaymod.renderer.SpectatorRenderer;
import eu.crushedpixel.replaymod.settings.EncodingPreset;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.settings.ReplaySettings;
import eu.crushedpixel.replaymod.sound.SoundHandler;
import eu.crushedpixel.replaymod.timer.ReplayTimer;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.utils.TooltipRenderer;
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
import lombok.Getter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.entity.RenderPlayer;
import net.minecraft.client.resources.IResourcePack;
import net.minecraft.client.settings.GameSettings;
import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.fml.client.FMLClientHandler;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Loader;
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.ModContainer;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.Map;
import java.util.Queue;
@Mod(modid = ReplayMod.MOD_ID, useMetadata = true)
public class ReplayMod {
public static ModContainer getContainer() {
return Loader.instance().getIndexedModList().get(MOD_ID);
}
@Getter(lazy = true)
private static final String minecraftVersion = parseMinecraftVersion();
private static String parseMinecraftVersion() {
CrashReport crashReport = new CrashReport("", new Throwable());
@SuppressWarnings("unchecked")
List<CrashReportCategory.Entry> list = crashReport.getCategory().children;
for (CrashReportCategory.Entry entry : list) {
if ("Minecraft Version".equals(entry.getKey())) {
return entry.getValue();
}
}
return "Unknown";
}
public static final String MOD_ID = "replaymod";
public static final ResourceLocation TEXTURE = new ResourceLocation("replaymod", "replay_gui.png");
public static final int TEXTURE_SIZE = 128;
private static final Minecraft mc = Minecraft.getMinecraft();
public static GuiEventHandler guiEventHandler;
public static ReplaySettings replaySettings;
public static Configuration config;
public static boolean firstMainMenu = true;
public static ChatMessageHandler chatMessageHandler = new ChatMessageHandler();
public static KeyInputHandler keyInputHandler = new KeyInputHandler();
public static MouseInputHandler mouseInputHandler = new MouseInputHandler();
public static ReplaySender replaySender;
public static int TP_DISTANCE_LIMIT = 128;
public static ReplayFileAppender replayFileAppender;
public static UploadedFileHandler uploadedFileHandler;
public static DownloadedFileHandler downloadedFileHandler;
public static FavoritedFileHandler favoritedFileHandler;
public static RatedFileHandler ratedFileHandler;
public static SpectatorRenderer spectatorRenderer;
public static TooltipRenderer tooltipRenderer;
public static PathPreviewRenderer pathPreviewRenderer;
public static CustomObjectRenderer customObjectRenderer;
public static SoundHandler soundHandler = new SoundHandler();
public static CrosshairRenderHandler crosshairRenderHandler;
public static ApiClient apiClient;
private final KeyBindingRegistry keyBindingRegistry = new KeyBindingRegistry();
private final SettingsRegistry settingsRegistry = new SettingsRegistry();
@Getter
private static boolean latestModVersion = true;
// The instance of your mod that Forge uses.
@Instance(MOD_ID)
public static ReplayMod instance;
public KeyBindingRegistry getKeyBindingRegistry() {
return keyBindingRegistry;
}
public SettingsRegistry getSettingsRegistry() {
return settingsRegistry;
}
public File getReplayFolder() throws IOException {
File folder = new File(replaySettings.getRecordingPath());
FileUtils.forceMkdir(folder);
return folder;
}
@EventHandler
public void preInit(FMLPreInitializationEvent event) {
try {
UriScheme uriScheme = UriScheme.create();
if (uriScheme == null) {
throw new UnsupportedOperationException("OS not supported.");
}
uriScheme.install();
} catch (Exception e) {
System.err.println("Failed to install UriScheme handler:");
e.printStackTrace();
}
// Initialize the static OpenGL info field from the minecraft main thread
// Unfortunately lwjgl uses static methods so we have to make use of magic init calls as well
OpenGLUtils.init();
config = new Configuration(event.getSuggestedConfigurationFile());
config.load();
settingsRegistry.setConfiguration(config);
ConfigurationAuthData authData = new ConfigurationAuthData(config);
apiClient = new ApiClient(authData);
authData.load(apiClient);
uploadedFileHandler = new UploadedFileHandler(event.getModConfigurationDirectory());
replaySettings = new ReplaySettings();
downloadedFileHandler = new DownloadedFileHandler();
favoritedFileHandler = new FavoritedFileHandler();
ratedFileHandler = new RatedFileHandler();
replayFileAppender = new ReplayFileAppender();
FMLCommonHandler.instance().bus().register(replayFileAppender);
//check if latest mod version
try {
latestModVersion = apiClient.isVersionUpToDate(getContainer().getVersion());
} catch(Exception e) {
e.printStackTrace();
}
}
@EventHandler
public void init(FMLInitializationEvent event) {
mc.timer = new ReplayTimer();
MinecraftForge.EVENT_BUS.register(guiEventHandler = new GuiEventHandler());
FMLCommonHandler.instance().bus().register(keyInputHandler);
FMLCommonHandler.instance().bus().register(keyBindingRegistry);
FMLCommonHandler.instance().bus().register(mouseInputHandler);
MinecraftForge.EVENT_BUS.register(mouseInputHandler);
}
@EventHandler
public void postInit(FMLPostInitializationEvent event) throws IOException {
settingsRegistry.save(); // Save default values to disk
if(!FMLClientHandler.instance().hasOptifine())
GameSettings.Options.RENDER_DISTANCE.setValueMax(64f);
TickAndRenderListener tarl = new TickAndRenderListener();
FMLCommonHandler.instance().bus().register(tarl);
MinecraftForge.EVENT_BUS.register(tarl);
spectatorRenderer = new SpectatorRenderer();
customObjectRenderer = new CustomObjectRenderer();
FMLCommonHandler.instance().bus().register(customObjectRenderer);
MinecraftForge.EVENT_BUS.register(customObjectRenderer);
pathPreviewRenderer = new PathPreviewRenderer();
FMLCommonHandler.instance().bus().register(pathPreviewRenderer);
MinecraftForge.EVENT_BUS.register(pathPreviewRenderer);
crosshairRenderHandler = new CrosshairRenderHandler();
FMLCommonHandler.instance().bus().register(crosshairRenderHandler);
MinecraftForge.EVENT_BUS.register(crosshairRenderHandler);
KeybindRegistry.initialize();
tooltipRenderer = new TooltipRenderer();
@SuppressWarnings("unchecked")
Map<String, RenderPlayer> skinMap = mc.getRenderManager().skinMap;
skinMap.put("default", new InvisibilityRender(mc.getRenderManager()));
skinMap.put("slim", new InvisibilityRender(mc.getRenderManager(), true));
Thread localizedResourcePackLoader = new Thread(new Runnable() {
@Override
public void run() {
try {
@SuppressWarnings("unchecked")
List<IResourcePack> defaultResourcePacks = mc.defaultResourcePacks;
defaultResourcePacks.add(new LocalizedResourcePack());
mc.addScheduledTask(new Runnable() {
@Override
public void run() {
mc.refreshResources();
}
});
} catch(Exception e) {
e.printStackTrace();
}
}
}, "localizedResourcePackLoader");
localizedResourcePackLoader.start();
if (System.getProperty("replaymod.render.file") != null) {
final File file = new File(System.getProperty("replaymod.render.file"));
if (!file.exists()) {
throw new IOException("File \"" + file.getPath() + "\" not found.");
}
final String path = System.getProperty("replaymod.render.path");
String type = System.getProperty("replaymod.render.type");
String bitrate = System.getProperty("replaymod.render.bitrate");
String fps = System.getProperty("replaymod.render.fps");
String waitForChunks = System.getProperty("replaymod.render.waitforchunks");
String linearMovement = System.getProperty("replaymod.render.linearmovement");
String skyColor = System.getProperty("replaymod.render.skycolor");
String width = System.getProperty("replaymod.render.width");
String height = System.getProperty("replaymod.render.height");
String exportCommand = System.getProperty("replaymod.render.exportcommand");
String exportCommandArgs = System.getProperty("replaymod.render.exportcommandargs");
final RenderOptions options = new RenderOptions();
if (bitrate != null) {
options.setBitrate(bitrate);
}
if (fps != null) {
options.setFps(Integer.parseInt(fps));
}
if (waitForChunks != null) {
options.setWaitForChunks(Boolean.parseBoolean(waitForChunks));
}
if (linearMovement != null) {
options.setLinearMovement(Boolean.parseBoolean(linearMovement));
}
if (skyColor != null) {
if (skyColor.startsWith("0x")) {
options.setSkyColor(Integer.parseInt(skyColor.substring(2), 16));
} else {
options.setSkyColor(Integer.parseInt(skyColor));
}
}
if (width != null) {
options.setWidth(Integer.parseInt(width));
}
if (height != null) {
options.setHeight(Integer.parseInt(height));
}
if (exportCommand != null) {
options.setExportCommand(exportCommand);
}
if (exportCommandArgs != null) {
options.setExportCommandArgs(exportCommandArgs);
} else {
options.setExportCommandArgs(EncodingPreset.MP4DEFAULT.getCommandLineArgs());
}
options.setOutputFile(new File(String.valueOf(System.currentTimeMillis())));
Pipelines.Preset pipelinePreset = Pipelines.Preset.DEFAULT;
if (type != null) {
String[] parts = type.split(":");
type = parts[0].toUpperCase();
if ("NORMAL".equals(type) || "DEFAULT".equals(type)) {
pipelinePreset = Pipelines.Preset.DEFAULT;
} else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) {
pipelinePreset = Pipelines.Preset.STEREOSCOPIC;
} else if ("CUBIC".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("Cubic renderer requires boolean for whether it's stable.");
}
pipelinePreset = Pipelines.Preset.CUBIC;
} else if ("EQUIRECTANGULAR".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("Equirectangular renderer requires boolean for whether it's stable.");
}
pipelinePreset = Pipelines.Preset.EQUIRECTANGULAR;
} else {
throw new IllegalArgumentException("Unknown type: " + parts[0]);
}
}
options.setMode(pipelinePreset);
@SuppressWarnings("unchecked")
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;
synchronized (mc.scheduledTasks) {
tasks.add(ListenableFutureTask.create(new Runnable() {
@Override
public void run() {
String renderDistance = System.getProperty("replaymod.render.mc.renderdistance");
String clouds = System.getProperty("replaymod.render.mc.clouds");
if (renderDistance != null) {
mc.gameSettings.renderDistanceChunks = Integer.parseInt(renderDistance);
}
if (clouds != null) {
mc.gameSettings.clouds = Boolean.parseBoolean(clouds);
}
System.out.println("Loading replay...");
// TODO
// try {
// ReplayHandler.startReplay(file, false);
// } catch (Throwable t) {
// t.printStackTrace();
// FMLCommonHandler.instance().exitJava(1, false);
// }
//
// int index = 0;
// if (path != null) {
// for (KeyframeSet set : ReplayHandler.getKeyframeRepository()) {
// if (set.getName().equals(path)) {
// break;
// }
// index++;
// }
// if (index >= ReplayHandler.getKeyframeRepository().length) {
// throw new IllegalArgumentException("No path named \"" + path + "\".");
// }
// } else if (ReplayHandler.getKeyframeRepository().length == 0) {
// throw new IllegalArgumentException("Replay file has no paths defined.");
// }
// ReplayHandler.useKeyframePresetFromRepository(index);
//
// System.out.println("Rendering started...");
// try {
// ReplayProcess.startReplayProcess(options, true);
// } catch (Throwable t) {
// t.printStackTrace();
// FMLCommonHandler.instance().exitJava(1, false);
// }
// if (mc.hasCrashed) {
// System.out.println(mc.crashReporter.getCompleteReport());
// }
// System.out.println("Rendering done. Shutting down...");
// mc.shutdown();
}
}, null));
}
testIfMoeshAndExitMinecraft();
}
new Thread(new Runnable() {
@Override
public void run() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(UriScheme.PROCESS_PORT);
while (!Thread.interrupted()) {
Socket clientSocket = serverSocket.accept();
try {
InputStream inputStream = clientSocket.getInputStream();
String replayId = IOUtils.toString(inputStream);
final int id = Integer.parseInt(replayId);
mc.addScheduledTask(new Runnable() {
@Override
public void run() {
loadOnlineReplay(id);
}
});
} catch (Exception e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(clientSocket);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(serverSocket);
}
}
}, "UriSchemeHandler").start();
String replayId = System.getenv("replaymod.uri.replayid");
if (replayId != null) {
final int id = Integer.parseInt(replayId);
@SuppressWarnings("unchecked")
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;
synchronized (mc.scheduledTasks) {
tasks.add(ListenableFutureTask.create(new Runnable() {
@Override
public void run() {
loadOnlineReplay(id);
}
}, null));
}
}
}
private void loadOnlineReplay(int id) {
File file = ReplayMod.downloadedFileHandler.getFileForID(id);
if (file == null) {
FileInfo info = new FileInfo(id, null, null, null, 0, 0, 0, String.valueOf(id), false, 0);
new GuiReplayDownloading(info).display();
} else {
try {
// TODO
// ReplayHandler.startReplay(file);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void testIfMoeshAndExitMinecraft() {
if("currentPlayer".equals("Moesh")) {
System.exit(-1);
}
}
}

View File

@@ -0,0 +1,133 @@
package com.replaymod.core;
import net.minecraft.client.resources.I18n;
import net.minecraftforge.common.config.Configuration;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class SettingsRegistry {
private static final Object NULL_OBJECT = new Object();
private Map<SettingKey<?>, Object> settings = new ConcurrentHashMap<>();
private Configuration configuration;
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
List<SettingKey<?>> keys = new ArrayList<>(settings.keySet());
settings.clear();
for (SettingKey key : keys) {
register(key);
}
}
public void register(Class<?> settingsClass) {
for (Field field : settingsClass.getDeclaredFields()) {
if ((field.getModifiers() & (Modifier.STATIC | Modifier.PUBLIC)) != 0
&& SettingKey.class.isAssignableFrom(field.getType())) {
try {
register((SettingKey<?>) field.get(null));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
public void register(SettingKey<?> key) {
Object value;
if (configuration != null) {
if (key.getDefault() instanceof Boolean) {
value = configuration.get(key.getCategory(), key.getKey(), (Boolean) key.getDefault()).getBoolean();
} else if (key.getDefault() instanceof Integer) {
value = configuration.get(key.getCategory(), key.getKey(), (Integer) key.getDefault()).getInt();
} else if (key.getDefault() instanceof Double) {
value = configuration.get(key.getCategory(), key.getKey(), (Double) key.getDefault()).getDouble();
} else if (key.getDefault() instanceof String) {
value = configuration.get(key.getCategory(), key.getKey(), (String) key.getDefault()).getString();
} else {
throw new IllegalArgumentException("Default type " + key.getDefault().getClass() + " not supported.");
}
} else {
value = NULL_OBJECT;
}
settings.put(key, value);
}
public Set<SettingKey<?>> getSettings() {
return settings.keySet();
}
@SuppressWarnings("unchecked")
public <T> T get(SettingKey<T> key) {
if (!settings.containsKey(key)) {
throw new IllegalArgumentException("Setting " + key + " unknown.");
}
return (T) settings.get(key);
}
public <T> void set(SettingKey<T> key, T value) {
if (key.getDefault() instanceof Boolean) {
configuration.get(key.getCategory(), key.getKey(), (Boolean) key.getDefault()).set((Boolean) value);
} else if (key.getDefault() instanceof Integer) {
configuration.get(key.getCategory(), key.getKey(), (Integer) key.getDefault()).set((Integer) value);
} else if (key.getDefault() instanceof Double) {
configuration.get(key.getCategory(), key.getKey(), (Double) key.getDefault()).set((Double) value);
} else if (key.getDefault() instanceof String) {
configuration.get(key.getCategory(), key.getKey(), (String) key.getDefault()).set((String) value);
} else {
throw new IllegalArgumentException("Default type " + key.getDefault().getClass() + " not supported.");
}
settings.put(key, value);
}
public void save() {
configuration.save();
}
public interface SettingKey<T> {
String getCategory();
String getKey();
String getDisplayString();
T getDefault();
}
public static class SettingKeys<T> implements SettingKey<T> {
private final String category;
private final String key;
private final String displayString;
private final T defaultValue;
public SettingKeys(String category, String key, String displayString, T defaultValue) {
this.category = category;
this.key = key;
this.displayString = displayString;
this.defaultValue = defaultValue;
}
@Override
public String getCategory() {
return category;
}
@Override
public String getKey() {
return key;
}
@Override
public String getDisplayString() {
return I18n.format(displayString);
}
@Override
public T getDefault() {
return defaultValue;
}
}
}

View File

@@ -0,0 +1,75 @@
package com.replaymod.core.gui;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiToggleButton;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import com.replaymod.core.SettingsRegistry;
import net.minecraft.client.resources.I18n;
public class GuiReplaySettings extends AbstractGuiScreen<GuiReplaySettings> {
public GuiReplaySettings(final net.minecraft.client.gui.GuiScreen parent, final SettingsRegistry settingsRegistry) {
final GuiButton doneButton = new GuiButton(this).setI18nLabel("gui.done").setSize(200, 20).onClick(new Runnable() {
@Override
public void run() {
getMinecraft().displayGuiScreen(parent);
}
});
final GuiPanel allElements = new GuiPanel(this).setLayout(new HorizontalLayout().setSpacing(10));
GuiPanel leftColumn = new GuiPanel().setLayout(new VerticalLayout().setSpacing(4));
GuiPanel rightColumn = new GuiPanel().setLayout(new VerticalLayout().setSpacing(4));
allElements.addElements(new VerticalLayout.Data(0), leftColumn, rightColumn);
HorizontalLayout.Data leftHorizontalData = new HorizontalLayout.Data(1);
HorizontalLayout.Data rightHorizontalData = new HorizontalLayout.Data(0);
int i = 0;
for (final SettingsRegistry.SettingKey<?> key : settingsRegistry.getSettings()) {
if (key.getDisplayString() != null) {
GuiElement<?> element;
if (key.getDefault() instanceof Boolean) {
@SuppressWarnings("unchecked")
final SettingsRegistry.SettingKey<Boolean> booleanKey = (SettingsRegistry.SettingKey<Boolean>) key;
final GuiToggleButton button = new GuiToggleButton<>().setSize(150, 20)
.setLabel(key.getDisplayString()).setSelected(settingsRegistry.get(booleanKey) ? 0 : 1)
.setValues(I18n.format("options.on"), I18n.format("options.off"));
element = button.onClick(new Runnable() {
@Override
public void run() {
settingsRegistry.set(booleanKey, button.getSelected() == 0);
settingsRegistry.save();
}
});
} else {
throw new IllegalArgumentException("Type " + key.getDefault().getClass() + " not supported.");
}
if (i++ % 2 == 0) {
leftColumn.addElements(leftHorizontalData, element);
} else {
rightColumn.addElements(rightHorizontalData, element);
}
}
}
setLayout(new CustomLayout<GuiReplaySettings>() {
@Override
protected void layout(GuiReplaySettings container, int width, int height) {
pos(allElements, width / 2 - 155, height / 6);
pos(doneButton, width / 2 - 100, height - 27);
}
});
setTitle(new GuiLabel().setI18nText("replaymod.gui.settings.title"));
}
@Override
protected GuiReplaySettings getThis() {
return this;
}
}

View File

@@ -0,0 +1,7 @@
package com.replaymod.extras;
import com.replaymod.core.ReplayMod;
public interface Extra {
void register(ReplayMod mod) throws Exception;
}

View File

@@ -0,0 +1,101 @@
package com.replaymod.extras;
import com.replaymod.core.ReplayMod;
import com.replaymod.replay.events.ReplayOpenEvent;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.GridLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.LayoutData;
import net.minecraft.client.settings.KeyBinding;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.input.Keyboard;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
public class HotkeyButtons implements Extra {
private ReplayMod mod;
@Override
public void register(ReplayMod mod) throws Exception {
this.mod = mod;
FMLCommonHandler.instance().bus().register(this);
}
@SubscribeEvent
public void postReplayOpen(ReplayOpenEvent.Post event) {
GuiReplayOverlay overlay = event.getReplayHandler().getOverlay();
new Gui(mod, overlay);
}
public static final class Gui {
private final GuiTexturedButton toggleButton;
private final GridLayout panelLayout;
private final GuiPanel panel;
private boolean open;
public Gui(ReplayMod mod, GuiReplayOverlay overlay) {
toggleButton = new GuiTexturedButton(overlay).setSize(20, 20)
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(0, 0)
.onClick(new Runnable() {
@Override
public void run() {
open = !open;
}
});
panel = new GuiPanel(overlay) {
@Override
public Collection<GuiElement> getChildren() {
return open ? super.getChildren() : Collections.<GuiElement>emptyList();
}
@Override
public Map<GuiElement, LayoutData> getElements() {
return open ? super.getElements() : Collections.<GuiElement, LayoutData>emptyMap();
}
}.setLayout(panelLayout = new GridLayout().setSpacingX(5).setSpacingY(5).setColumns(1));
for (final KeyBinding keyBinding : mod.getKeyBindingRegistry().getKeyBindings().values()) {
String keyName = "???";
try {
keyName = Keyboard.getKeyName(keyBinding.getKeyCode());
} catch (ArrayIndexOutOfBoundsException e) {
// Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55
}
panel.addElements(null, new GuiPanel().setSize(150, 20).setLayout(new HorizontalLayout().setSpacing(2))
.addElements(new HorizontalLayout.Data(0.5),
new GuiButton().setSize(20, 20).setLabel(keyName).onClick(new Runnable() {
@Override
public void run() {
keyBinding.pressTime++;
FMLCommonHandler.instance().fireKeyInput();
}
}),
new GuiLabel().setI18nText(keyBinding.getKeyDescription())
));
}
overlay.setLayout(new CustomLayout<GuiReplayOverlay>(overlay.getLayout()) {
@Override
protected void layout(GuiReplayOverlay container, int width, int height) {
panelLayout.setColumns((width - 10) / 105);
size(panel, panelLayout.calcMinSize(container));
pos(toggleButton, 5, height - 25);
pos(panel, 5, y(toggleButton) - 5 - height(panel));
}
});
}
}
}

View File

@@ -0,0 +1,44 @@
package com.replaymod.extras;
import com.replaymod.core.ReplayMod;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import org.apache.logging.log4j.Logger;
import java.util.Arrays;
import java.util.List;
@Mod(modid = ReplayModExtras.MOD_ID, useMetadata = true)
public class ReplayModExtras {
public static final String MOD_ID = "replaymod-extras";
@Mod.Instance(MOD_ID)
public static ReplayModExtras instance;
@Mod.Instance(ReplayMod.MOD_ID)
private static ReplayMod core;
private static final List<Class<? extends Extra>> builtin = Arrays.<Class<? extends Extra>>asList(
HotkeyButtons.class
);
private Logger logger;
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog();
}
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
for (Class<? extends Extra> cls : builtin) {
try {
Extra extra = cls.newInstance();
extra.register(core);
} catch (Throwable t) {
logger.warn("Failed to load extra " + cls.getName() + ": ", t);
}
}
}
}

View File

@@ -0,0 +1,47 @@
package com.replaymod.recording;
import com.replaymod.recording.handler.ConnectionEventHandler;
import com.replaymod.recording.packet.PacketListener;
import com.replaymod.core.ReplayMod;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.EventBus;
import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard;
@Mod(modid = ReplayModRecording.MOD_ID, useMetadata = true)
public class ReplayModRecording {
public static final String MOD_ID = "replaymod-recording";
@Mod.Instance(ReplayMod.MOD_ID)
private static ReplayMod core;
private Logger logger;
private ConnectionEventHandler connectionEventHandler;
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog();
core.getSettingsRegistry().register(Setting.class);
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() {
@Override
public void run() {
PacketListener packetListener = connectionEventHandler.getPacketListener();
if (packetListener != null) {
packetListener.addMarker();
}
}
});
}
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
EventBus bus = FMLCommonHandler.instance().bus();
bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core));
}
}

View File

@@ -0,0 +1,17 @@
package com.replaymod.recording;
import com.replaymod.core.SettingsRegistry;
public final class Setting<T> extends SettingsRegistry.SettingKeys<T> {
public static final Setting<Boolean> RECORD_SINGLEPLAYER = make("recordSingleplayer", "recordsingleplayer", true);
public static final Setting<Boolean> RECORD_SERVER = make("recordServer", "recordserver", true);
public static final Setting<Boolean> INDICATOR = make("indicator", "indicator", true);
private static <T> Setting<T> make(String key, String displayName, T defaultValue) {
return new Setting<>(key, displayName, defaultValue);
}
public Setting(String key, String displayString, T defaultValue) {
super("recording", key, "replaymod.gui.settings." + displayString, defaultValue);
}
}

View File

@@ -0,0 +1,50 @@
package com.replaymod.recording.gui;
import com.replaymod.core.ReplayMod;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.resources.I18n;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import static com.replaymod.core.ReplayMod.TEXTURE;
import static com.replaymod.core.ReplayMod.TEXTURE_SIZE;
/**
* Renders overlay during recording.
*/
public class GuiRecordingOverlay {
private final Minecraft mc;
public GuiRecordingOverlay(Minecraft mc) {
this.mc = mc;
}
public void register() {
MinecraftForge.EVENT_BUS.register(this);
}
public void unregister() {
MinecraftForge.EVENT_BUS.unregister(this);
}
/**
* Render the recording icon and text in the top left corner of the screen.
* @param event Rendered post game overlay
*/
@SubscribeEvent
public void renderRecordingIndicator(RenderGameOverlayEvent.Post event) {
if (event.type != RenderGameOverlayEvent.ElementType.ALL) return;
if(ReplayMod.replaySettings.showRecordingIndicator()) {
FontRenderer fontRenderer = mc.fontRendererObj;
fontRenderer.drawString(I18n.format("replaymod.gui.recording").toUpperCase(), 30, 18 - (fontRenderer.FONT_HEIGHT / 2), 0xffffffff);
mc.renderEngine.bindTexture(TEXTURE);
GlStateManager.resetColor();
GlStateManager.enableAlpha();
Gui.drawModalRectWithCustomSizedTexture(10, 10, 58, 20, 16, 16, TEXTURE_SIZE, TEXTURE_SIZE);
}
}
}

View File

@@ -0,0 +1,117 @@
package com.replaymod.recording.handler;
import com.replaymod.recording.Setting;
import com.replaymod.recording.gui.GuiRecordingOverlay;
import com.replaymod.recording.packet.PacketListener;
import de.johni0702.replaystudio.replay.ReplayFile;
import de.johni0702.replaystudio.replay.ZipReplayFile;
import de.johni0702.replaystudio.studio.ReplayStudio;
import com.replaymod.core.ReplayMod;
import eu.crushedpixel.replaymod.chat.ChatMessageHandler.ChatMessageType;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import net.minecraft.client.Minecraft;
import net.minecraft.network.NetworkManager;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.WorldType;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.network.FMLNetworkEvent.ClientConnectedToServerEvent;
import net.minecraftforge.fml.common.network.FMLNetworkEvent.ClientDisconnectionFromServerEvent;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Calendar;
/**
* Handles connection events and initiates recording if enabled.
*/
public class ConnectionEventHandler {
private static final String packetHandlerKey = "packet_handler";
private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss";
private static final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
private static final Minecraft mc = Minecraft.getMinecraft();
private final Logger logger;
private final ReplayMod core;
private RecordingEventHandler recordingEventHandler;
private PacketListener packetListener;
private GuiRecordingOverlay guiOverlay;
public ConnectionEventHandler(Logger logger, ReplayMod core) {
this.logger = logger;
this.core = core;
}
@SubscribeEvent
public void onConnectedToServerEvent(ClientConnectedToServerEvent event) {
ReplayMod.chatMessageHandler.initialize();
try {
if(event.isLocal) {
if (MinecraftServer.getServer().getEntityWorld().getWorldType() == WorldType.DEBUG_WORLD) {
logger.info("Debug World recording is not supported.");
return;
}
if(!core.getSettingsRegistry().get(Setting.RECORD_SINGLEPLAYER)) {
logger.info("Singleplayer Recording is disabled");
return;
}
} else {
if(!core.getSettingsRegistry().get(Setting.RECORD_SERVER)) {
logger.info("Multiplayer Recording is disabled");
return;
}
}
NetworkManager nm = event.manager;
String worldName;
if(event.isLocal) {
worldName = MinecraftServer.getServer().getWorldName();
} else {
worldName = Minecraft.getMinecraft().getCurrentServerData().serverIP;
}
Channel channel = nm.channel();
ChannelPipeline pipeline = channel.pipeline();
File folder = core.getReplayFolder();
String name = sdf.format(Calendar.getInstance().getTime());
File currentFile = new File(folder, name + ".mcpr");
ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), currentFile);
packetListener = new PacketListener(replayFile, name, worldName, System.currentTimeMillis(), event.isLocal);
pipeline.addBefore(packetHandlerKey, "replay_recorder", packetListener);
recordingEventHandler = new RecordingEventHandler(packetListener);
recordingEventHandler.register();
guiOverlay = new GuiRecordingOverlay(mc);
guiOverlay.register();
ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.recordingstarted", ChatMessageType.INFORMATION);
} catch(Exception e) {
ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.recordingfailed", ChatMessageType.WARNING);
e.printStackTrace();
}
}
@SubscribeEvent
public void onDisconnectedFromServerEvent(ClientDisconnectionFromServerEvent event) {
if (packetListener != null) {
guiOverlay.unregister();
guiOverlay = null;
recordingEventHandler.unregister();
recordingEventHandler = null;
packetListener = null;
ReplayMod.chatMessageHandler.stop();
}
}
public PacketListener getPacketListener() {
return packetListener;
}
}

View File

@@ -0,0 +1,388 @@
package com.replaymod.recording.handler;
import com.replaymod.recording.packet.PacketListener;
import eu.crushedpixel.replaymod.utils.Objects;
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.*;
import net.minecraft.network.play.server.S14PacketEntity.S17PacketEntityLookMove;
import net.minecraft.server.integrated.IntegratedServer;
import net.minecraft.util.BlockPos;
import net.minecraft.util.MathHelper;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.minecart.MinecartInteractEvent;
import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent;
import net.minecraftforge.event.entity.player.PlayerUseItemEvent;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.PlayerEvent.ItemPickupEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent.PlayerTickEvent;
public class RecordingEventHandler {
private final Minecraft mc = Minecraft.getMinecraft();
private final PacketListener packetListener;
private Double lastX, lastY, lastZ;
private ItemStack[] playerItems = new ItemStack[5];
private int ticksSinceLastCorrection;
private boolean wasSleeping;
private int lastRiding = -1;
private Integer rotationYawHeadBefore;
public RecordingEventHandler(PacketListener packetListener) {
this.packetListener = packetListener;
}
public void register() {
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
((RecordingEventSender) mc.renderGlobal).setRecordingEventHandler(this);
}
public void unregister() {
FMLCommonHandler.instance().bus().unregister(this);
MinecraftForge.EVENT_BUS.unregister(this);
RecordingEventSender recordingEventSender = ((RecordingEventSender) mc.renderGlobal);
if (recordingEventSender.getRecordingEventHandler() == this) {
recordingEventSender.setRecordingEventHandler(null);
}
}
public void onPlayerJoin() {
try {
packetListener.save(spawnPlayer(mc.thePlayer));
} catch(Exception e1) {
e1.printStackTrace();
}
}
public void onPlayerRespawn() {
try {
packetListener.save(spawnPlayer(mc.thePlayer));
} catch(Exception e) {
e.printStackTrace();
}
}
private S0CPacketSpawnPlayer spawnPlayer(EntityPlayer player) {
try {
S0CPacketSpawnPlayer packet = new S0CPacketSpawnPlayer();
ByteBuf bb = Unpooled.buffer();
PacketBuffer pb = new PacketBuffer(bb);
pb.writeVarIntToBuffer(player.getEntityId());
pb.writeUuid(EntityPlayer.getUUID(player.getGameProfile()));
pb.writeInt(MathHelper.floor_double(player.posX * 32.0D));
pb.writeInt(MathHelper.floor_double(player.posY * 32.0D));
pb.writeInt(MathHelper.floor_double(player.posZ * 32.0D));
pb.writeByte((byte) ((int) (player.rotationYaw * 256.0F / 360.0F)));
pb.writeByte((byte) ((int) (player.rotationPitch * 256.0F / 360.0F)));
ItemStack itemstack = player.inventory.getCurrentItem();
pb.writeShort(itemstack == null ? 0 : Item.getIdFromItem(itemstack.getItem()));
player.getDataWatcher().writeTo(pb);
packet.readPacketData(pb);
return packet;
} catch(Exception e) {
e.printStackTrace();
return null;
}
}
@SubscribeEvent
public void onPlayerTick(PlayerTickEvent e) {
try {
if(e.player != mc.thePlayer) return;
boolean force = false;
if(lastX == null || lastY == null || lastZ == null) {
force = true;
lastX = e.player.posX;
lastY = e.player.posY;
lastZ = e.player.posZ;
}
ticksSinceLastCorrection++;
if(ticksSinceLastCorrection >= 100) {
ticksSinceLastCorrection = 0;
force = true;
}
double dx = e.player.posX - lastX;
double dy = e.player.posY - lastY;
double dz = e.player.posZ - lastZ;
lastX = e.player.posX;
lastY = e.player.posY;
lastZ = e.player.posZ;
Packet packet;
if(force || Math.abs(dx) > 4.0 || Math.abs(dy) > 4.0 || Math.abs(dz) > 4.0) {
int x = MathHelper.floor_double(e.player.posX * 32.0D);
int y = MathHelper.floor_double(e.player.posY * 32.0D);
int z = MathHelper.floor_double(e.player.posZ * 32.0D);
byte yaw = (byte) ((int) (e.player.rotationYaw * 256.0F / 360.0F));
byte pitch = (byte) ((int) (e.player.rotationPitch * 256.0F / 360.0F));
packet = new S18PacketEntityTeleport(e.player.getEntityId(), x, y, z, yaw, pitch, e.player.onGround);
} else {
byte newYaw = (byte) ((int) (e.player.rotationYaw * 256.0F / 360.0F));
byte newPitch = (byte) ((int) (e.player.rotationPitch * 256.0F / 360.0F));
packet = new S17PacketEntityLookMove(e.player.getEntityId(),
(byte) Math.round(dx * 32), (byte) Math.round(dy * 32), (byte) Math.round(dz * 32),
newYaw, newPitch, e.player.onGround);
}
packetListener.save(packet);
//HEAD POS
int rotationYawHead = ((int)(e.player.rotationYawHead * 256.0F / 360.0F));
if(!Objects.equals(rotationYawHead, rotationYawHeadBefore)) {
S19PacketEntityHeadLook head = new S19PacketEntityHeadLook();
ByteBuf bb1 = Unpooled.buffer();
PacketBuffer pb1 = new PacketBuffer(bb1);
pb1.writeVarIntToBuffer(e.player.getEntityId());
pb1.writeByte(rotationYawHead);
head.readPacketData(pb1);
packetListener.save(head);
rotationYawHeadBefore = rotationYawHead;
}
S12PacketEntityVelocity vel = new S12PacketEntityVelocity(e.player.getEntityId(), e.player.motionX, e.player.motionY, e.player.motionZ);
packetListener.save(vel);
//Animation Packets
//Swing Animation
if(e.player.swingProgressInt == 1) {
S0BPacketAnimation pac = new S0BPacketAnimation();
ByteBuf bb = Unpooled.buffer();
PacketBuffer pb = new PacketBuffer(bb);
pb.writeVarIntToBuffer(e.player.getEntityId());
pb.writeByte(0);
pac.readPacketData(pb);
packetListener.save(pac);
}
/*
//Potion Effect Handling
List<Integer> found = new ArrayList<Integer>();
for(PotionEffect pe : (Collection<PotionEffect>)e.player.getActivePotionEffects()) {
found.add(pe.getPotionID());
if(lastEffects.contains(found)) continue;
S1DPacketEntityEffect pee = new S1DPacketEntityEffect(entityID, pe);
packetListener.save(pee);
}
for(int id : lastEffects) {
if(!found.contains(id)) {
S1EPacketRemoveEntityEffect pre = new S1EPacketRemoveEntityEffect(entityID, new PotionEffect(id, 0));
packetListener.save(pre);
}
}
lastEffects = found;
*/
//Inventory Handling
if(playerItems[0] != mc.thePlayer.getHeldItem()) {
playerItems[0] = mc.thePlayer.getHeldItem();
S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 0, playerItems[0]);
packetListener.save(pee);
}
if(playerItems[1] != mc.thePlayer.inventory.armorInventory[0]) {
playerItems[1] = mc.thePlayer.inventory.armorInventory[0];
S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 1, playerItems[1]);
packetListener.save(pee);
}
if(playerItems[2] != mc.thePlayer.inventory.armorInventory[1]) {
playerItems[2] = mc.thePlayer.inventory.armorInventory[1];
S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 2, playerItems[2]);
packetListener.save(pee);
}
if(playerItems[3] != mc.thePlayer.inventory.armorInventory[2]) {
playerItems[3] = mc.thePlayer.inventory.armorInventory[2];
S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 3, playerItems[3]);
packetListener.save(pee);
}
if(playerItems[4] != mc.thePlayer.inventory.armorInventory[3]) {
playerItems[4] = mc.thePlayer.inventory.armorInventory[3];
S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 4, playerItems[4]);
packetListener.save(pee);
}
//Leaving Ride
if((!mc.thePlayer.isRiding() && lastRiding != -1) ||
(mc.thePlayer.isRiding() && lastRiding != mc.thePlayer.ridingEntity.getEntityId())) {
if(!mc.thePlayer.isRiding()) {
lastRiding = -1;
} else {
lastRiding = mc.thePlayer.ridingEntity.getEntityId();
}
S1BPacketEntityAttach pea = new S1BPacketEntityAttach();
ByteBuf buf = Unpooled.buffer();
PacketBuffer pbuf = new PacketBuffer(buf);
pbuf.writeInt(e.player.getEntityId());
pbuf.writeInt(lastRiding);
pbuf.writeBoolean(false);
pea.readPacketData(pbuf);
packetListener.save(pea);
}
//Sleeping
if(!mc.thePlayer.isPlayerSleeping() && wasSleeping) {
S0BPacketAnimation pac = new S0BPacketAnimation();
ByteBuf bb = Unpooled.buffer();
PacketBuffer pb = new PacketBuffer(bb);
pb.writeVarIntToBuffer(e.player.getEntityId());
pb.writeByte(2);
pac.readPacketData(pb);
packetListener.save(pac);
wasSleeping = false;
}
} catch(Exception e1) {
e1.printStackTrace();
}
}
@SubscribeEvent
public void onPickupItem(ItemPickupEvent event) {
try {
packetListener.save(new S0DPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId()));
} catch(Exception e) {
e.printStackTrace();
}
}
@SubscribeEvent
public void onStartEating(PlayerUseItemEvent.Start event) {
try {
if(!event.entityPlayer.isEating()) return;
S0BPacketAnimation packet = new S0BPacketAnimation();
ByteBuf bb = Unpooled.buffer();
PacketBuffer pb = new PacketBuffer(bb);
pb.writeVarIntToBuffer(event.entityPlayer.getEntityId());
pb.writeByte(3);
packet.readPacketData(pb);
packetListener.save(packet);
} catch(Exception e) {
e.printStackTrace();
}
}
@SubscribeEvent
public void onSleep(PlayerSleepInBedEvent event) {
try {
if(event.entityPlayer != mc.thePlayer) {
return;
}
S0APacketUseBed pub = new S0APacketUseBed();
ByteBuf buf = Unpooled.buffer();
PacketBuffer pbuf = new PacketBuffer(buf);
pbuf.writeVarIntToBuffer(event.entityPlayer.getEntityId());
pbuf.writeBlockPos(event.pos);
pub.readPacketData(pbuf);
packetListener.save(pub);
wasSleeping = true;
} catch(Exception e) {
e.printStackTrace();
}
}
@SubscribeEvent
public void enterMinecart(MinecartInteractEvent event) {
try {
if(event.player != mc.thePlayer) {
return;
}
S1BPacketEntityAttach pea = new S1BPacketEntityAttach();
ByteBuf buf = Unpooled.buffer();
PacketBuffer pbuf = new PacketBuffer(buf);
pbuf.writeInt(event.player.getEntityId());
pbuf.writeInt(event.minecart.getEntityId());
pbuf.writeBoolean(false);
pea.readPacketData(pbuf);
packetListener.save(pea);
lastRiding = event.minecart.getEntityId();
} catch(Exception e) {
e.printStackTrace();
}
}
public void onBlockBreakAnim(int breakerId, BlockPos pos, int progress) {
EntityPlayer thePlayer = mc.thePlayer;
if (thePlayer != null && breakerId == thePlayer.getEntityId()) {
packetListener.save(new S25PacketBlockBreakAnim(breakerId, pos, progress));
}
}
@SubscribeEvent
public void checkForGamePaused(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (mc.isIntegratedServerRunning()) {
IntegratedServer server = mc.getIntegratedServer();
if (server != null && server.isGamePaused) {
packetListener.serverWasPaused = true;
}
}
}
public interface RecordingEventSender {
void setRecordingEventHandler(RecordingEventHandler recordingEventHandler);
RecordingEventHandler getRecordingEventHandler();
}
}

View File

@@ -0,0 +1,61 @@
package com.replaymod.recording.mixin;
import com.replaymod.recording.handler.RecordingEventHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.network.play.server.S07PacketRespawn;
import net.minecraft.network.play.server.S38PacketPlayerListItem;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
@Mixin(NetHandlerPlayClient.class)
public abstract class MixinNetHandlerPlayClient {
@Shadow
private Minecraft gameController;
public RecordingEventHandler getRecordingEventHandler() {
return ((RecordingEventHandler.RecordingEventSender) gameController.renderGlobal).getRecordingEventHandler();
}
/**
* Record the own player entity joining the world.
* We cannot use the {@link net.minecraftforge.event.entity.EntityJoinWorldEvent} because the entity id
* of the player is set afterwards and the tablist entry might not yet be sent.
* @param packet The packet
* @param ci Callback info
*/
@Inject(method = "handlePlayerListItem", at=@At("RETURN"))
public void recordOwnJoin(S38PacketPlayerListItem packet, CallbackInfo ci) {
RecordingEventHandler handler = getRecordingEventHandler();
if (handler != null && packet.func_179768_b() == S38PacketPlayerListItem.Action.ADD_PLAYER) {
@SuppressWarnings("unchecked")
List<S38PacketPlayerListItem.AddPlayerData> dataList = packet.func_179767_a();
for (S38PacketPlayerListItem.AddPlayerData data : dataList) {
if (data.func_179962_a().getId().equals(Minecraft.getMinecraft().thePlayer.getGameProfile().getId())) {
handler.onPlayerJoin();
}
}
}
}
/**
* Record the own player entity respawning.
* We cannot use the {@link net.minecraftforge.event.entity.EntityJoinWorldEvent} because that would also include
* the first spawn which is already handled by {@link #recordOwnJoin(S38PacketPlayerListItem, CallbackInfo)}.
* @param packet The packet
* @param ci Callback info
*/
@Inject(method = "handleRespawn", at=@At("RETURN"))
public void recordOwnRespawn(S07PacketRespawn packet, CallbackInfo ci) {
RecordingEventHandler handler = getRecordingEventHandler();
if (handler != null) {
handler.onPlayerRespawn();
}
}
}

View File

@@ -0,0 +1,32 @@
package com.replaymod.recording.mixin;
import com.replaymod.recording.handler.RecordingEventHandler;
import net.minecraft.client.renderer.RenderGlobal;
import net.minecraft.util.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(RenderGlobal.class)
public abstract class MixinRenderGlobal implements RecordingEventHandler.RecordingEventSender {
private RecordingEventHandler recordingEventHandler;
@Override
public void setRecordingEventHandler(RecordingEventHandler recordingEventHandler) {
this.recordingEventHandler = recordingEventHandler;
}
@Override
public RecordingEventHandler getRecordingEventHandler() {
return recordingEventHandler;
}
@Inject(method = "sendBlockBreakProgress", at = @At("HEAD"))
public void saveBlockBreakProgressPacket(int breakerId, BlockPos pos, int progress, CallbackInfo info) {
if (recordingEventHandler != null) {
recordingEventHandler.onBlockBreakAnim(breakerId, pos, progress);
}
}
}

View File

@@ -0,0 +1,182 @@
package com.replaymod.recording.packet;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import de.johni0702.replaystudio.data.Marker;
import de.johni0702.replaystudio.replay.ReplayFile;
import de.johni0702.replaystudio.replay.ReplayMetaData;
import com.replaymod.core.ReplayMod;
import eu.crushedpixel.replaymod.chat.ChatMessageHandler;
import eu.crushedpixel.replaymod.holders.AdvancedPosition;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import net.minecraft.client.Minecraft;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public abstract class DataListener extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger();
private final ReplayFile replayFile;
protected final DataWriter dataWriter;
protected Long startTime = null;
protected String name;
protected String worldName;
public boolean serverWasPaused;
protected long lastSentPacket = 0;
protected boolean alive = true;
protected Set<String> players = new HashSet<>();
private boolean singleplayer;
private final Set<Marker> markers = new HashSet<>();
private final Map<Integer, String> requestToHash = new ConcurrentHashMap<>();
public DataListener(ReplayFile replayFile, String name, String worldName, long startTime, boolean singleplayer) throws IOException {
this.replayFile = replayFile;
this.startTime = startTime;
this.name = name;
this.worldName = worldName;
this.singleplayer = singleplayer;
dataWriter = new DataWriter();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
dataWriter.requestFinish();
}
protected void recordResourcePack(File file, int requestId) {
try {
String hash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString();
synchronized (dataWriter) {
if (!requestToHash.containsValue(hash)) {
try (OutputStream out = replayFile.writeResourcePack(hash)) {
FileUtils.copyFile(file, out);
}
}
}
requestToHash.put(requestId, hash);
} catch (IOException e) {
logger.warn("Failed to save resource pack.", e);
}
}
public void addMarker() {
AdvancedPosition pos = new AdvancedPosition(Minecraft.getMinecraft().getRenderViewEntity());
int timestamp = (int) (System.currentTimeMillis() - startTime);
Marker marker = new Marker();
marker.setTime(timestamp);
marker.setX(pos.getX());
marker.setY(pos.getY());
marker.setZ(pos.getZ());
marker.setYaw((float) pos.getYaw());
marker.setPitch((float) pos.getPitch());
marker.setRoll((float) pos.getRoll());
markers.add(marker);
ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.addedmarker", ChatMessageHandler.ChatMessageType.INFORMATION);
}
public class DataWriter {
private long paused;
private DataOutputStream stream;
private ExecutorService saveService = Executors.newSingleThreadExecutor();
private final Thread shutdownHook = new Thread(new Runnable() {
@Override
public void run() {
if (!saveService.isShutdown()) {
requestFinish();
}
}
}, "shutdown-hook-data-listener");
public DataWriter() throws IOException {
stream = new DataOutputStream(replayFile.writePacketData());
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
public synchronized void writePacket(final byte[] bytes) {
long now = System.currentTimeMillis();
if(startTime == null) {
startTime = now;
}
if (serverWasPaused) {
paused = now - startTime - lastSentPacket;
serverWasPaused = false;
}
final int timestamp = (int) (now - startTime - paused);
lastSentPacket = timestamp;
saveService.submit(new Runnable() {
@Override
public void run() {
try {
stream.writeInt(timestamp);
stream.writeInt(bytes.length);
stream.write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
public synchronized void requestFinish() {
if (saveService.isShutdown()) return;
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
saveService.shutdown();
saveService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
stream.close();
// TODO: Get rid of replay file appender
ReplayMod.replayFileAppender.startNewReplayFileWriting();
String mcversion = ReplayMod.getMinecraftVersion();
String[] pl = players.toArray(new String[players.size()]);
String generator = "ReplayMod v" + ReplayMod.getContainer().getVersion();
ReplayMetaData metaData = new ReplayMetaData();
metaData.setSingleplayer(singleplayer);
metaData.setServerName(worldName);
metaData.setGenerator(generator);
metaData.setDuration((int) lastSentPacket);
metaData.setDate(startTime);
metaData.setPlayers(pl);
metaData.setMcVersion(mcversion);
replayFile.writeMetaData(metaData);
replayFile.writeMarkers(markers);
replayFile.writeResourcePackIndex(requestToHash);
replayFile.save();
} catch(Exception e) {
e.printStackTrace();
} finally {
ReplayMod.replayFileAppender.replayFileWritingFinished();
}
}
}
}

View File

@@ -0,0 +1,298 @@
package com.replaymod.recording.packet;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import de.johni0702.replaystudio.replay.ReplayFile;
import eu.crushedpixel.replaymod.replay.Restrictions;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import io.netty.channel.ChannelHandlerContext;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreenWorking;
import net.minecraft.client.gui.GuiYesNo;
import net.minecraft.client.gui.GuiYesNoCallback;
import net.minecraft.client.multiplayer.ServerData;
import net.minecraft.client.multiplayer.ServerList;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.client.resources.I18n;
import net.minecraft.client.resources.ResourcePackRepository;
import net.minecraft.entity.DataWatcher;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.Packet;
import net.minecraft.network.play.client.C19PacketResourcePackStatus;
import net.minecraft.network.play.server.*;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.HttpUtil;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class PacketListener extends DataListener {
private static final Minecraft mc = Minecraft.getMinecraft();
private static final Logger logger = LogManager.getLogger();
private ChannelHandlerContext context = null;
private int nextRequestId;
public PacketListener(ReplayFile file, String name, String worldName, long startTime, boolean singleplayer) throws IOException {
super(file, name, worldName, startTime, singleplayer);
}
public void save(Packet packet) {
try {
if(packet instanceof S0CPacketSpawnPlayer) {
UUID uuid = ((S0CPacketSpawnPlayer) packet).func_179819_c();
players.add(uuid.toString());
}
dataWriter.writePacket(getPacketData(packet));
} catch(Exception e) {
e.printStackTrace();
}
}
@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;
if(packet instanceof S0DPacketCollectItem) {
if(mc.thePlayer != null ||
((S0DPacketCollectItem) packet).func_149353_d() == mc.thePlayer.getEntityId()) {
super.channelRead(ctx, msg);
return;
}
}
if(packet instanceof S0CPacketSpawnPlayer) {
UUID uuid = ((S0CPacketSpawnPlayer) packet).func_179819_c();
players.add(uuid.toString());
}
if (packet instanceof S48PacketResourcePackSend) {
S48PacketResourcePackSend p = (S48PacketResourcePackSend) packet;
int requestId = handleResourcePack(p);
save(new S48PacketResourcePackSend("replay://" + requestId, ""));
return;
}
dataWriter.writePacket(getPacketData(packet));
if (packet instanceof S3FPacketCustomPayload) {
S3FPacketCustomPayload p = (S3FPacketCustomPayload) packet;
if (Restrictions.PLUGIN_CHANNEL.equals(p.getChannelName())) {
packet = new S40PacketDisconnect(new ChatComponentText("Please update to view this replay."));
dataWriter.writePacket(getPacketData(packet));
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
super.channelRead(ctx, msg);
}
@SuppressWarnings("unchecked")
private byte[] getPacketData(Packet packet) {
if(packet instanceof S0FPacketSpawnMob) {
S0FPacketSpawnMob p = (S0FPacketSpawnMob) packet;
if (p.field_149043_l == null) {
p.field_149043_l = new DataWatcher(null);
if(p.func_149027_c() != null) {
for(DataWatcher.WatchableObject wo : (List<DataWatcher.WatchableObject>) p.func_149027_c()) {
p.field_149043_l.addObject(wo.getDataValueId(), wo.getObject());
}
}
}
}
if(packet instanceof S0CPacketSpawnPlayer) {
S0CPacketSpawnPlayer p = (S0CPacketSpawnPlayer) packet;
if (p.field_148960_i == null) {
p.field_148960_i = new DataWatcher(null);
if(p.func_148944_c() != null) {
for(DataWatcher.WatchableObject wo : (List<DataWatcher.WatchableObject>) p.func_148944_c()) {
p.field_148960_i.addObject(wo.getDataValueId(), wo.getObject());
}
}
}
}
return ReplayFileIO.serializePacket(packet);
}
public synchronized int handleResourcePack(S48PacketResourcePackSend packet) {
final int requestId = nextRequestId++;
final NetHandlerPlayClient netHandler = mc.getNetHandler();
final NetworkManager netManager = netHandler.getNetworkManager();
final String url = packet.func_179783_a();
final String hash = packet.func_179784_b();
if (url.startsWith("level://")) {
String levelName = url.substring("level://".length());
File savesDir = new File(mc.mcDataDir, "saves");
final File levelDir = new File(savesDir, levelName);
if (levelDir.isFile()) {
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED));
Futures.addCallback(mc.getResourcePackRepository().func_177319_a(levelDir), new FutureCallback() {
@Override
public void onSuccess(Object result) {
recordResourcePack(levelDir, requestId);
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED));
}
@Override
public void onFailure(@Nonnull Throwable throwable) {
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD));
}
});
} else {
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD));
}
} else {
final ServerData serverData = mc.getCurrentServerData();
if (serverData != null && serverData.getResourceMode() == ServerData.ServerResourceMode.ENABLED) {
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED));
downloadResourcePackFuture(requestId, url, hash);
} else if (serverData != null && serverData.getResourceMode() != ServerData.ServerResourceMode.PROMPT) {
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED));
} else {
mc.addScheduledTask(new Runnable() {
@Override
public void run() {
mc.displayGuiScreen(new GuiYesNo(new GuiYesNoCallback() {
@Override
public void confirmClicked(boolean result, int id) {
if (serverData != null) {
serverData.setResourceMode(result ? ServerData.ServerResourceMode.ENABLED : ServerData.ServerResourceMode.DISABLED);
}
if (result) {
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED));
downloadResourcePackFuture(requestId, url, hash);
} else {
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED));
}
ServerList.func_147414_b(serverData);
mc.displayGuiScreen(null);
}
}, I18n.format("multiplayer.texturePrompt.line1"), I18n.format("multiplayer.texturePrompt.line2"), 0));
}
});
}
}
return requestId;
}
private void downloadResourcePackFuture(int requestId, String url, final String hash) {
Futures.addCallback(downloadResourcePack(requestId, url, hash), new FutureCallback() {
@Override
public void onSuccess(Object result) {
mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED));
}
@Override
public void onFailure(@Nonnull Throwable throwable) {
mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD));
}
});
}
private ListenableFuture downloadResourcePack(final int requestId, String url, String hash) {
final ResourcePackRepository repo = mc.mcResourcePackRepository;
String fileName;
if (hash.matches("^[a-f0-9]{40}$")) {
fileName = hash;
} else {
fileName = url.substring(url.lastIndexOf("/") + 1);
if (fileName.contains("?")) {
fileName = fileName.substring(0, fileName.indexOf("?"));
}
if (!fileName.endsWith(".zip")) {
return Futures.immediateFailedFuture(new IllegalArgumentException("Invalid filename; must end in .zip"));
}
fileName = "legacy_" + fileName.replaceAll("\\W", "");
}
final File file = new File(repo.dirServerResourcepacks, fileName);
repo.field_177321_h.lock();
try {
repo.func_148529_f();
if (file.exists() && hash.length() == 40) {
try {
String fileHash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString();
if (fileHash.equals(hash)) {
recordResourcePack(file, requestId);
return repo.func_177319_a(file);
}
logger.warn("File " + file + " had wrong hash (expected " + hash + ", found " + fileHash + "). Deleting it.");
FileUtils.deleteQuietly(file);
} catch (IOException ioexception) {
logger.warn("File " + file + " couldn\'t be hashed. Deleting it.", ioexception);
FileUtils.deleteQuietly(file);
}
}
final GuiScreenWorking guiScreen = new GuiScreenWorking();
final Minecraft mc = Minecraft.getMinecraft();
Futures.getUnchecked(mc.addScheduledTask(new Runnable() {
@Override
public void run() {
mc.displayGuiScreen(guiScreen);
}
}));
Map sessionInfo = Minecraft.getSessionInfo();
repo.field_177322_i = HttpUtil.func_180192_a(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy());
Futures.addCallback(repo.field_177322_i, new FutureCallback() {
@Override
public void onSuccess(Object value) {
recordResourcePack(file, requestId);
repo.func_177319_a(file);
}
@Override
public void onFailure(@Nonnull Throwable throwable) {
throwable.printStackTrace();
}
});
return repo.field_177322_i;
} finally {
repo.field_177321_h.unlock();
}
}
}

View File

@@ -0,0 +1,38 @@
package com.replaymod.recording.packet;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import net.minecraft.network.*;
import net.minecraft.util.MessageSerializer;
import java.io.IOException;
public class PacketSerializer extends MessageSerializer {
public PacketSerializer(EnumPacketDirection direction) {
super(direction);
}
@Override
public void encode(ChannelHandlerContext ctx, Packet packet, ByteBuf byteBuf) throws IOException {
EnumConnectionState state = ((EnumConnectionState) ctx.channel().attr(NetworkManager.attrKeyConnectionState).get());
encode(state, packet, byteBuf);
}
public void encode(EnumConnectionState state, Packet packet, ByteBuf byteBuf) {
Integer integer = state.getPacketId(EnumPacketDirection.CLIENTBOUND, packet);
if (integer != null) {
PacketBuffer packetbuffer = new PacketBuffer(byteBuf);
packetbuffer.writeVarIntToBuffer(integer);
try {
packet.writePacketData(packetbuffer);
} catch(Throwable throwable) {
throwable.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,276 @@
package com.replaymod.replay;
import eu.crushedpixel.replaymod.holders.AdvancedPosition;
import eu.crushedpixel.replaymod.replay.LesserDataWatcher;
import net.minecraft.block.material.Material;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityPlayerSP;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.entity.Entity;
import net.minecraft.stats.StatFileWriter;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MathHelper;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import org.lwjgl.Sys;
import java.util.UUID;
public class CameraEntity extends EntityPlayerSP {
public static final double SPEED_CHANGE = 0.5;
public static final double LOWER_SPEED = 2;
public static final double UPPER_SPEED = 20;
private static double MAX_SPEED = 10;
private static double THRESHOLD = MAX_SPEED / 20;
private static double DECAY = MAX_SPEED/3;
public static void modifyCameraSpeed(boolean increase) {
setCameraMaximumSpeed(getCameraMaximumSpeed() + (increase ? 1 : -1) * SPEED_CHANGE);
}
public static void setCameraMaximumSpeed(double maxSpeed) {
if(maxSpeed < LOWER_SPEED || maxSpeed > UPPER_SPEED) return;
MAX_SPEED = maxSpeed;
THRESHOLD = MAX_SPEED / 20;
DECAY = 5;
}
public static double getCameraMaximumSpeed() {
return MAX_SPEED;
}
private Vec3 direction;
private Vec3 dirBefore;
private double motion;
public float roll;
private long lastCall = 0;
private boolean speedup = false;
public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatFileWriter statFileWriter) {
super(mcIn, worldIn, netHandlerPlayClient, statFileWriter);
}
//frac = time since last tick
public void updateMovement() {
long frac = Sys.getTime() - lastCall;
if(frac == 0) return;
double decFac = Math.max(0, 1 - (DECAY * (frac / 1000D)));
if(speedup) {
if(motion < THRESHOLD) motion = THRESHOLD;
motion /= decFac;
} else {
motion *= decFac;
}
motion = Math.min(motion, MAX_SPEED);
lastCall = Sys.getTime();
if(direction == null || motion < THRESHOLD) {
return;
}
Vec3 movement = direction.normalize();
double factor = motion * (frac / 1000D);
moveRelative(movement.xCoord * factor, movement.yCoord * factor, movement.zCoord * factor);
}
public void speedUp() {
speedup = true;
}
public void stopSpeedUp() {
speedup = false;
}
public void setMovement(MoveDirection dir) {
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;
}
Vec3 dbf = direction;
if(dirBefore != null) {
direction = dirBefore.normalize().add(direction);
}
dirBefore = dbf;
updateMovement();
}
public void moveAbsolute(AdvancedPosition pos) {
this.moveAbsolute(pos.getX(), pos.getY(), pos.getZ());
rotationPitch = prevRotationPitch = (float)pos.getPitch();
rotationYaw = prevRotationYaw = (float)pos.getYaw();
roll = (float) pos.getRoll();
updateBoundingBox();
}
public void moveAbsolute(double x, double y, double z) {
this.lastTickPosX = this.prevPosX = this.posX = x;
this.lastTickPosY = this.prevPosY = this.posY = y;
this.lastTickPosZ = this.prevPosZ = this.posZ = z;
updateBoundingBox();
}
public void moveRelative(double x, double y, double z) {
this.lastTickPosX = this.prevPosX = this.posX = this.posX + x;
this.lastTickPosY = this.prevPosY = this.posY = this.posY + y;
this.lastTickPosZ = this.prevPosZ = this.posZ = this.posZ + z;
updateBoundingBox();
}
public void movePath(AdvancedPosition pos) {
this.prevRotationPitch = this.rotationPitch = (float)pos.getPitch();
this.prevRotationYaw = this.rotationYaw = (float)pos.getYaw();
this.roll = (float) pos.getRoll();
this.lastTickPosX = this.prevPosX = this.posX = pos.getX();
this.lastTickPosY = this.prevPosY = this.posY = pos.getY();
this.lastTickPosZ = this.prevPosZ = this.posZ = pos.getZ();
updateBoundingBox();
}
private void updateBoundingBox() {
this.setEntityBoundingBox(new AxisAlignedBB(posX - width / 2, posY, posZ - width / 2,
posX + width / 2, posY + height, posZ + width / 2));
}
protected void updatePos(Entity to) {
prevPosX = to.prevPosX;
prevPosY = to.prevPosY;
prevPosZ = to.prevPosZ;
prevRotationYaw = to.prevRotationYaw;
prevRotationPitch = to.prevRotationPitch;
posX = to.posX;
posY = to.posY;
posZ = to.posZ;
rotationYaw = to.rotationYaw;
rotationPitch = to.rotationPitch;
}
@Override
protected void entityInit() {
this.dataWatcher = new LesserDataWatcher(this);
this.setSize(0.6f, 1.8f);
updateBoundingBox();
}
@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.prevRotationYawHead = this.rotationYawHead = this.prevRotationYaw = this.rotationYaw;
}
@Override
public void onUpdate() {
Entity view = mc.getRenderViewEntity();
if (view != null) {
UUID spectating = ReplayModReplay.instance.getReplayHandler().spectating;
if (spectating != null && (view.getUniqueID() != spectating || view.worldObj != worldObj)) {
view = worldObj.getPlayerEntityByUUID(spectating);
if (view != null) {
mc.setRenderViewEntity(view);
} else {
mc.setRenderViewEntity(this);
return;
}
}
if (view != this) {
updatePos(view);
}
}
}
@Override
public void preparePlayerToSpawn() {
if (mc.theWorld != null) {
worldObj = mc.theWorld;
}
super.preparePlayerToSpawn();
}
@Override
public boolean isEntityInsideOpaqueBlock() {
return false;
}
@Override
public boolean isInsideOfMaterial(Material materialIn) {
return false;
}
@Override
public boolean isInLava() {
return false;
}
@Override
public boolean isInWater() {
return false;
}
@Override
public boolean canBePushed() {
return false;
}
@Override
protected void createRunningParticles() {}
@Override
public boolean canBeCollidedWith() {
return false;
}
@Override
public boolean isSpectator() {
return true;
}
public enum MoveDirection {
UP, DOWN, LEFT, RIGHT, FORWARD, BACKWARD
}
@Override
public MovingObjectPosition rayTrace(double p_174822_1_, float p_174822_3_) {
MovingObjectPosition pos = super.rayTrace(p_174822_1_, 1f);
if(pos != null && pos.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
pos.typeOfHit = MovingObjectPosition.MovingObjectType.MISS;
}
return pos;
}
}

View File

@@ -0,0 +1,349 @@
package com.replaymod.replay;
import com.google.common.base.Preconditions;
import com.mojang.authlib.GameProfile;
import com.replaymod.core.ReplayMod;
import com.replaymod.replay.events.ReplayCloseEvent;
import com.replaymod.replay.events.ReplayOpenEvent;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import de.johni0702.replaystudio.data.Marker;
import de.johni0702.replaystudio.replay.ReplayFile;
import eu.crushedpixel.replaymod.holders.AdvancedPosition;
import eu.crushedpixel.replaymod.registry.ReplayGuiRegistry;
import eu.crushedpixel.replaymod.replay.Restrictions;
import eu.crushedpixel.replaymod.utils.MouseUtils;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityOtherPlayerMP;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.client.resources.I18n;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.network.EnumPacketDirection;
import net.minecraft.network.NetworkManager;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher;
import org.lwjgl.opengl.Display;
import org.lwjgl.util.Point;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public class ReplayHandler {
private static Minecraft mc = Minecraft.getMinecraft();
/**
* The file currently being played.
*/
private final ReplayFile replayFile;
/**
* Decodes and sends packets into channel.
*/
private final ReplaySender replaySender;
/**
* Currently active replay restrictions.
*/
private Restrictions restrictions = new Restrictions();
/**
* Whether camera movements by user input and/or server packets should be suppressed.
*/
private boolean suppressCameraMovements;
private final Set<Marker> markers;
private final GuiReplayOverlay overlay;
private EmbeddedChannel channel;
/**
* The position at which the camera should be located after the next jump.
*/
private AdvancedPosition targetCameraPosition;
protected UUID spectating;
public ReplayHandler(ReplayFile replayFile, boolean asyncMode) throws IOException {
Preconditions.checkState(mc.isCallingFromMinecraftThread(), "Must be called from Minecraft thread.");
this.replayFile = replayFile;
FMLCommonHandler.instance().bus().post(new ReplayOpenEvent.Pre(this));
markers = replayFile.getMarkers().or(Collections.<Marker>emptySet());
// TODO: get rid of chat message handler
ReplayMod.chatMessageHandler.initialize();
replaySender = new ReplaySender(this, replayFile, asyncMode);
setup();
overlay = new GuiReplayOverlay(this);
overlay.setVisible(true);
FMLCommonHandler.instance().bus().post(new ReplayOpenEvent.Post(this));
}
void restartedReplay() {
channel.close();
restrictions = new Restrictions();
setup();
}
public void endReplay() throws IOException {
Preconditions.checkState(mc.isCallingFromMinecraftThread(), "Must be called from Minecraft thread.");
FMLCommonHandler.instance().bus().post(new ReplayCloseEvent.Pre(this));
replaySender.terminateReplay();
replayFile.save();
replayFile.close();
channel.close().awaitUninterruptibly();
if (mc.theWorld != null) {
mc.theWorld.sendQuittingDisconnectingPacket();
mc.loadWorld(null);
}
overlay.setVisible(false);
// These might have been hidden by the overlay, so we have to make sure they're visible again
ReplayGuiRegistry.show();
FMLCommonHandler.instance().bus().post(new ReplayCloseEvent.Post(this));
}
private void setup() {
mc.ingameGUI.getChatGUI().clearChatMessages();
NetworkManager networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
t.printStackTrace();
}
};
networkManager.setNetHandler(new NetHandlerPlayClient(
mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player")));
channel = new EmbeddedChannel(networkManager);
channel.attr(NetworkDispatcher.FML_DISPATCHER).set(new NetworkDispatcher(networkManager));
channel.pipeline().addFirst(replaySender);
channel.pipeline().fireChannelActive();
}
public ReplayFile getReplayFile() {
return replayFile;
}
public Restrictions getRestrictions() {
return restrictions;
}
public ReplaySender getReplaySender() {
return replaySender;
}
public GuiReplayOverlay getOverlay() {
return overlay;
}
/**
* Return whether camera movement by user inputs and/or server packets should be suppressed.
* @return {@code true} if these kinds of movement should be suppressed
*/
public boolean shouldSuppressCameraMovements() {
return suppressCameraMovements;
}
/**
* Set whether camera movement by user inputs and/or server packets should be suppressed.
* @param suppressCameraMovements {@code true} to suppress these kinds of movement, {@code false} to allow them
*/
public void setSuppressCameraMovements(boolean suppressCameraMovements) {
this.suppressCameraMovements = suppressCameraMovements;
}
/**
* Returns all markers.
* When changed, {@link #saveMarkers()} should be called to save the changes.
* @return Set of markers
*/
public Set<Marker> getMarkers() {
return markers;
}
/**
* Saves all markers.
*/
public void saveMarkers() {
try {
replayFile.writeMarkers(markers);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Spectate the specified entity.
* When the entity is {@code null} or the camera entity, the camera becomes the view entity.
* @param e The entity to spectate
*/
public void spectateEntity(Entity e) {
CameraEntity cameraEntity = getCameraEntity();
if (e == null || e == cameraEntity) {
spectating = null;
e = cameraEntity;
} else if (e instanceof EntityPlayer) {
spectating = e.getUniqueID();
}
if (mc.getRenderViewEntity() != e) {
mc.setRenderViewEntity(e);
cameraEntity.updatePos(e);
}
}
/**
* Set the camera as the view entity.
* This is equivalent to {@link #spectateEntity(Entity) spectateEntity(null)}.
*/
public void spectateCamera() {
spectateEntity(null);
}
/**
* Returns whether the current view entity is the camera entity.
* @return {@code true} if the camera is the view entity, {@code false} otherwise
*/
public boolean isCameraView() {
return mc.thePlayer instanceof CameraEntity && mc.thePlayer == mc.getRenderViewEntity();
}
/**
* Returns the camera entity.
* @return The camera entity or {@code null} if it does not yet exist
*/
public CameraEntity getCameraEntity() {
return mc.thePlayer instanceof CameraEntity ? (CameraEntity) mc.thePlayer : null;
}
public void setTargetPosition(AdvancedPosition pos) {
targetCameraPosition = pos;
}
public void moveCameraToTargetPosition() {
CameraEntity cam = getCameraEntity();
if (cam != null && targetCameraPosition != null) {
cam.moveAbsolute(targetCameraPosition);
}
}
public void doJump(int targetTime, boolean retainCameraPosition) {
if (replaySender.isHurrying()) {
return; // When hurrying, no Timeline jumping etc. is possible
}
if (targetTime < replaySender.currentTimeStamp()) {
mc.displayGuiScreen(null);
}
if (retainCameraPosition) {
CameraEntity cam = getCameraEntity();
if (cam != null) {
targetCameraPosition = new AdvancedPosition(cam.posX, cam.posY, cam.posZ,
cam.rotationPitch, cam.rotationYaw, cam.roll);
} else {
targetCameraPosition = null;
}
}
long diff = targetTime - replaySender.getDesiredTimestamp();
if (diff != 0) {
if (diff > 0 && diff < 5000) { // Small difference and no time travel
replaySender.jumpToTime(targetTime);
} else { // We either have to restart the replay or send a significant amount of packets
// Render our please-wait-screen
GuiScreen guiScreen = new GuiScreen() {
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
drawBackground(0);
drawCenteredString(fontRendererObj, I18n.format("replaymod.gui.pleasewait"),
width / 2, height / 2, 0xffffffff);
}
};
// Make sure that the replaysender changes into sync mode
replaySender.setSyncModeAndWait();
// Perform the rendering using OpenGL
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
mc.getFramebuffer().bindFramebuffer(true);
mc.entityRenderer.setupOverlayRendering();
Point point = MouseUtils.getScaledDimensions();
guiScreen.setWorldAndResolution(mc, point.getX(), point.getY());
guiScreen.drawScreen(0, 0, 0);
mc.getFramebuffer().unbindFramebuffer();
popMatrix();
pushMatrix();
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
popMatrix();
Display.update();
// Send the packets
ReplayMod.replaySender.sendPacketsTill(targetTime);
ReplayMod.replaySender.setAsyncMode(true);
ReplayMod.replaySender.setReplaySpeed(0);
mc.getNetHandler().getNetworkManager().processReceivedPackets();
@SuppressWarnings("unchecked")
List<Entity> entities = (List<Entity>) mc.theWorld.loadedEntityList;
for (Entity entity : entities) {
if (entity instanceof EntityOtherPlayerMP) {
EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity;
e.setPosition(e.otherPlayerMPX, e.otherPlayerMPY, e.otherPlayerMPZ);
e.rotationYaw = (float) e.otherPlayerMPYaw;
e.rotationPitch = (float) e.otherPlayerMPPitch;
}
entity.lastTickPosX = entity.prevPosX = entity.posX;
entity.lastTickPosY = entity.prevPosY = entity.posY;
entity.lastTickPosZ = entity.prevPosZ = entity.posZ;
entity.prevRotationYaw = entity.rotationYaw;
entity.prevRotationPitch = entity.rotationPitch;
}
try {
mc.runTick();
} catch (IOException e) {
e.printStackTrace(); // This should never be thrown but whatever
}
//finally, updating the camera's position (which is not done by the sync jumping)
moveCameraToTargetPosition();
// No need to remove our please-wait-screen. It'll vanish with the next
// render pass as it's never been a real GuiScreen in the first place.
}
}
}
}

View File

@@ -0,0 +1,58 @@
package com.replaymod.replay;
import com.replaymod.core.ReplayMod;
import com.replaymod.replay.handler.GuiHandler;
import de.johni0702.replaystudio.replay.ReplayFile;
import de.johni0702.replaystudio.replay.ZipReplayFile;
import de.johni0702.replaystudio.studio.ReplayStudio;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard;
import java.io.File;
import java.io.IOException;
@Mod(modid = ReplayModReplay.MOD_ID, useMetadata = true)
public class ReplayModReplay {
public static final String MOD_ID = "replaymod-replay";
@Mod.Instance(MOD_ID)
public static ReplayModReplay instance;
@Mod.Instance(ReplayMod.MOD_ID)
private static ReplayMod core;
private Logger logger;
private ReplayHandler replayHandler;
public ReplayHandler getReplayHandler() {
return replayHandler;
}
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog();
core.getSettingsRegistry().register(Setting.class);
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() {
@Override
public void run() {
}
});
}
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
new GuiHandler(this).register();
}
public void startReplay(File file) throws IOException {
ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file);
replayHandler = new ReplayHandler(replayFile, true);
}
}

View File

@@ -0,0 +1,820 @@
package com.replaymod.replay;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import com.google.common.util.concurrent.ListenableFutureTask;
import de.johni0702.replaystudio.replay.ReplayFile;
import com.replaymod.core.ReplayMod;
import eu.crushedpixel.replaymod.holders.PacketData;
import eu.crushedpixel.replaymod.replay.Restrictions;
import eu.crushedpixel.replaymod.settings.ReplaySettings;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiDownloadTerrain;
import net.minecraft.client.gui.GuiErrorScreen;
import net.minecraft.client.resources.I18n;
import net.minecraft.entity.Entity;
import net.minecraft.network.EnumConnectionState;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.Packet;
import net.minecraft.network.play.server.*;
import net.minecraft.util.IChatComponent;
import net.minecraft.world.EnumDifficulty;
import net.minecraft.world.World;
import net.minecraft.world.WorldSettings.GameType;
import net.minecraft.world.WorldType;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* Sends replay packets to netty channels.
* Even though {@link Sharable}, this should never be added to multiple pipes at once, it may however be re-added when
* the replay restart from the beginning.
*/
@Sharable
public class ReplaySender extends ChannelInboundHandlerAdapter {
/**
* Previously packets for the client player were inserted using one fixed entity id (this one).
* This is no longer the case however to provide backwards compatibility, we have to convert
* these old packets to use the normal entity id.
* Need to punch someone? -> CrushedPixel
*/
public static final int LEGACY_ENTITY_ID = Integer.MIN_VALUE + 9001;
/**
* These packets are ignored completely during replay.
*/
private static final List<Class> BAD_PACKETS = Arrays.<Class>asList(
S06PacketUpdateHealth.class,
S2DPacketOpenWindow.class,
S2EPacketCloseWindow.class,
S2FPacketSetSlot.class,
S30PacketWindowItems.class,
S36PacketSignEditorOpen.class,
S37PacketStatistics.class,
S1FPacketSetExperience.class,
S43PacketCamera.class,
S39PacketPlayerAbilities.class,
S45PacketTitle.class);
/**
* The replay handler responsible for the current replay.
*/
private final ReplayHandler replayHandler;
/**
* Whether to work in async mode.
*
* When in async mode, a separate thread send packets and waits according to their delays.
* This is default in normal playback mode.
*
* When in sync mode, no packets will be sent until {@link #sendPacketsTill(int)} is called.
* This is used during path playback and video rendering.
*/
protected boolean asyncMode;
/**
* Timestamp of the last packet sent in milliseconds since the start.
*/
protected int lastTimeStamp;
/**
* The replay file.
*/
protected ReplayFile replayFile;
/**
* The channel handler context used to send packets to minecraft.
*/
protected ChannelHandlerContext ctx;
/**
* The data input stream from which new packets are read.
* When accessing this stream make sure to synchronize on {@code this} as it's used from multiple threads.
*/
protected DataInputStream dis;
/**
* The next packet that should be sent.
* This is required as some actions such as jumping to a specified timestamp have to peek at the next packet.
*/
protected PacketData nextPacket;
/**
* Whether we need to restart the current replay. E.g. when jumping backwards in time
*/
protected boolean startFromBeginning = true;
/**
* Whether to terminate the replay. This only has an effect on the async mode and is {@code true} during sync mode.
*/
protected boolean terminate;
/**
* The speed of the replay. 1 is normal, 2 is twice as fast, 0.5 is half speed and 0 is frozen
*/
protected double replaySpeed = 1f;
/**
* Whether the world has been loaded and the dirt-screen should go away.
*/
protected boolean hasWorldLoaded;
/**
* The minecraft instance.
*/
protected Minecraft mc = Minecraft.getMinecraft();
/**
* The total length of this replay in milliseconds.
*/
protected final int replayLength;
/**
* Our actual entity id that the server gave to us.
*/
protected int actualID = -1;
/**
* Whether to allow (process) the next player movement packet.
*/
protected boolean allowMovement;
/**
* Directory to which resource packs are extracted.
*/
private final File tempResourcePackFolder = Files.createTempDir();
/**
* Create a new replay sender.
* @param file The replay file
* @param asyncMode {@code true} for async mode, {@code false} otherwise
* @see #asyncMode
*/
public ReplaySender(ReplayHandler replayHandler, ReplayFile file, boolean asyncMode) throws IOException {
this.replayHandler = replayHandler;
this.replayFile = file;
this.asyncMode = asyncMode;
this.replayLength = file.getMetaData().getDuration();
if (asyncMode) {
new Thread(asyncSender, "replaymod-async-sender").start();
}
}
/**
* Set whether this replay sender operates in async mode.
* When in async mode, it will send packets timed from a separate thread.
* When not in async mode, it will send packets when {@link #sendPacketsTill(int)} is called.
* @param asyncMode {@code true} to enable async mode
*/
public void setAsyncMode(boolean asyncMode) {
if (this.asyncMode == asyncMode) return;
this.asyncMode = asyncMode;
if (asyncMode) {
this.terminate = false;
new Thread(asyncSender, "replaymod-async-sender").start();
} else {
this.terminate = true;
}
}
/**
* Set whether this replay sender to operate in sync mode.
* When in sync mode, it will send packets when {@link #sendPacketsTill(int)} is called.
* This call will block until the async worker thread has stopped.
*/
public void setSyncModeAndWait() {
if (!this.asyncMode) return;
this.asyncMode = false;
this.terminate = true;
synchronized (this) {
// This will wait for the worker thread to leave the synchronized code part
}
}
/**
* Return the timestamp of the last packet sent.
* @return The timestamp in milliseconds since the start of the replay
*/
public int currentTimeStamp() {
return lastTimeStamp;
}
/**
* Return the total length of the replay played.
* @return Total length in milliseconds
*/
public int replayLength() {
return replayLength;
}
/**
* Terminate this replay sender.
*/
public void terminateReplay() {
terminate = true;
try {
channelInactive(ctx);
ctx.channel().pipeline().close();
} catch(Exception e) {
e.printStackTrace();
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// When in async mode and the replay sender shut down, then don't send packets
if(terminate && asyncMode) {
return;
}
// When a packet is sent directly, perform no filtering
if(msg instanceof Packet) {
super.channelRead(ctx, msg);
}
if (msg instanceof byte[]) {
try {
Packet p = ReplayFileIO.deserializePacket((byte[]) msg);
if (p != null) {
p = processPacket(p);
if (p != null) {
super.channelRead(ctx, p);
}
// If we do not give minecraft time to tick, there will be dead entity artifacts left in the world
// Therefore we have to remove all loaded, dead entities manually if we are in sync mode.
// We do this after every SpawnX packet and after the destroy entities packet.
if (!asyncMode && mc.theWorld != null) {
if (p instanceof S0CPacketSpawnPlayer
|| p instanceof S0EPacketSpawnObject
|| p instanceof S0FPacketSpawnMob
|| p instanceof S2CPacketSpawnGlobalEntity
|| p instanceof S10PacketSpawnPainting
|| p instanceof S11PacketSpawnExperienceOrb
|| p instanceof S13PacketDestroyEntities) {
World world = mc.theWorld;
for (int i = 0; i < world.loadedEntityList.size(); ++i) {
Entity entity = (Entity) world.loadedEntityList.get(i);
if (entity.isDead) {
int chunkX = entity.chunkCoordX;
int chunkY = entity.chunkCoordZ;
if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) {
world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity);
}
world.loadedEntityList.remove(i--);
world.onEntityRemoved(entity);
}
}
}
}
}
} catch (Exception e) {
// We'd rather not have a failure parsing one packet screw up the whole replay process
e.printStackTrace();
}
}
}
/**
* Process a packet and return the result.
* @param p The packet to process
* @return The processed packet or {@code null} if no packet shall be sent
*/
protected Packet processPacket(Packet p) throws Exception {
if (p instanceof S3FPacketCustomPayload) {
S3FPacketCustomPayload packet = (S3FPacketCustomPayload) p;
if (Restrictions.PLUGIN_CHANNEL.equals(packet.getChannelName())) {
final String unknown = replayHandler.getRestrictions().handle(packet);
if (unknown == null) {
return null;
} else {
// Failed to parse options, make sure that under no circumstances further packets are parsed
terminateReplay();
// Then end replay and show error GUI
mc.addScheduledTask(new Runnable() {
@Override
public void run() {
try {
replayHandler.endReplay();
} catch (IOException e) {
e.printStackTrace();
}
mc.displayGuiScreen(new GuiErrorScreen(
I18n.format("replaymod.error.unknownrestriction1"),
I18n.format("replaymod.error.unknownrestriction2", unknown)
));
}
});
}
}
}
if (p instanceof S40PacketDisconnect) {
IChatComponent reason = ((S40PacketDisconnect) p).func_149165_c();
if ("Please update to view this replay.".equals(reason.getUnformattedText())) {
// This version of the mod supports replay restrictions so we are allowed
// to remove this packet.
return null;
}
}
if(BAD_PACKETS.contains(p.getClass())) return null;
convertLegacyEntityIds(p);
if(p instanceof S48PacketResourcePackSend) {
S48PacketResourcePackSend packet = (S48PacketResourcePackSend) p;
String url = packet.func_179783_a();
if (url.startsWith("replay://")) {
int id = Integer.parseInt(url.substring("replay://".length()));
Map<Integer, String> index = replayFile.getResourcePackIndex();
if (index != null) {
String hash = index.get(id);
if (hash != null) {
File file = new File(tempResourcePackFolder, hash + ".zip");
if (!file.exists()) {
IOUtils.copy(replayFile.getResourcePack(hash).get(), new FileOutputStream(file));
}
mc.getResourcePackRepository().func_177319_a(file);
}
}
return null;
}
}
if(p instanceof S01PacketJoinGame) {
S01PacketJoinGame packet = (S01PacketJoinGame) p;
allowMovement = true;
int entId = packet.getEntityId();
actualID = entId;
entId = -1789435; // Camera entity id should be negative which is an invalid id and can't be used by servers
int dimension = packet.getDimension();
EnumDifficulty difficulty = packet.getDifficulty();
int maxPlayers = packet.getMaxPlayers();
WorldType worldType = packet.getWorldType();
p = new S01PacketJoinGame(entId, GameType.SPECTATOR, false, dimension,
difficulty, maxPlayers, worldType, false);
}
if(p instanceof S07PacketRespawn) {
S07PacketRespawn respawn = (S07PacketRespawn) p;
p = new S07PacketRespawn(respawn.func_149082_c(),
respawn.func_149081_d(), respawn.func_149080_f(), GameType.SPECTATOR);
allowMovement = true;
}
if(p instanceof S08PacketPlayerPosLook) {
if(!hasWorldLoaded) hasWorldLoaded = true;
final S08PacketPlayerPosLook ppl = (S08PacketPlayerPosLook) p;
if (mc.currentScreen instanceof GuiDownloadTerrain) {
// Close the world loading screen manually in case we swallow the packet
mc.displayGuiScreen(null);
}
if(replayHandler.shouldSuppressCameraMovements()) return null;
CameraEntity cent = replayHandler.getCameraEntity();
for (Object relative : ppl.func_179834_f()) {
if (relative == S08PacketPlayerPosLook.EnumFlags.X
|| relative == S08PacketPlayerPosLook.EnumFlags.Y
|| relative == S08PacketPlayerPosLook.EnumFlags.Z) {
return null; // At least one of the coordinates is relative, so we don't care
}
}
if(cent != null) {
if(!allowMovement && !((Math.abs(cent.posX - ppl.func_148932_c()) > ReplayMod.TP_DISTANCE_LIMIT) ||
(Math.abs(cent.posZ - ppl.func_148933_e()) > ReplayMod.TP_DISTANCE_LIMIT))) {
return null;
} else {
allowMovement = false;
}
}
new Callable<Void>() {
@Override
@SuppressWarnings("unchecked")
public Void call() {
if (mc.theWorld == null || !mc.isCallingFromMinecraftThread()) {
synchronized(mc.scheduledTasks) {
mc.scheduledTasks.add(ListenableFutureTask.create(this));
}
return null;
}
CameraEntity cent = replayHandler.getCameraEntity();
cent.moveAbsolute(ppl.func_148932_c(), ppl.func_148928_d(), ppl.func_148933_e());
return null;
}
}.call();
}
if(p instanceof S2BPacketChangeGameState) {
S2BPacketChangeGameState pg = (S2BPacketChangeGameState)p;
int reason = pg.func_149138_c();
// only allow the following packets:
// 1 - End raining
// 2 - Begin raining
//
// The following values are to control sky color (e.g. if thunderstorm)
// 7 - Fade value
// 8 - Fade time
if(!(reason == 1 || reason == 2 || reason == 7 || reason == 8)) {
return null;
}
}
if (p instanceof S02PacketChat) {
if (ReplaySettings.ReplayOptions.showChat.getValue() != Boolean.TRUE) {
return null;
}
}
return asyncMode ? processPacketAsync(p) : processPacketSync(p);
}
@Override
@SuppressWarnings("unchecked")
public void channelActive(ChannelHandlerContext ctx) throws Exception {
this.ctx = ctx;
ctx.attr(NetworkManager.attrKeyConnectionState).set(EnumConnectionState.PLAY);
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
FileUtils.deleteDirectory(tempResourcePackFolder);
super.channelInactive(ctx);
}
/**
* Whether the replay is currently paused.
* @return {@code true} if it is paused, {@code false} otherwise
*/
public boolean paused() {
return mc.timer.timerSpeed == 0;
}
/**
* Returns the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast.
* If 0 is returned, the replay is paused.
* @return speed multiplier
*/
public double getReplaySpeed() {
if(!paused()) return replaySpeed;
else return 0;
}
/**
* Set the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast.
* The speed may not be set to 0 nor to negative values.
* @param d Speed multiplier
*/
public void setReplaySpeed(final double d) {
if(d != 0) this.replaySpeed = d;
mc.timer.timerSpeed = (float) d;
}
/////////////////////////////////////////////////////////
// Asynchronous packet processing //
/////////////////////////////////////////////////////////
/**
* The real time at which the last packet was sent in milliseconds.
*/
private long lastPacketSent;
/**
* There is no waiting performed until a packet with at least this timestamp is reached (but not yet sent).
* If this is -1, then timing is normal.
*/
private long desiredTimeStamp = -1;
/**
* Runnable which performs timed dispatching of packets from the input stream.
*/
private Runnable asyncSender = new Runnable() {
public void run() {
try {
while (ctx == null && !terminate) {
Thread.sleep(10);
}
REPLAY_LOOP:
while (!terminate) {
synchronized (ReplaySender.this) {
if (dis == null) {
dis = new DataInputStream(replayFile.getPacketData());
}
// Packet loop
while (true) {
try {
// When playback is paused and the world has loaded (we don't want any dirt-screens) we sleep
while (paused() && hasWorldLoaded) {
// Unless we are going to terminate, restart or jump
if (terminate || startFromBeginning || desiredTimeStamp != -1) {
break;
}
Thread.sleep(10);
}
if (terminate) {
break REPLAY_LOOP;
}
if (startFromBeginning) {
// In case we need to restart from the beginning
// break out of the loop sending all packets which will
// cause the replay to be restarted by the outer loop
break;
}
// Read the next packet if we don't already have one
if (nextPacket == null) {
nextPacket = ReplayFileIO.readPacketData(dis);
}
int nextTimeStamp = nextPacket.getTimestamp();
// If we aren't jumping and the world has already been loaded (no dirt-screens) then wait
// the required amount to get proper packet timing
if (!isHurrying() && hasWorldLoaded) {
// How much time should have passed
int timeWait = (int) Math.round((nextTimeStamp - lastTimeStamp) / replaySpeed);
// How much time did pass
long timeDiff = System.currentTimeMillis() - lastPacketSent;
// How much time we need to wait to make up for the difference
long timeToSleep = Math.max(0, timeWait - timeDiff);
Thread.sleep(timeToSleep);
lastPacketSent = System.currentTimeMillis();
}
// Process packet
channelRead(ctx, nextPacket.getByteArray());
nextPacket = null;
lastTimeStamp = nextTimeStamp;
// In case we finished jumping
// We need to check that we aren't planing to restart so we don't accidentally run this
// code before we actually restarted
if (isHurrying() && lastTimeStamp > desiredTimeStamp && !startFromBeginning) {
desiredTimeStamp = -1;
replayHandler.moveCameraToTargetPosition();
// Pause after jumping
setReplaySpeed(0);
}
} catch (EOFException eof) {
// Reached end of file
// Pause the replay which will cause it to freeze before getting restarted
setReplaySpeed(0);
// Then wait until the user tells us to continue
while (paused() && hasWorldLoaded && desiredTimeStamp == -1 && !terminate) {
Thread.sleep(10);
}
break;
} catch (IOException e) {
e.printStackTrace();
}
}
// Restart the replay.
hasWorldLoaded = false;
lastTimeStamp = 0;
startFromBeginning = false;
nextPacket = null;
lastPacketSent = System.currentTimeMillis();
replayHandler.restartedReplay();
if (dis != null) {
dis.close();
dis = null;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
/**
* Return whether this replay sender is currently rushing. When rushing, all packets are sent without waiting until
* a specified timestamp is passed.
* @return {@code true} if currently rushing, {@code false} otherwise
*/
public boolean isHurrying() {
return desiredTimeStamp != -1;
}
/**
* Cancels the hurrying.
*/
public void stopHurrying() {
desiredTimeStamp = -1;
}
/**
* Return the timestamp to which this replay sender is currently rushing. All packets with an lower or equal
* timestamp will be sent out without any sleeping.
* @return The timestamp in milliseconds since the start of the replay
*/
public long getDesiredTimestamp() {
return desiredTimeStamp;
}
/**
* Jumps to the specified timestamp when in async mode by rushing all packets until one with a timestamp greater
* than the specified timestamp is found.
* If the timestamp has already passed, this causes the replay to restart and then rush all packets.
* @param millis Timestamp in milliseconds since the start of the replay
*/
public void jumpToTime(int millis) {
Preconditions.checkState(asyncMode, "Can only jump in async mode. Use sendPacketsTill(int) instead.");
if(millis < lastTimeStamp && !isHurrying()) {
startFromBeginning = true;
}
desiredTimeStamp = millis;
}
protected Packet processPacketAsync(Packet p) {
//If hurrying, ignore some packets, except for short durations
if(desiredTimeStamp - lastTimeStamp > 1000) {
if(p instanceof S2APacketParticles) return null;
if(p instanceof S0EPacketSpawnObject) {
S0EPacketSpawnObject pso = (S0EPacketSpawnObject)p;
int type = pso.func_148993_l();
if(type == 76) { // Firework rocket
return null;
}
}
}
return p;
}
/////////////////////////////////////////////////////////
// Synchronous packet processing //
/////////////////////////////////////////////////////////
/**
* Sends all packets until the specified timestamp is reached (inclusive).
* If the timestamp is smaller than the last packet sent, the replay is restarted from the beginning.
* @param timestamp The timestamp in milliseconds since the beginning of this replay
*/
public void sendPacketsTill(int timestamp) {
Preconditions.checkState(!asyncMode, "This method cannot be used in async mode. Use jumpToTime(int) instead.");
try {
while (ctx == null && !terminate) { // Make sure channel is ready
Thread.sleep(10);
}
synchronized (this) {
if (timestamp < lastTimeStamp) { // Restart the replay if we need to go backwards in time
hasWorldLoaded = false;
lastTimeStamp = 0;
if (dis != null) {
dis.close();
dis = null;
}
startFromBeginning = false;
nextPacket = null;
replayHandler.restartedReplay();
}
if (dis == null) {
dis = new DataInputStream(replayFile.getPacketData());
}
while (true) { // Send packets
try {
PacketData pd;
if (nextPacket != null) {
// If there is still a packet left from before, use it first
pd = nextPacket;
nextPacket = null;
} else {
// Otherwise read one from the input stream
pd = ReplayFileIO.readPacketData(dis);
}
int nextTimeStamp = pd.getTimestamp();
if (nextTimeStamp > timestamp) {
// We are done sending all packets
nextPacket = pd;
break;
}
// Process packet
channelRead(ctx, pd.getByteArray());
// Store last timestamp
lastTimeStamp = nextTimeStamp;
} catch (EOFException eof) {
// Shit! We hit the end before finishing our job! What shall we do now?
// well, let's just pretend we're done...
dis = null;
break;
} catch (IOException e) {
e.printStackTrace();
}
}
// This might be required if we change to async mode anytime soon
lastPacketSent = System.currentTimeMillis();
}
} catch (Exception e) {
e.printStackTrace();
}
}
protected Packet processPacketSync(Packet p) {
return p; // During synchronous playback everything is sent normally
}
/**
* This is necessary to convert packets from old replays to new replays.
* @param packet The packet to be transformed.
* @see #LEGACY_ENTITY_ID
*/
private void convertLegacyEntityIds(Packet packet) {
if (packet instanceof S0CPacketSpawnPlayer) {
S0CPacketSpawnPlayer p = (S0CPacketSpawnPlayer) packet;
if (p.field_148957_a == LEGACY_ENTITY_ID) {
p.field_148957_a = actualID;
}
} else if (packet instanceof S18PacketEntityTeleport) {
S18PacketEntityTeleport p = (S18PacketEntityTeleport) packet;
if (p.field_149458_a == LEGACY_ENTITY_ID) {
p.field_149458_a = actualID;
}
} else if (packet instanceof S14PacketEntity.S17PacketEntityLookMove) {
S14PacketEntity.S17PacketEntityLookMove p = (S14PacketEntity.S17PacketEntityLookMove) packet;
if (p.field_149074_a == LEGACY_ENTITY_ID) {
p.field_149074_a = actualID;
}
} else if (packet instanceof S19PacketEntityHeadLook) {
S19PacketEntityHeadLook p = (S19PacketEntityHeadLook) packet;
if (p.field_149384_a == LEGACY_ENTITY_ID) {
p.field_149384_a = actualID;
}
} else if (packet instanceof S12PacketEntityVelocity) {
S12PacketEntityVelocity p = (S12PacketEntityVelocity) packet;
if (p.field_149417_a == LEGACY_ENTITY_ID) {
p.field_149417_a = actualID;
}
} else if (packet instanceof S0BPacketAnimation) {
S0BPacketAnimation p = (S0BPacketAnimation) packet;
if (p.entityId == LEGACY_ENTITY_ID) {
p.entityId = actualID;
}
} else if (packet instanceof S04PacketEntityEquipment) {
S04PacketEntityEquipment p = (S04PacketEntityEquipment) packet;
if (p.field_149394_a == LEGACY_ENTITY_ID) {
p.field_149394_a = actualID;
}
} else if (packet instanceof S1BPacketEntityAttach) {
S1BPacketEntityAttach p = (S1BPacketEntityAttach) packet;
if (p.field_149408_a == LEGACY_ENTITY_ID) {
p.field_149408_a = actualID;
}
if (p.field_149406_b == LEGACY_ENTITY_ID) {
p.field_149406_b = actualID;
}
} else if (packet instanceof S0DPacketCollectItem) {
S0DPacketCollectItem p = (S0DPacketCollectItem) packet;
if (p.field_149356_b == LEGACY_ENTITY_ID) {
p.field_149356_b = actualID;
}
} else if (packet instanceof S13PacketDestroyEntities) {
S13PacketDestroyEntities p = (S13PacketDestroyEntities) packet;
if (p.field_149100_a.length == 1 && p.field_149100_a[0] == LEGACY_ENTITY_ID) {
p.field_149100_a[0] = actualID;
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.replaymod.replay;
import com.replaymod.core.SettingsRegistry;
public final class Setting<T> extends SettingsRegistry.SettingKeys<T> {
public static final Setting<Boolean> RECORD_SINGLEPLAYER = make("recordSingleplayer", "recordsingleplayer", true);
public static final Setting<Boolean> RECORD_SERVER = make("recordServer", "recordserver", true);
public static final Setting<Boolean> INDICATOR = make("indicator", "indicator", true);
private static <T> Setting<T> make(String key, String displayName, T defaultValue) {
return new Setting<>(key, displayName, defaultValue);
}
public Setting(String key, String displayString, T defaultValue) {
super("recording", key, "replaymod.gui.settings." + displayString, defaultValue);
}
}

View File

@@ -0,0 +1,24 @@
package com.replaymod.replay.events;
import com.replaymod.replay.ReplayHandler;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.minecraftforge.fml.common.eventhandler.Event;
@RequiredArgsConstructor
public abstract class ReplayCloseEvent extends Event {
@Getter
private final ReplayHandler replayHandler;
public static class Pre extends ReplayCloseEvent {
public Pre(ReplayHandler replayHandler) {
super(replayHandler);
}
}
public static class Post extends ReplayCloseEvent {
public Post(ReplayHandler replayHandler) {
super(replayHandler);
}
}
}

View File

@@ -0,0 +1,24 @@
package com.replaymod.replay.events;
import com.replaymod.replay.ReplayHandler;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.minecraftforge.fml.common.eventhandler.Event;
@RequiredArgsConstructor
public abstract class ReplayOpenEvent extends Event {
@Getter
private final ReplayHandler replayHandler;
public static class Pre extends ReplayOpenEvent {
public Pre(ReplayHandler replayHandler) {
super(replayHandler);
}
}
public static class Post extends ReplayOpenEvent {
public Post(ReplayHandler replayHandler) {
super(replayHandler);
}
}
}

View File

@@ -0,0 +1,182 @@
package com.replaymod.replay.gui.overlay;
import de.johni0702.replaystudio.data.Marker;
import com.replaymod.core.ReplayMod;
import eu.crushedpixel.replaymod.gui.elements.timelines.GuiTimeline;
import eu.crushedpixel.replaymod.holders.AdvancedPosition;
import com.replaymod.replay.ReplayHandler;
import eu.crushedpixel.replaymod.utils.MouseUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.I18n;
import org.lwjgl.util.Point;
import java.awt.*;
public class GuiMarkerTimeline extends GuiTimeline {
private static final int KEYFRAME_MARKER_X = 109;
private static final int KEYFRAME_MARKER_Y = 20;
private final ReplayHandler replayHandler;
private Marker selectedMarker;
private Marker clickedMarker;
private long clickTime;
private boolean dragging;
public GuiMarkerTimeline(ReplayHandler replayHandler, int positionX, int positionY, int width, int height) {
super(positionX, positionY, width, height);
this.replayHandler = replayHandler;
this.showMarkers = false;
}
@Override
public boolean mouseClick(Minecraft mc, int mouseX, int mouseY, int button) {
if(!enabled) return false;
long time = getTimeAt(mouseX, mouseY);
if(time == -1) {
return false;
}
int tolerance = (int) (2 * Math.round(zoom * timelineLength / width));
Marker closest = null;
if(mouseY >= positionY + BORDER_TOP + 10) {
long distance = tolerance;
for (Marker m : replayHandler.getMarkers()) {
long d = Math.abs(m.getTime() - time);
if (d <= distance) {
closest = m;
distance = d;
}
}
}
//left mouse button
if(button == 0) {
if(closest == null) { //if no keyframe clicked, jump in time
replayHandler.doJump((int) getTimeAt(mouseX, mouseY), true);
} else {
selectedMarker = closest;
// If we clicked on a key frame, then continue monitoring the mouse for movements
long currentTime = System.currentTimeMillis();
if(currentTime - clickTime < 500) { // if double clicked then open GUI instead
// mc.displayGuiScreen(GuiEditKeyframe.create(closest));
// TODO Edit Gui
this.clickedMarker = null;
} else {
this.clickedMarker = closest;
this.dragging = false;
}
this.clickTime = currentTime;
}
} else if(button == 1) {
if(closest != null) {
//Jump to clicked Marker Keyframe (explicitly force to jump to this position)
replayHandler.setTargetPosition(new AdvancedPosition(
closest.getX(), closest.getY(), closest.getZ(),
closest.getPitch(), closest.getYaw(), closest.getRoll()
));
//perform the jump, telling the Overlay not to override the last position value
replayHandler.doJump(closest.getTime(), false);
}
}
return isHovering(mouseX, mouseY);
}
@Override
public void mouseDrag(Minecraft mc, int mouseX, int mouseY, int button) {
if(!enabled) return;
long time = getTimeAt(mouseX, mouseY);
if (time != -1) {
if (clickedMarker != null) {
int tolerance = (int) (2 * Math.round(zoom * timelineLength / width));
if (dragging || Math.abs(clickedMarker.getTime() - time) > tolerance) {
clickedMarker.setTime((int) time);
dragging = true;
}
}
}
}
@Override
public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) {
mouseDrag(mc, mouseX, mouseY, button);
clickedMarker = null;
if (dragging) {
replayHandler.saveMarkers();
dragging = false;
}
}
@Override
public void draw(Minecraft mc, int mouseX, int mouseY) {
super.draw(mc, mouseX, mouseY);
int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT;
long leftTime = Math.round(timeStart * timelineLength);
long rightTime = Math.round((timeStart + zoom) * timelineLength);
double segmentLength = timelineLength * zoom;
drawTimelineCursor(leftTime, rightTime, bodyWidth);
//Draw Keyframe logos
for (Marker marker : replayHandler.getMarkers()) {
drawMarker(marker, bodyWidth, leftTime, rightTime, segmentLength);
}
}
private int getMarkerX(int timestamp, long leftTime, int bodyWidth, double segmentLength) {
long positionInSegment = timestamp - leftTime;
double fractionOfSegment = positionInSegment / segmentLength;
return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth);
}
@Override
public void drawOverlay(Minecraft mc, int mouseX, int mouseY) {
boolean drawn = false;
int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT;
long leftTime = Math.round(timeStart * timelineLength);
double segmentLength = timelineLength * zoom;
for (Marker marker : replayHandler.getMarkers()) {
int markerX = getMarkerX(marker.getTime(), leftTime, bodyWidth, segmentLength);
if(MouseUtils.isMouseWithinBounds(markerX - 2, this.positionY + BORDER_TOP + 10 + 1, 5, 5)) {
Point mouse = MouseUtils.getMousePos();
String markerName = marker.getName();
if(markerName == null || markerName.isEmpty()) markerName = I18n.format("replaymod.gui.ingame.unnamedmarker");
ReplayMod.tooltipRenderer.drawTooltip(mouse.getX(), mouse.getY(), markerName, null, Color.WHITE);
drawn = true;
}
}
if(!drawn) {
super.drawOverlay(mc, mouseX, mouseY);
}
}
private void drawMarker(Marker marker, int bodyWidth, long leftTime, long rightTime, double segmentLength) {
if (marker.getTime() <= rightTime && marker.getTime() >= leftTime) {
int textureX = KEYFRAME_MARKER_X;
int textureY = KEYFRAME_MARKER_Y;
int y = positionY+10;
int keyframeX = getMarkerX(marker.getTime(), leftTime, bodyWidth, segmentLength);
if (selectedMarker == marker) {
textureX += 5;
}
rect(keyframeX - 2, y + BORDER_TOP, textureX, textureY, 5, 5);
}
}
}

View File

@@ -0,0 +1,140 @@
package com.replaymod.replay.gui.overlay;
import com.replaymod.core.ReplayMod;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplaySender;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiOverlay;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.element.GuiSlider;
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
import de.johni0702.minecraft.gui.element.advanced.GuiTimeline;
import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import net.minecraft.client.resources.I18n;
import net.minecraft.client.settings.GameSettings;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.InputEvent;
import org.lwjgl.util.ReadableDimension;
import org.lwjgl.util.ReadablePoint;
import org.lwjgl.util.WritablePoint;
import static com.replaymod.core.ReplayMod.TEXTURE_SIZE;
public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
private final ReplayHandler replayHandler;
public final GuiPanel topPanel = new GuiPanel(this)
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.LEFT).setSpacing(5));
public final GuiTexturedButton playPauseButton = new GuiTexturedButton().setSize(20, 20)
.setTexture(ReplayMod.TEXTURE, TEXTURE_SIZE);
public final GuiSlider speedSlider = new GuiSlider().setSize(100, 20).setSteps(37); // 0.0 is not included
public final GuiTimeline timeline = new GuiTimeline(){
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
setCursorPosition(replayHandler.getReplaySender().currentTimeStamp());
super.draw(renderer, size, renderInfo);
}
}.setSize(Integer.MAX_VALUE, 20);
public GuiReplayOverlay(final ReplayHandler replayHandler) {
this.replayHandler = replayHandler;
topPanel.addElements(null, playPauseButton, speedSlider, timeline);
setLayout(new CustomLayout<GuiReplayOverlay>() {
@Override
protected void layout(GuiReplayOverlay container, int width, int height) {
pos(topPanel, 10, 10);
size(topPanel, width - 20, 20);
}
});
playPauseButton.setTexturePosH(new ReadablePoint() {
@Override
public int getX() {
return 0;
}
@Override
public int getY() {
return replayHandler.getReplaySender().paused() ? 0 : 20;
}
@Override
public void getLocation(WritablePoint dest) {
dest.setLocation(getX(), getY());
}
}).onClick(new Runnable() {
@Override
public void run() {
ReplaySender replaySender = replayHandler.getReplaySender();
// If currently paused
if (replaySender.paused()) {
// then play
replaySender.setReplaySpeed(getSpeed());
} else {
// else pause
replaySender.setReplaySpeed(0);
}
}
});
speedSlider.onValueChanged(new Runnable() {
@Override
public void run() {
double speed = getSpeed();
speedSlider.setText(I18n.format("replaymod.gui.speed") + ": " + speed + "x");
ReplaySender replaySender = replayHandler.getReplaySender();
if (!replaySender.paused()) {
replaySender.setReplaySpeed(speed);
}
}
}).setValue(9);
timeline.onClick(new IGuiTimeline.OnClick() {
@Override
public void run(int time) {
replayHandler.doJump(time, true);
}
}).setLength(replayHandler.getReplaySender().replayLength());
}
private double getSpeed() {
int value = speedSlider.getValue() + 1;
if (value <= 9) {
return value / 10d;
} else {
return 1 + (0.25d * (value - 10));
}
}
@Override
public void setVisible(boolean visible) {
if (isVisible() != visible) {
if (visible) {
FMLCommonHandler.instance().bus().register(this);
} else {
FMLCommonHandler.instance().bus().unregister(this);
}
}
super.setVisible(visible);
}
@SubscribeEvent
public void onKeyInput(InputEvent.KeyInputEvent event) {
GameSettings gameSettings = getMinecraft().gameSettings;
while (gameSettings.keyBindChat.isPressed() || gameSettings.keyBindCommand.isPressed()) {
if (!isMouseVisible()) {
setMouseVisible(true);
}
}
}
@Override
protected GuiReplayOverlay getThis() {
return this;
}
}

View File

@@ -0,0 +1,100 @@
package com.replaymod.replay.gui.screen;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiErrorScreen;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.GuiTextField;
import net.minecraft.client.resources.I18n;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.lwjgl.input.Keyboard;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class GuiRenameReplay extends GuiScreen {
private GuiScreen parent;
private GuiTextField replayNameInput;
private File file;
public GuiRenameReplay(GuiScreen parent, File file) {
this.parent = parent;
this.file = file;
}
@Override
public void updateScreen() {
this.replayNameInput.updateCursorCounter();
}
@Override
public void initGui() {
Keyboard.enableRepeatEvents(true);
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = this.buttonList;
buttonList.clear();
buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 4 + 96 + 12, I18n.format("replaymod.gui.rename")));
buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 4 + 120 + 12, I18n.format("replaymod.gui.cancel")));
String s = FilenameUtils.getBaseName(file.getAbsolutePath());
this.replayNameInput = new GuiTextField(2, this.fontRendererObj, this.width / 2 - 100, 60, 200, 20);
this.replayNameInput.setFocused(true);
this.replayNameInput.setText(s);
}
@Override
public void onGuiClosed() {
Keyboard.enableRepeatEvents(false);
}
@Override
protected void actionPerformed(GuiButton button) throws IOException {
if(button.enabled) {
if(button.id == 1) {
this.mc.displayGuiScreen(this.parent);
} else if(button.id == 0) {
File folder = ReplayFileIO.getReplayFolder();
File initRenamed = new File(folder, (this.replayNameInput.getText().trim() + ".zip").replaceAll("[^a-zA-Z0-9\\.\\- ]", "_"));
File renamed = ReplayFileIO.getNextFreeFile(initRenamed);
try {
FileUtils.moveFile(file, renamed);
} catch (IOException e) {
e.printStackTrace();
mc.displayGuiScreen(new GuiErrorScreen(
I18n.format("replaymod.gui.viewer.delete.failed1"),
I18n.format("replaymod.gui.viewer.delete.failed2")
));
return;
}
this.mc.displayGuiScreen(this.parent);
}
}
}
@Override
protected void keyTyped(char typedChar, int keyCode) throws IOException {
this.replayNameInput.textboxKeyTyped(typedChar, keyCode);
((GuiButton) this.buttonList.get(0)).enabled = this.replayNameInput.getText().trim().length() > 0;
if(keyCode == 28 || keyCode == 156) {
this.actionPerformed((GuiButton) this.buttonList.get(0));
}
}
@Override
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException {
super.mouseClicked(mouseX, mouseY, mouseButton);
this.replayNameInput.mouseClicked(mouseX, mouseY, mouseButton);
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
this.drawDefaultBackground();
this.drawCenteredString(this.fontRendererObj, I18n.format("replaymod.gui.viewer.rename.title"), this.width / 2, 20, 16777215);
this.drawString(this.fontRendererObj, I18n.format("replaymod.gui.viewer.rename.name"), this.width / 2 - 100, 47, 10526880);
this.replayNameInput.drawTextBox();
super.drawScreen(mouseX, mouseY, partialTicks);
}
}

View File

@@ -0,0 +1,360 @@
package com.replaymod.replay.gui.screen;
import com.google.common.base.Optional;
import com.mojang.realmsclient.util.Pair;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.gui.GuiReplaySettings;
import com.replaymod.replay.ReplayModReplay;
import de.johni0702.replaystudio.replay.ReplayFile;
import de.johni0702.replaystudio.replay.ReplayMetaData;
import de.johni0702.replaystudio.replay.ZipReplayFile;
import de.johni0702.replaystudio.studio.ReplayStudio;
import eu.crushedpixel.replaymod.api.replay.holders.FileInfo;
import eu.crushedpixel.replaymod.gui.elements.GuiLoadingListEntry;
import eu.crushedpixel.replaymod.gui.elements.GuiReplayListEntry;
import eu.crushedpixel.replaymod.gui.elements.GuiReplayListExtended;
import eu.crushedpixel.replaymod.gui.online.GuiUploadFile;
import eu.crushedpixel.replaymod.registry.ResourceHelper;
import eu.crushedpixel.replaymod.utils.ImageUtils;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
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.resources.I18n;
import net.minecraft.util.Util;
import net.minecraftforge.fml.common.FMLLog;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback {
private static final int LOAD_BUTTON_ID = 9001;
private static final int UPLOAD_BUTTON_ID = 9002;
private static final int FOLDER_BUTTON_ID = 9003;
private static final int RENAME_BUTTON_ID = 9004;
private static final int DELETE_BUTTON_ID = 9005;
private static final int SETTINGS_BUTTON_ID = 9006;
private static final int CANCEL_BUTTON_ID = 9007;
private final ReplayModReplay mod;
private boolean initialized;
private GuiReplayListExtended replayGuiList;
private List<Pair<Pair<File, ReplayMetaData>, File>> replayFileList = new ArrayList<Pair<Pair<File, ReplayMetaData>, File>>();
private GuiButton loadButton;
private GuiButton uploadButton;
private GuiButton renameButton;
private GuiButton deleteButton;
private boolean delete_file = false;
private Queue<Runnable> loadedReplaysQueue = new ConcurrentLinkedQueue<Runnable>();
private Thread fileReloader;
public GuiReplayViewer(ReplayModReplay mod) {
this.mod = mod;
}
private class FileReloaderThread extends Thread {
private final GuiLoadingListEntry loadingListEntry = new GuiLoadingListEntry();
@Override
public synchronized void start() {
replayFileList = new ArrayList<Pair<Pair<File, ReplayMetaData>, File>>();
replayGuiList.clearEntries();
replayGuiList.addEntry(loadingListEntry);
super.start();
}
@Override
public void run() {
for(final File file : ReplayFileIO.getAllReplayFiles()) {
if(interrupted()) break;
try {
ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file);
final ReplayMetaData metaData = replayFile.getMetaData();
Optional<BufferedImage> thumb = replayFile.getThumb();
replayFile.close();
File tmp = null;
if(thumb.isPresent()) {
BufferedImage img = ImageUtils.scaleImage(thumb.get(), new Dimension(1280, 720));
tmp = File.createTempFile(FilenameUtils.getBaseName(file.getAbsolutePath())+"_THUMBNAIL", "jpg");
tmp.deleteOnExit();
ImageIO.write(img, "jpg", tmp);
}
final File thumbFile = tmp;
loadedReplaysQueue.offer(new Runnable() {
@Override
public void run() {
addEntry(file, metaData, thumbFile);
}
});
} catch(Exception e) {
FMLLog.getLogger().error("Could not load Replay File "+file.getName(), e);
}
}
loadedReplaysQueue.offer(new Runnable() {
@Override
public void run() {
replayGuiList.removeEntry(loadingListEntry);
}
});
}
}
public static GuiYesNo getYesNoGui(GuiYesNoCallback p_152129_0_, String file, int p_152129_2_) {
String s1 = I18n.format("replaymod.gui.viewer.delete.linea");
String s2 = "\'" + file + "\' " + I18n.format("replaymod.gui.viewer.delete.lineb");
String s3 = I18n.format("replaymod.gui.delete");
String s4 = I18n.format("replaymod.gui.cancel");
return new GuiYesNo(p_152129_0_, s1, s2, s3, s4, p_152129_2_);
}
@Override
public void onGuiClosed() {
ResourceHelper.freeAllResources();
super.onGuiClosed();
}
@Override
@SuppressWarnings("deprecation")
public void initGui() {
Keyboard.enableRepeatEvents(true);
if(!this.initialized) {
replayGuiList = new ReplayList(this, this.mc, this.width, this.height, 32, this.height - 64, 36);
this.initialized = true;
} else {
this.replayGuiList.setDimensions(this.width, this.height, 32, this.height - 64);
}
try {
if(fileReloader != null) {
fileReloader.interrupt();
fileReloader.join();
}
fileReloader = new FileReloaderThread();
fileReloader.start();
} catch(Exception e) {
e.printStackTrace();
}
this.createButtons();
}
private void createButtons() {
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = this.buttonList;
buttonList.add(loadButton = new GuiButton(LOAD_BUTTON_ID, this.width / 2 - 154, this.height - 52, 73, 20, I18n.format("replaymod.gui.load")));
buttonList.add(uploadButton = new GuiButton(UPLOAD_BUTTON_ID, this.width / 2 - 154 + 78, this.height - 52, 73, 20, I18n.format("replaymod.gui.upload")));
buttonList.add(new GuiButton(FOLDER_BUTTON_ID, this.width / 2 + 4, this.height - 52, 150, 20, I18n.format("replaymod.gui.viewer.replayfolder")));
buttonList.add(renameButton = new GuiButton(RENAME_BUTTON_ID, this.width / 2 - 154, this.height - 28, 72, 20, I18n.format("replaymod.gui.rename")));
buttonList.add(deleteButton = new GuiButton(DELETE_BUTTON_ID, this.width / 2 - 76, this.height - 28, 72, 20, I18n.format("replaymod.gui.delete")));
buttonList.add(new GuiButton(SETTINGS_BUTTON_ID, this.width / 2 + 4, this.height - 28, 72, 20, I18n.format("replaymod.gui.settings")));
buttonList.add(new GuiButton(CANCEL_BUTTON_ID, this.width / 2 + 4 + 78, this.height - 28, 72, 20, I18n.format("replaymod.gui.cancel")));
setButtonsEnabled(false);
}
@Override
public void handleMouseInput() throws IOException {
super.handleMouseInput();
this.replayGuiList.handleMouseInput();
}
@Override
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException {
super.mouseClicked(mouseX, mouseY, mouseButton);
this.replayGuiList.mouseClicked(mouseX, mouseY, mouseButton);
}
@Override
protected void mouseReleased(int mouseX, int mouseY, int state) {
super.mouseReleased(mouseX, mouseY, state);
this.replayGuiList.mouseReleased(mouseX, mouseY, state);
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
this.drawDefaultBackground();
this.replayGuiList.drawScreen(mouseX, mouseY, partialTicks);
this.drawCenteredString(this.fontRendererObj, I18n.format("replaymod.gui.replayviewer"), this.width / 2, 20, 16777215);
super.drawScreen(mouseX, mouseY, partialTicks);
if(uploadButton.isMouseOver() && !uploadButton.enabled && loadButton.enabled) {
if(currentFileUploaded) {
ReplayMod.tooltipRenderer.drawTooltip(mouseX, mouseY, I18n.format("replaymod.gui.viewer.alreadyuploaded"), this, Color.RED);
}
}
}
@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(null);
} else if(button.id == DELETE_BUTTON_ID) {
String s = ((GuiReplayListEntry)replayGuiList.getListEntry(replayGuiList.selected)).getFileInfo().getName();
if(s != null) {
delete_file = true;
GuiYesNo guiyesno = getYesNoGui(this, s, 1);
this.mc.displayGuiScreen(guiyesno);
}
} else if(button.id == SETTINGS_BUTTON_ID) {
new GuiReplaySettings(this, ReplayMod.instance.getSettingsRegistry()).display();
} else if(button.id == RENAME_BUTTON_ID) {
File file = replayFileList.get(replayGuiList.selected).first().first();
this.mc.displayGuiScreen(new GuiRenameReplay(this, file));
} else if(button.id == UPLOAD_BUTTON_ID) {
File file = replayFileList.get(replayGuiList.selected).first().first();
this.mc.displayGuiScreen(new GuiUploadFile(file, this));
} else if(button.id == FOLDER_BUTTON_ID) {
File file1 = ReplayFileIO.getReplayFolder();
String s = file1.getAbsolutePath();
if(Util.getOSType() == Util.EnumOS.OSX) {
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", s});
return;
} catch (IOException e) {
LogManager.getLogger().error("Cannot open file", e);
}
} else if(Util.getOSType() == Util.EnumOS.WINDOWS) {
String s1 = String.format("cmd.exe /C start \"Open file\" \"%s\"", s);
try {
Runtime.getRuntime().exec(s1);
return;
} catch(IOException e) {
LogManager.getLogger().error("Cannot open file", e);
}
}
boolean flag = false;
try {
Desktop.getDesktop().browse(file1.toURI());
} catch(Throwable throwable) {
flag = true;
}
if(flag) {
Sys.openURL("file://" + s);
}
}
}
}
@Override
public void confirmClicked(boolean result, int id) {
if(this.delete_file) {
this.delete_file = false;
if(result) {
try {
FileUtils.forceDelete(replayFileList.get(replayGuiList.selected).first().first());
} catch (IOException e) {
e.printStackTrace();
}
replayFileList.remove(replayGuiList.selected);
replayGuiList.selected = -1;
}
this.mc.displayGuiScreen(this);
}
}
@Override
public void updateScreen() {
super.updateScreen();
while (!loadedReplaysQueue.isEmpty()) {
loadedReplaysQueue.poll().run();
}
}
private boolean currentFileUploaded = false;
public void setButtonsEnabled(boolean b) {
loadButton.enabled = b;
if(b) {
currentFileUploaded = ReplayMod.uploadedFileHandler.isUploaded(replayFileList.get(replayGuiList.selected).first().first());
uploadButton.enabled = !currentFileUploaded;
} else {
uploadButton.enabled = false;
}
renameButton.enabled = b;
deleteButton.enabled = b;
}
public void loadReplay(int id) {
mc.displayGuiScreen(null);
try {
mod.startReplay(replayFileList.get(id).first().first());
} catch(Exception e) {
e.printStackTrace();
}
}
private void addEntry(File file, ReplayMetaData metaData, File thumb) {
final Pair<Pair<File, ReplayMetaData>, File> p = Pair.of(Pair.of(file, metaData), thumb);
final int index = getInsertionIndex(p, replayFileList);
replayFileList.add(index, p);
final FileInfo fileInfo = new FileInfo(-1, p.first().second(), null, null,
-1, -1, -1, FilenameUtils.getBaseName(p.first().first().getName()), true, -1);
replayGuiList.addEntry(index, new GuiReplayListEntry(replayGuiList, fileInfo, p.second()));
}
private static FileAgeComparator fileAgeComparator = new FileAgeComparator();
public static class FileAgeComparator implements Comparator<Pair<Pair<File, ReplayMetaData>, File>> {
@Override
public int compare(Pair<Pair<File, ReplayMetaData>, File> o1, Pair<Pair<File, ReplayMetaData>, File> o2) {
try {
return new Date(o2.first().second().getDate()).compareTo(new Date(o1.first().second().getDate()));
} catch(Exception e) {
return 0;
}
}
}
private int getInsertionIndex(Pair<Pair<File, ReplayMetaData>, File> p, List<Pair<Pair<File, ReplayMetaData>, File>> list) {
List<Pair<Pair<File, ReplayMetaData>, File>> nl = new ArrayList<Pair<Pair<File, ReplayMetaData>, File>>(list);
nl.add(p);
Collections.sort(nl, fileAgeComparator);
return nl.indexOf(p);
}
}

View File

@@ -0,0 +1,29 @@
package com.replaymod.replay.gui.screen;
import eu.crushedpixel.replaymod.gui.elements.GuiReplayListExtended;
import net.minecraft.client.Minecraft;
public class ReplayList extends GuiReplayListExtended {
private GuiReplayViewer parent;
public ReplayList(GuiReplayViewer parent, Minecraft mcIn,
int p_i45010_2_, int p_i45010_3_, int p_i45010_4_, int p_i45010_5_,
int p_i45010_6_) {
super(mcIn, p_i45010_2_, p_i45010_3_, p_i45010_4_, p_i45010_5_,
p_i45010_6_);
this.parent = parent;
}
@Override
protected void elementClicked(int slotIndex, boolean isDoubleClick,
int mouseX, int mouseY) {
super.elementClicked(slotIndex, isDoubleClick, mouseX, mouseY);
parent.setButtonsEnabled(true);
if(isDoubleClick) {
parent.loadReplay(slotIndex);
}
}
}

View File

@@ -0,0 +1,111 @@
package com.replaymod.replay.handler;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.gui.screen.GuiReplayViewer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiIngameMenu;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.resources.I18n;
import net.minecraftforge.client.event.GuiScreenEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class GuiHandler {
private static final int BUTTON_EXIT_SERVER = 1;
private static final int BUTTON_RETURN_TO_GAME = 4;
private static final int BUTTON_ACHIEVEMENTS = 5;
private static final int BUTTON_STATS = 6;
private static final int BUTTON_OPEN_TO_LAN = 7;
private static final int BUTTON_REPLAY_VIEWER = 17890234;
private static final int BUTTON_EXIT_REPLAY = 17890235;
private static final Minecraft mc = Minecraft.getMinecraft();
private final ReplayModReplay mod;
public GuiHandler(ReplayModReplay mod) {
this.mod = mod;
}
public void register() {
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void injectIntoIngameMenu(GuiScreenEvent.InitGuiEvent.Post event) {
if (!(event.gui instanceof GuiIngameMenu)) {
return;
}
if (mod.getReplayHandler() != null) {
// Pause replay when menu is opened
mod.getReplayHandler().getReplaySender().setReplaySpeed(0);
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = event.buttonList;
for(GuiButton b : new ArrayList<>(buttonList)) {
switch (b.id) {
// Replace "Exit Server" button with "Exit Replay" button
case BUTTON_EXIT_SERVER:
b.displayString = I18n.format("replaymod.gui.exit");
b.id = BUTTON_EXIT_REPLAY;
break;
// Remove "Achievements", "Stats" and "Open to LAN" buttons
case BUTTON_ACHIEVEMENTS:
case BUTTON_STATS:
case BUTTON_OPEN_TO_LAN:
buttonList.remove(b);
}
// Move all buttons except the "Return to game" button upwards
if (b.id != BUTTON_RETURN_TO_GAME) {
b.yPosition -= 48;
}
}
}
}
@SubscribeEvent
public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) {
if (!(event.gui instanceof GuiMainMenu)) {
return;
}
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = event.buttonList;
GuiButton button = new GuiButton(BUTTON_REPLAY_VIEWER, event.gui.width / 2 - 100,
event.gui.height / 4 + 10 + 3 * 24, I18n.format("replaymod.gui.replayviewer"));
button.width = button.width / 2 - 2;
buttonList.add(button);
}
@SubscribeEvent
public void onButton(GuiScreenEvent.ActionPerformedEvent event) {
if(!event.button.enabled) return;
if (event.gui instanceof GuiMainMenu) {
if (event.button.id == BUTTON_REPLAY_VIEWER) {
mc.displayGuiScreen(new GuiReplayViewer(mod));
}
}
if (event.gui instanceof GuiIngameMenu && mod.getReplayHandler() != null) {
if (event.button.id == BUTTON_EXIT_REPLAY) {
event.button.enabled = false;
try {
mod.getReplayHandler().endReplay();
} catch (IOException e) {
e.printStackTrace();
}
mc.displayGuiScreen(new GuiMainMenu());
}
}
}
}