package cpw.mods.fml.common.asm.transformers;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
public class MCPMerger
{
private static Hashtable<String, ClassInfo> clients = new Hashtable<String, ClassInfo>();
private static Hashtable<String, ClassInfo> shared = new Hashtable<String, ClassInfo>();
private static Hashtable<String, ClassInfo> servers = new Hashtable<String, ClassInfo>();
private static HashSet<String> copyToServer = new HashSet<String>();
private static HashSet<String> copyToClient = new HashSet<String>();
private static HashSet<String> dontAnnotate = new HashSet<String>();
private static final boolean DEBUG = false;
public static void main(String[] args)
{
if (args.length != 3)
{
System.out.println("Usage: MCPMerger <MapFile> <minecraft.jar> <minecraft_server.jar>");
System.exit(1);
}
File map_file = new File(args[0]);
File client_jar = new File(args[1]);
File server_jar = new File(args[2]);
File client_jar_tmp = new File(args[1] + ".MergeBack");
File server_jar_tmp = new File(args[2] + ".MergeBack");
if (client_jar_tmp.exists() && !client_jar_tmp.delete())
{
System.out.println("Could not delete temp file: " + client_jar_tmp);
}
if (server_jar_tmp.exists() && !server_jar_tmp.delete())
{
System.out.println("Could not delete temp file: " + server_jar_tmp);
}
if (!client_jar.exists())
{
System.out.println("Could not find minecraft.jar: " + client_jar);
System.exit(1);
}
if (!server_jar.exists())
{
System.out.println("Could not find minecraft_server.jar: " + server_jar);
System.exit(1);
}
if (!client_jar.renameTo(client_jar_tmp))
{
System.out.println("Could not rename file: " + client_jar + " -> " + client_jar_tmp);
System.exit(1);
}
if (!server_jar.renameTo(server_jar_tmp))
{
System.out.println("Could not rename file: " + server_jar + " -> " + server_jar_tmp);
System.exit(1);
}
if (!readMapFile(map_file))
{
System.out.println("Could not read map file: " + map_file);
System.exit(1);
}
try
{
processJar(client_jar_tmp, server_jar_tmp, client_jar, server_jar);
}
catch (IOException e)
{
e.printStackTrace();
System.exit(1);
}
if (!client_jar_tmp.delete())
{
System.out.println("Could not delete temp file: " + client_jar_tmp);
}
if (!server_jar_tmp.delete())
{
System.out.println("Could not delete temp file: " + server_jar_tmp);
}
}
private static boolean readMapFile(File mapFile)
{
try
{
FileInputStream fstream = new FileInputStream(mapFile);
DataInputStream in = new DataInputStream(fstream);
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = br.readLine()) != null)
{
line = line.split("#")[0];
char cmd = line.charAt(0);
line = line.substring(1).trim();
switch (cmd)
{
case '!': dontAnnotate.add(line); break;
case '<': copyToClient.add(line); break;
case '>': copyToServer.add(line); break;
}
}
in.close();
return true;
}
catch (Exception e)
{
System.err.println("Error: " + e.getMessage());
return false;
}
}
public static void processJar(File clientInFile, File serverInFile, File clientOutFile, File serverOutFile) throws IOException
{
ZipFile cInJar = null;
ZipFile sInJar = null;
ZipOutputStream cOutJar = null;
ZipOutputStream sOutJar = null;
try
{
try
{
cInJar = new ZipFile(clientInFile);
sInJar = new ZipFile(serverInFile);
}
catch (FileNotFoundException e)
{
throw new FileNotFoundException("Could not open input file: " + e.getMessage());
}
try
{
cOutJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(clientOutFile)));
sOutJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(serverOutFile)));
}
catch (FileNotFoundException e)
{
throw new FileNotFoundException("Could not open output file: " + e.getMessage());
}
Hashtable<String, ZipEntry> cClasses = getClassEntries(cInJar, cOutJar);
Hashtable<String, ZipEntry> sClasses = getClassEntries(sInJar, sOutJar);
HashSet<String> cAdded = new HashSet<String>();
HashSet<String> sAdded = new HashSet<String>();
for (Entry<String, ZipEntry> entry : cClasses.entrySet())
{
String name = entry.getKey();
ZipEntry cEntry = entry.getValue();
ZipEntry sEntry = sClasses.get(name);
if (sEntry == null)
{
if (!copyToServer.contains(name))
{
copyClass(cInJar, cEntry, cOutJar, null, true);
cAdded.add(name);
}
else
{
if (DEBUG)
{
System.out.println("Copy class c->s : " + name);
}
copyClass(cInJar, cEntry, cOutJar, sOutJar, true);
cAdded.add(name);
sAdded.add(name);
}
continue;
}
sClasses.remove(name);
ClassInfo info = new ClassInfo(name);
shared.put(name, info);
byte[] cData = readEntry(cInJar, entry.getValue());
byte[] sData = readEntry(sInJar, sEntry);
byte[] data = processClass(cData, sData, info);
ZipEntry newEntry = new ZipEntry(cEntry.getName());
cOutJar.putNextEntry(newEntry);
cOutJar.write(data);
sOutJar.putNextEntry(newEntry);
sOutJar.write(data);
cAdded.add(name);
sAdded.add(name);
}
for (Entry<String, ZipEntry> entry : sClasses.entrySet())
{
if (DEBUG)
{
System.out.println("Copy class s->c : " + entry.getKey());
}
copyClass(sInJar, entry.getValue(), cOutJar, sOutJar, false);
}
for (String name : new String[]{SideOnly.class.getName(), Side.class.getName()})
{
String eName = name.replace(".", "/");
byte[] data = getClassBytes(name);
ZipEntry newEntry = new ZipEntry(name.replace(".", "/").concat(".class"));
if (!cAdded.contains(eName))
{
cOutJar.putNextEntry(newEntry);
cOutJar.write(data);
}
if (!sAdded.contains(eName))
{
sOutJar.putNextEntry(newEntry);
sOutJar.write(data);
}
}
}
finally
{
if (cInJar != null)
{
try { cInJar.close(); } catch (IOException e){}
}
if (sInJar != null)
{
try { sInJar.close(); } catch (IOException e) {}
}
if (cOutJar != null)
{
try { cOutJar.close(); } catch (IOException e){}
}
if (sOutJar != null)
{
try { sOutJar.close(); } catch (IOException e) {}
}
}
}
private static void copyClass(ZipFile inJar, ZipEntry entry, ZipOutputStream outJar, ZipOutputStream outJar2, boolean isClientOnly) throws IOException
{
ClassReader reader = new ClassReader(readEntry(inJar, entry));
ClassNode classNode = new ClassNode();
reader.accept(classNode, 0);
if (!dontAnnotate.contains(classNode.name))
{
if (classNode.visibleAnnotations == null) classNode.visibleAnnotations = new ArrayList<AnnotationNode>();
classNode.visibleAnnotations.add(getSideAnn(isClientOnly));
}
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(writer);
byte[] data = writer.toByteArray();
ZipEntry newEntry = new ZipEntry(entry.getName());
if (outJar != null)
{
outJar.putNextEntry(newEntry);
outJar.write(data);
}
if (outJar2 != null)
{
outJar2.putNextEntry(newEntry);
outJar2.write(data);
}
}
private static AnnotationNode getSideAnn(boolean isClientOnly)
{
AnnotationNode ann = new AnnotationNode(Type.getDescriptor(SideOnly.class));
ann.values = new ArrayList<Object>();
ann.values.add("value");
ann.values.add(new String[]{ Type.getDescriptor(Side.class), (isClientOnly ? "CLIENT" : "SERVER")});
return ann;
}
@SuppressWarnings("unchecked")
private static Hashtable<String, ZipEntry> getClassEntries(ZipFile inFile, ZipOutputStream outFile) throws IOException
{
Hashtable<String, ZipEntry> ret = new Hashtable<String, ZipEntry>();
for (ZipEntry entry : Collections.list((Enumeration<ZipEntry>)inFile.entries()))
{
if (entry.isDirectory())
{
outFile.putNextEntry(entry);
continue;
}
String entryName = entry.getName();
if (!entryName.endsWith(".class") || entryName.startsWith("."))
{
ZipEntry newEntry = new ZipEntry(entry.getName());
outFile.putNextEntry(newEntry);
outFile.write(readEntry(inFile, entry));
}
else
{
ret.put(entryName.replace(".class", ""), entry);
}
}
return ret;
}
private static byte[] readEntry(ZipFile inFile, ZipEntry entry) throws IOException
{
return readFully(inFile.getInputStream(entry));
}
private static byte[] readFully(InputStream stream) throws IOException
{
byte[] data = new byte[4096];
ByteArrayOutputStream entryBuffer = new ByteArrayOutputStream();
int len;
do
{
len = stream.read(data);
if (len > 0)
{
entryBuffer.write(data, 0, len);
}
} while (len != -1);
return entryBuffer.toByteArray();
}
private static class ClassInfo
{
public String name;
public ArrayList<FieldNode> cField = new ArrayList<FieldNode>();
public ArrayList<FieldNode> sField = new ArrayList<FieldNode>();
public ArrayList<MethodNode> cMethods = new ArrayList<MethodNode>();
public ArrayList<MethodNode> sMethods = new ArrayList<MethodNode>();
public ClassInfo(String name){ this.name = name; }
public boolean isSame() { return (cField.size() == 0 && sField.size() == 0 && cMethods.size() == 0 && sMethods.size() == 0); }
}
public static byte[] processClass(byte[] cIn, byte[] sIn, ClassInfo info)
{
ClassNode cClassNode = getClassNode(cIn);
ClassNode sClassNode = getClassNode(sIn);
processFields(cClassNode, sClassNode, info);
processMethods(cClassNode, sClassNode, info);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cClassNode.accept(writer);
return writer.toByteArray();
}
private static ClassNode getClassNode(byte[] data)
{
ClassReader reader = new ClassReader(data);
ClassNode classNode = new ClassNode();
reader.accept(classNode, 0);
return classNode;
}
@SuppressWarnings("unchecked")
private static void processFields(ClassNode cClass, ClassNode sClass, ClassInfo info)
{
List<FieldNode> cFields = cClass.fields;
List<FieldNode> sFields = sClass.fields;
int sI = 0;
for (int x = 0; x < cFields.size(); x++)
{
FieldNode cF = cFields.get(x);
if (sI < sFields.size())
{
if (!cF.name.equals(sFields.get(sI).name))
{
boolean serverHas = false;
for (int y = sI + 1; y < sFields.size(); y++)
{
if (cF.name.equals(sFields.get(y).name))
{
serverHas = true;
break;
}
}
if (serverHas)
{
boolean clientHas = false;
FieldNode sF = sFields.get(sI);
for (int y = x + 1; y < cFields.size(); y++)
{
if (sF.name.equals(cFields.get(y).name))
{
clientHas = true;
break;
}
}
if (!clientHas)
{
if (sF.visibleAnnotations == null) sF.visibleAnnotations = new ArrayList<AnnotationNode>();
sF.visibleAnnotations.add(getSideAnn(false));
cFields.add(x++, sF);
info.sField.add(sF);
}
}
else
{
if (cF.visibleAnnotations == null) cF.visibleAnnotations = new ArrayList<AnnotationNode>();
cF.visibleAnnotations.add(getSideAnn(true));
sFields.add(sI, cF);
info.cField.add(cF);
}
}
}
else
{
if (cF.visibleAnnotations == null) cF.visibleAnnotations = new ArrayList<AnnotationNode>();
cF.visibleAnnotations.add(getSideAnn(true));
sFields.add(sI, cF);
info.cField.add(cF);
}
sI++;
}
if (sFields.size() != cFields.size())
{
for (int x = cFields.size(); x < sFields.size(); x++)
{
FieldNode sF = sFields.get(x);
if (sF.visibleAnnotations == null) sF.visibleAnnotations = new ArrayList<AnnotationNode>();
sF.visibleAnnotations.add(getSideAnn(true));
cFields.add(x++, sF);
info.sField.add(sF);
}
}
}
private static class MethodWrapper
{
private MethodNode node;
public boolean client;
public boolean server;
public MethodWrapper(MethodNode node)
{
this.node = node;
}
@Override
public boolean equals(Object obj)
{
if (obj == null || !(obj instanceof MethodWrapper)) return false;
MethodWrapper mw = (MethodWrapper) obj;
boolean eq = Objects.equal(node.name, mw.node.name) && Objects.equal(node.desc, mw.node.desc);
if (eq)
{
mw.client = this.client | mw.client;
mw.server = this.server | mw.server;
this.client = this.client | mw.client;
this.server = this.server | mw.server;
if (DEBUG)
{
System.out.printf(" eq: %s %s\n", this, mw);
}
}
return eq;
}
@Override
public int hashCode()
{
return Objects.hashCode(node.name, node.desc);
}
@Override
public String toString()
{
return Objects.toStringHelper(this).add("name", node.name).add("desc",node.desc).add("server",server).add("client",client).toString();
}
}
@SuppressWarnings("unchecked")
private static void processMethods(ClassNode cClass, ClassNode sClass, ClassInfo info)
{
List<MethodNode> cMethods = (List<MethodNode>)cClass.methods;
List<MethodNode> sMethods = (List<MethodNode>)sClass.methods;
LinkedHashSet<MethodWrapper> allMethods = Sets.newLinkedHashSet();
int cPos = 0;
int sPos = 0;
int cLen = cMethods.size();
int sLen = sMethods.size();
String clientName = "";
String lastName = clientName;
String serverName = "";
while (cPos < cLen || sPos < sLen)
{
do
{
if (sPos>=sLen)
{
break;
}
MethodNode sM = sMethods.get(sPos);
serverName = sM.name;
if (!serverName.equals(lastName) && cPos != cLen)
{
if (DEBUG)
{
System.out.printf("Server -skip : %s %s %d (%s %d) %d [%s]\n", sClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
}
break;
}
MethodWrapper mw = new MethodWrapper(sM);
mw.server = true;
allMethods.add(mw);
if (DEBUG)
{
System.out.printf("Server *add* : %s %s %d (%s %d) %d [%s]\n", sClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
}
sPos++;
}
while (sPos < sLen);
do
{
if (cPos>=cLen)
{
break;
}
MethodNode cM = cMethods.get(cPos);
lastName = clientName;
clientName = cM.name;
if (!clientName.equals(lastName) && sPos != sLen)
{
if (DEBUG)
{
System.out.printf("Client -skip : %s %s %d (%s %d) %d [%s]\n", cClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
}
break;
}
MethodWrapper mw = new MethodWrapper(cM);
mw.client = true;
allMethods.add(mw);
if (DEBUG)
{
System.out.printf("Client *add* : %s %s %d (%s %d) %d [%s]\n", cClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
}
cPos++;
}
while (cPos < cLen);
}
cMethods.clear();
sMethods.clear();
for (MethodWrapper mw : allMethods)
{
if (DEBUG)
{
System.out.println(mw);
}
cMethods.add(mw.node);
sMethods.add(mw.node);
if (mw.server && mw.client)
{
// no op
}
else
{
if (mw.node.visibleAnnotations == null) mw.node.visibleAnnotations = Lists.newArrayListWithExpectedSize(1);
mw.node.visibleAnnotations.add(getSideAnn(mw.client));
if (mw.client)
{
info.sMethods.add(mw.node);
}
else
{
info.cMethods.add(mw.node);
}
}
}
}
public static byte[] getClassBytes(String name) throws IOException
{
InputStream classStream = null;
try
{
classStream = MCPMerger.class.getResourceAsStream("/" + name.replace('.', '/').concat(".class"));
return readFully(classStream);
}
finally
{
if (classStream != null)
{
try
{
classStream.close();
}
catch (IOException e){}
}
}
}
}