Save resource packs to replay file and use those during replay

This commit is contained in:
johni0702
2015-05-29 12:10:06 +02:00
parent 95e2c932f0
commit 37d31408bd
7 changed files with 323 additions and 115 deletions

View File

@@ -1,5 +1,7 @@
package eu.crushedpixel.replaymod.recording;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.google.gson.Gson;
import eu.crushedpixel.replaymod.gui.GuiReplaySaving;
import eu.crushedpixel.replaymod.holders.PacketData;
@@ -8,16 +10,20 @@ import eu.crushedpixel.replaymod.utils.ReplayFileIO;
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.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.*;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public abstract class DataListener extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger();
protected File file;
protected Long startTime = null;
protected String name;
@@ -29,6 +35,10 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter {
private boolean singleplayer;
private Gson gson = new Gson();
private final File tempResourcePacksFolder = Files.createTempDir();
private final Map<Integer, String> requestToHash = new ConcurrentHashMap<Integer, String>();
private final Map<String, File> resourcePacks = new HashMap<String, File>();
public DataListener(File file, String name, String worldName, long startTime, boolean singleplayer) throws FileNotFoundException {
this.file = file;
this.startTime = startTime;
@@ -54,6 +64,22 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter {
dataWriter.requestFinish(players);
}
protected void recordResourcePack(File file, int requestId) {
try {
String hash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString();
requestToHash.put(requestId, hash);
synchronized (resourcePacks) {
if (!resourcePacks.containsKey(hash)) {
File tempFile = new File(tempResourcePacksFolder, hash);
FileUtils.copyFile(file, tempFile);
resourcePacks.put(hash, tempFile);
}
}
} catch (IOException e) {
logger.warn("Failed to save resource pack.", e);
}
}
public class DataWriter {
private boolean active = true;
@@ -138,9 +164,10 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter {
File archive = new File(folder, name + ReplayFile.ZIP_FILE_EXTENSION);
archive.createNewFile();
ReplayFileIO.writeReplayFile(archive, file, metaData);
ReplayFileIO.writeReplayFile(archive, file, metaData, resourcePacks, requestToHash);
file.delete();
FileUtils.deleteDirectory(tempResourcePacksFolder);
GuiReplaySaving.replaySaving = false;
} catch(Exception e) {

View File

@@ -1,27 +1,50 @@
package eu.crushedpixel.replaymod.recording;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import eu.crushedpixel.replaymod.holders.PacketData;
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.S0CPacketSpawnPlayer;
import net.minecraft.network.play.server.S0DPacketCollectItem;
import net.minecraft.network.play.server.S0FPacketSpawnMob;
import net.minecraft.network.play.server.S48PacketResourcePackSend;
import net.minecraft.util.HttpUtil;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.FileNotFoundException;
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(File file, String name, String worldName, long startTime, boolean singleplayer) throws FileNotFoundException {
super(file, name, worldName, startTime, singleplayer);
@@ -34,7 +57,7 @@ public class PacketListener extends DataListener {
players.add(uuid.toString());
}
PacketData pd = getPacketData(context, packet);
PacketData pd = getPacketData(packet);
writeData(pd);
} catch(Exception e) {
e.printStackTrace();
@@ -72,7 +95,14 @@ public class PacketListener extends DataListener {
players.add(uuid.toString());
}
PacketData pd = getPacketData(ctx, packet);
if (packet instanceof S48PacketResourcePackSend) {
S48PacketResourcePackSend p = (S48PacketResourcePackSend) packet;
int requestId = handleResourcePack(p);
saveOnly(new S48PacketResourcePackSend("replay://" + requestId, ""));
return;
}
PacketData pd = getPacketData(packet);
writeData(pd);
} catch(Exception e) {
e.printStackTrace();
@@ -89,7 +119,7 @@ public class PacketListener extends DataListener {
}
@SuppressWarnings("unchecked")
private PacketData getPacketData(ChannelHandlerContext ctx, Packet packet) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
private PacketData getPacketData(Packet packet) throws IOException {
if(startTime == null) startTime = System.currentTimeMillis();
@@ -120,10 +150,148 @@ public class PacketListener extends DataListener {
}
}
byte[] array = ReplayFileIO.serializePacket(packet);
return new PacketData(array, timestamp);
}
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() {
public void onSuccess(Object result) {
recordResourcePack(levelDir, requestId);
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED));
}
public void onFailure(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() {
public void run() {
mc.displayGuiScreen(new GuiYesNo(new GuiYesNoCallback() {
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() {
public void onSuccess(Object result) {
mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED));
}
public void onFailure(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() {
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() {
public void onSuccess(Object value) {
recordResourcePack(file, requestId);
repo.func_177319_a(file);
}
public void onFailure(Throwable throwable) {
throwable.printStackTrace();
}
});
return repo.field_177322_i;
} finally {
repo.field_177321_h.unlock();
}
}
}

View File

@@ -1,6 +1,7 @@
package eu.crushedpixel.replaymod.replay;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import com.google.common.util.concurrent.ListenableFutureTask;
import eu.crushedpixel.replaymod.ReplayMod;
import eu.crushedpixel.replaymod.entities.CameraEntity;
@@ -15,7 +16,6 @@ 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.resources.ResourcePackRepository;
import net.minecraft.network.EnumConnectionState;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.Packet;
@@ -24,14 +24,12 @@ import net.minecraft.world.EnumDifficulty;
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.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
@@ -137,6 +135,11 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
*/
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
@@ -249,7 +252,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
* @param p The packet to process
* @return The processed packet or {@code null} if no packet shall be sent
*/
protected Packet processPacket(Packet p) {
protected Packet processPacket(Packet p) throws Exception {
if(BAD_PACKETS.contains(p.getClass())) return null;
if(p instanceof S29PacketSoundEffect && ReplayProcess.isVideoRecording()) {
@@ -262,7 +265,21 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
if(p instanceof S48PacketResourcePackSend) {
S48PacketResourcePackSend packet = (S48PacketResourcePackSend) p;
new ResourcePackCheck(packet.func_179783_a(), packet.func_179784_b()).start();
String url = packet.func_179783_a();
if (url.startsWith("replay://")) {
int id = Integer.parseInt(url.substring("replay://".length()));
Map<Integer, String> index = replayFile.resourcePackIndex().get();
if (index != null) {
String hash = index.get(id);
if (hash != null) {
File file = new File(tempResourcePackFolder, hash + ".zip");
if (!file.exists()) {
IOUtils.copy(replayFile.resourcePack(hash).get(), new FileOutputStream(file));
}
mc.getResourcePackRepository().func_177319_a(file);
}
}
}
return null;
}
@@ -355,6 +372,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
replayFile.close();
FileUtils.deleteDirectory(tempResourcePackFolder);
super.channelInactive(ctx);
}
@@ -386,102 +404,6 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
MCTimerHandler.setTimerSpeed((float) d);
}
/**
* Checks for stored resource packs and loads them or downloads a new one.
*/
private static class ResourcePackCheck extends Thread {
private static Minecraft mc = Minecraft.getMinecraft();
private static ResourcePackRepository repo = mc.getResourcePackRepository();
private String url, hash;
public ResourcePackCheck(String url, String hash) {
this.url = url;
this.hash = hash;
}
/**
* Return the location of the stored resource pack.
* @param url The original url of the resource pack
* @param hash The hash code of the resource pack
* @return File location of the resource pack
*/
private File getServerResourcePackLocation(String url, String hash) {
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 null;
}
filename = "legacy_" + filename.replaceAll("\\W", "");
}
return new File(repo.dirServerResourcepacks, filename);
}
/**
* Download a resource pack from a specified URL.
* @param url The URL to download from
* @param file The target file location
* @return {@code true} if the download was successful, {@code false} if an I/O-error occured
*/
private boolean downloadServerResourcePack(String url, File file) {
try {
FileUtils.copyURLToFile(new URL(url), file);
return true;
} catch(Exception e) {
e.printStackTrace();
}
return false;
}
/**
* Add the resource pack to the loaded resource packs if resource packs are enabled in the config.
* If there is no local copy of the resource pack, this loads the resource pack from the specified url.
*/
@Override
public void run() {
try {
boolean use = ReplayMod.replaySettings.getUseResourcePacks();
if(!use) return;
System.out.println("Looking for downloaded Resource Pack...");
File rp = getServerResourcePackLocation(url, hash);
if(rp == null) {
System.out.println("Invalid Resource Pack provided");
return;
}
if(rp.exists()) {
System.out.println("Resource Pack found!");
repo.func_177319_a(rp);
} else {
System.out.println("No Resource Pack found.");
System.out.println("Attempting to download Resource Pack...");
boolean success = downloadServerResourcePack(url, rp);
System.out.println(success ? "Resource pack was successfully downloaded!" : "Resource Pack download failed.");
if(success) {
repo.func_177319_a(rp);
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
/////////////////////////////////////////////////////////
// Asynchronous packet processing //
/////////////////////////////////////////////////////////

View File

@@ -1,5 +1,6 @@
package eu.crushedpixel.replaymod.studio;
import com.google.common.io.Files;
import com.google.gson.JsonObject;
import de.johni0702.replaystudio.PacketData;
import de.johni0702.replaystudio.filter.ChangeTimestampFilter;
@@ -9,13 +10,17 @@ import de.johni0702.replaystudio.io.ReplayOutputStream;
import de.johni0702.replaystudio.stream.PacketStream;
import de.johni0702.replaystudio.studio.ReplayStudio;
import eu.crushedpixel.replaymod.recording.ReplayMetaData;
import eu.crushedpixel.replaymod.utils.ReplayFile;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class StudioImplementation {
@@ -49,13 +54,25 @@ public class StudioImplementation {
out.close();
ReplayMetaData metaData = ReplayFileIO.getMetaData(replayFile);
ReplayFile replay = new ReplayFile(replayFile);
ReplayMetaData metaData = replay.metadata().get();
ending = Math.min(metaData.getDuration(), ending);
metaData.setDuration(ending - beginning);
Map<Integer, String> resourcePackIndex = replay.resourcePackIndex().get();
Map<String, File> resourcePacks = new HashMap<String, File>();
File tempFolder = Files.createTempDir();
for (String hash : resourcePackIndex.values()) {
if (!resourcePacks.containsKey(hash)) {
File resourcePack = new File(tempFolder, hash);
IOUtils.copy(replay.resourcePack(hash).get(), new FileOutputStream(resourcePack));
resourcePacks.put(hash, resourcePack);
}
}
outputFile.createNewFile();
ReplayFileIO.writeReplayFile(outputFile, temp, metaData);
ReplayFileIO.writeReplayFile(outputFile, temp, metaData, resourcePacks, resourcePackIndex);
}
//TODO Work with Johni to connect multiple Replay Files

View File

@@ -2,6 +2,8 @@ package eu.crushedpixel.replaymod.utils;
import com.google.common.base.Supplier;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import eu.crushedpixel.replaymod.holders.KeyframeSet;
import eu.crushedpixel.replaymod.holders.PlayerVisibility;
import eu.crushedpixel.replaymod.recording.ReplayMetaData;
@@ -11,6 +13,8 @@ import org.apache.commons.compress.archivers.zip.ZipFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class ReplayFile extends ZipFile {
@@ -21,6 +25,8 @@ public class ReplayFile extends ZipFile {
public static final String ENTRY_METADATA = "metaData" + JSON_FILE_EXTENSION;
public static final String ENTRY_PATHS = "paths";
public static final String ENTRY_THUMB = "thumb";
public static final String ENTRY_RESOURCE_PACK = "resourcepack/%s.zip";
public static final String ENTRY_RESOURCE_PACK_INDEX = "resourcepack/index.json";
public static final String ENTRY_VISIBILITY = "visibility";
private final File file;
@@ -138,4 +144,52 @@ public class ReplayFile extends ZipFile {
}
};
}
public ZipArchiveEntry resourcePackIndexEntry() {
return getEntry(ENTRY_RESOURCE_PACK_INDEX);
}
public Supplier<Map<Integer, String>> resourcePackIndex() {
return new Supplier<Map<Integer, String>>() {
@Override
@SuppressWarnings("unchecked")
public Map<Integer, String> get() {
try {
ZipArchiveEntry entry = resourcePackIndexEntry();
if (entry == null) {
return null;
}
Map<Integer, String> index = new HashMap<Integer, String>();
JsonObject json = new Gson().fromJson(new InputStreamReader(getInputStream(entry)), JsonObject.class);
for (Map.Entry<String, JsonElement> e : json.entrySet()) {
index.put(Integer.parseInt(e.getKey()), e.getValue().getAsString());
}
return index;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
}
public ZipArchiveEntry resourcePackEntry(String hash) {
return getEntry(String.format(ENTRY_RESOURCE_PACK, hash));
}
public Supplier<InputStream> resourcePack(final String hash) {
return new Supplier<InputStream>() {
@Override
public InputStream get() {
try {
ZipArchiveEntry entry = resourcePackEntry(hash);
if (entry == null) {
return null;
}
return getInputStream(entry);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
}
}

View File

@@ -16,12 +16,14 @@ import net.minecraft.network.PacketBuffer;
import net.minecraft.network.play.server.S01PacketJoinGame;
import net.minecraft.network.play.server.S08PacketPlayerPosLook;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@@ -58,7 +60,8 @@ public class ReplayFileIO {
return files;
}
public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData) throws IOException {
public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData,
Map<String, File> resourcePacks, Map<Integer, String> resourcePackRequests) throws IOException {
byte[] buffer = new byte[1024];
if(!replayFile.exists()) {
@@ -86,6 +89,20 @@ public class ReplayFileIO {
fis.close();
zos.closeEntry();
if (!resourcePackRequests.isEmpty()) {
zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_RESOURCE_PACK_INDEX));
pw = new PrintWriter(zos);
pw.write(new Gson().toJson(resourcePackRequests));
pw.flush();
zos.closeEntry();
for (Map.Entry<String, File> entry : resourcePacks.entrySet()) {
zos.putNextEntry(new ZipEntry(String.format(ReplayFile.ENTRY_RESOURCE_PACK, entry.getKey())));
IOUtils.copy(new FileInputStream(entry.getValue()), zos);
zos.closeEntry();
}
}
zos.close();
}

View File

@@ -9,6 +9,7 @@ public net.minecraft.client.Minecraft field_71429_W # leftClickCounter
public net.minecraft.client.Minecraft field_71445_n # isGamePaused
public net.minecraft.client.Minecraft field_152351_aB # scheduledTasks
public net.minecraft.client.Minecraft field_110449_ao # defaultResourcePacks
public net.minecraft.client.Minecraft field_110448_aq # mcResourcePackRepository
public net.minecraft.client.Minecraft func_71386_F()L # getSystemTime
public net.minecraft.client.Minecraft func_71383_b(I)V # updateDebugProfilerName
@@ -36,6 +37,8 @@ public net.minecraft.entity.Entity field_70180_af # dataWatcher
# ResourcePackRepository
public net.minecraft.client.resources.ResourcePackRepository field_148534_e # dirServerResourcepacks
public net.minecraft.client.resources.ResourcePackRepository field_177321_h # lock
public net.minecraft.client.resources.ResourcePackRepository field_177322_i # httpRequest
# EntityRenderer
public net.minecraft.client.renderer.EntityRenderer field_147711_ac # resourceManager