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,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();
}
}
}
}