    001    package cpw.mods.fml.common.asm.transformers;
    002
    003    import java.io.BufferedOutputStream;
    004    import java.io.BufferedReader;
    005    import java.io.ByteArrayOutputStream;
    006    import java.io.DataInputStream;
    007    import java.io.File;
    008    import java.io.FileInputStream;
    009    import java.io.FileNotFoundException;
    010    import java.io.FileOutputStream;
    011    import java.io.IOException;
    012    import java.io.InputStream;
    013    import java.io.InputStreamReader;
    014    import java.util.ArrayList;
    015    import java.util.Collections;
    016    import java.util.Enumeration;
    017    import java.util.HashSet;
    018    import java.util.Hashtable;
    019    import java.util.LinkedHashSet;
    020    import java.util.List;
    021    import java.util.Map.Entry;
    022    import java.util.zip.ZipEntry;
    023    import java.util.zip.ZipFile;
    024    import java.util.zip.ZipOutputStream;
    025
    026    import org.objectweb.asm.ClassReader;
    027    import org.objectweb.asm.ClassWriter;
    028    import org.objectweb.asm.Type;
    029    import org.objectweb.asm.tree.AnnotationNode;
    030    import org.objectweb.asm.tree.ClassNode;
    031    import org.objectweb.asm.tree.FieldNode;
    032    import org.objectweb.asm.tree.MethodNode;
    033
    034    import com.google.common.base.Objects;
    035    import com.google.common.collect.Lists;
    036    import com.google.common.collect.Sets;
    037
    038    import cpw.mods.fml.common.Side;
    039    import cpw.mods.fml.common.asm.SideOnly;
    040
    041    public class MCPMerger
    042    {
    043        private static Hashtable<String, ClassInfo> clients = new Hashtable<String, ClassInfo>();
    044        private static Hashtable<String, ClassInfo> shared  = new Hashtable<String, ClassInfo>();
    045        private static Hashtable<String, ClassInfo> servers = new Hashtable<String, ClassInfo>();
    046        private static HashSet<String> copyToServer = new HashSet<String>();
    047        private static HashSet<String> copyToClient = new HashSet<String>();
    048        private static final boolean DEBUG = false;
    049
    050        public static void main(String[] args)
    051        {
    052            if (args.length != 3)
    053            {
    054                System.out.println("Usage: MCPMerger <MapFile> <minecraft.jar> <minecraft_server.jar>");
    055                System.exit(1);
    056            }
    057
    058            File map_file = new File(args[0]);
    059            File client_jar = new File(args[1]);
    060            File server_jar = new File(args[2]);
    061            File client_jar_tmp = new File(args[1] + ".MergeBack");
    062            File server_jar_tmp = new File(args[2] + ".MergeBack");
    063
    064
    065            if (client_jar_tmp.exists() && !client_jar_tmp.delete())
    066            {
    067                System.out.println("Could not delete temp file: " + client_jar_tmp);
    068            }
    069
    070            if (server_jar_tmp.exists() && !server_jar_tmp.delete())
    071            {
    072                System.out.println("Could not delete temp file: " + server_jar_tmp);
    073            }
    074
    075            if (!client_jar.exists())
    076            {
    077                System.out.println("Could not find minecraft.jar: " + client_jar);
    078                System.exit(1);
    079            }
    080
    081            if (!server_jar.exists())
    082            {
    083                System.out.println("Could not find minecraft_server.jar: " + server_jar);
    084                System.exit(1);
    085            }
    086
    087            if (!client_jar.renameTo(client_jar_tmp))
    088            {
    089                System.out.println("Could not rename file: " + client_jar + " -> " + client_jar_tmp);
    090                System.exit(1);
    091            }
    092
    093            if (!server_jar.renameTo(server_jar_tmp))
    094            {
    095                System.out.println("Could not rename file: " + server_jar + " -> " + server_jar_tmp);
    096                System.exit(1);
    097            }
    098
    099            if (!readMapFile(map_file))
    100            {
    101                System.out.println("Could not read map file: " + map_file);
    102                System.exit(1);
    103            }
    104
    105            try
    106            {
    107                processJar(client_jar_tmp, server_jar_tmp, client_jar, server_jar);
    108            }
    109            catch (IOException e)
    110            {
    111                e.printStackTrace();
    112                System.exit(1);
    113            }
    114
    115            if (!client_jar_tmp.delete())
    116            {
    117                System.out.println("Could not delete temp file: " + client_jar_tmp);
    118            }
    119
    120            if (!server_jar_tmp.delete())
    121            {
    122                System.out.println("Could not delete temp file: " + server_jar_tmp);
    123            }
    124        }
    125
    126        private static boolean readMapFile(File mapFile)
    127        {
    128            try
    129            {
    130                FileInputStream fstream = new FileInputStream(mapFile);
    131                DataInputStream in = new DataInputStream(fstream);
    132                BufferedReader br = new BufferedReader(new InputStreamReader(in));
    133
    134                String line;
    135                while ((line = br.readLine()) != null)
    136                {
    137            line = line.split("#")[0];
    138                    boolean toClient = line.charAt(0) == '<';
    139                    line = line.substring(1).trim();
    140                    if (toClient) copyToClient.add(line);
    141                    else copyToServer.add(line);
    142                }
    143
    144                in.close();
    145                return true;
    146            }
    147            catch (Exception e)
    148            {
    149                System.err.println("Error: " + e.getMessage());
    150                return false;
    151            }
    152        }
    153
    154        public static void processJar(File clientInFile, File serverInFile, File clientOutFile, File serverOutFile) throws IOException
    155        {
    156            ZipFile cInJar = null;
    157            ZipFile sInJar = null;
    158            ZipOutputStream cOutJar = null;
    159            ZipOutputStream sOutJar = null;
    160
    161            try
    162            {
    163                try
    164                {
    165                    cInJar = new ZipFile(clientInFile);
    166                    sInJar = new ZipFile(serverInFile);
    167                }
    168                catch (FileNotFoundException e)
    169                {
    170                    throw new FileNotFoundException("Could not open input file: " + e.getMessage());
    171                }
    172                try
    173                {
    174                    cOutJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(clientOutFile)));
    175                    sOutJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(serverOutFile)));
    176                }
    177                catch (FileNotFoundException e)
    178                {
    179                    throw new FileNotFoundException("Could not open output file: " + e.getMessage());
    180                }
    181                Hashtable<String, ZipEntry> cClasses = getClassEntries(cInJar, cOutJar);
    182                Hashtable<String, ZipEntry> sClasses = getClassEntries(sInJar, sOutJar);
    183                HashSet<String> cAdded = new HashSet<String>();
    184                HashSet<String> sAdded = new HashSet<String>();
    185
    186                for (Entry<String, ZipEntry> entry : cClasses.entrySet())
    187                {
    188                    String name = entry.getKey();
    189                    ZipEntry cEntry = entry.getValue();
    190                    ZipEntry sEntry = sClasses.get(name);
    191
    192                    if (sEntry == null)
    193                    {
    194                        if (!copyToServer.contains(name))
    195                        {
    196                            copyClass(cInJar, cEntry, cOutJar, null, true);
    197                            cAdded.add(name);
    198                        }
    199                        else
    200                        {
    201                            if (DEBUG)
    202                            {
    203                                System.out.println("Copy class c->s : " + name);
    204                            }
    205                            copyClass(cInJar, cEntry, cOutJar, sOutJar, true);
    206                            cAdded.add(name);
    207                            sAdded.add(name);
    208                        }
    209                        continue;
    210                    }
    211
    212                    sClasses.remove(name);
    213                    ClassInfo info = new ClassInfo(name);
    214                    shared.put(name, info);
    215
    216                    byte[] cData = readEntry(cInJar, entry.getValue());
    217                    byte[] sData = readEntry(sInJar, sEntry);
    218                    byte[] data = processClass(cData, sData, info);
    219
    220                    ZipEntry newEntry = new ZipEntry(cEntry.getName());
    221                    cOutJar.putNextEntry(newEntry);
    222                    cOutJar.write(data);
    223                    sOutJar.putNextEntry(newEntry);
    224                    sOutJar.write(data);
    225                    cAdded.add(name);
    226                    sAdded.add(name);
    227                }
    228                for (Entry<String, ZipEntry> entry : sClasses.entrySet())
    229                {
    230                    if (!copyToClient.contains(entry.getKey()))
    231                    {
    232                        copyClass(sInJar, entry.getValue(), null, sOutJar, false);
    233                    }
    234                    else
    235                    {
    236                        if (DEBUG)
    237                        {
    238                            System.out.println("Copy class s->c : " + entry.getKey());
    239                        }
    240                        copyClass(sInJar, entry.getValue(), cOutJar, sOutJar, false);
    241                    }
    242                }
    243
    244                for (String name : new String[]{SideOnly.class.getName(), Side.class.getName()})
    245                {
    246                    String eName = name.replace(".", "/");
    247                    byte[] data = getClassBytes(name);
    248                    ZipEntry newEntry = new ZipEntry(name.replace(".", "/").concat(".class"));
    249                    if (!cAdded.contains(eName))
    250                    {
    251                        cOutJar.putNextEntry(newEntry);
    252                        cOutJar.write(data);
    253                    }
    254                    if (!sAdded.contains(eName))
    255                    {
    256                        sOutJar.putNextEntry(newEntry);
    257                        sOutJar.write(data);
    258                    }
    259                }
    260
    261            }
    262            finally
    263            {
    264                if (cInJar != null)
    265                {
    266                    try { cInJar.close(); } catch (IOException e){}
    267                }
    268
    269                if (sInJar != null)
    270                {
    271                    try { sInJar.close(); } catch (IOException e) {}
    272                }
    273                if (cOutJar != null)
    274                {
    275                    try { cOutJar.close(); } catch (IOException e){}
    276                }
    277
    278                if (sOutJar != null)
    279                {
    280                    try { sOutJar.close(); } catch (IOException e) {}
    281                }
    282            }
    283        }
    284
    285        private static void copyClass(ZipFile inJar, ZipEntry entry, ZipOutputStream outJar, ZipOutputStream outJar2, boolean isClientOnly) throws IOException
    286        {
    287            ClassReader reader = new ClassReader(readEntry(inJar, entry));
    288            ClassNode classNode = new ClassNode();
    289
    290            reader.accept(classNode, 0);
    291
    292            if (!classNode.name.equals("bdz")) //Special case CodecMus so I dont have to make a new patch, anyone who uses this in production code is.. bad.
    293            {
    294                if (classNode.visibleAnnotations == null) classNode.visibleAnnotations = new ArrayList<AnnotationNode>();
    295                classNode.visibleAnnotations.add(getSideAnn(isClientOnly));
    296            }
    297
    298            ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    299            classNode.accept(writer);
    300            byte[] data = writer.toByteArray();
    301
    302            ZipEntry newEntry = new ZipEntry(entry.getName());
    303            if (outJar != null)
    304            {
    305                outJar.putNextEntry(newEntry);
    306                outJar.write(data);
    307            }
    308            if (outJar2 != null)
    309            {
    310                outJar2.putNextEntry(newEntry);
    311                outJar2.write(data);
    312            }
    313        }
    314
    315        private static AnnotationNode getSideAnn(boolean isClientOnly)
    316        {
    317            AnnotationNode ann = new AnnotationNode(Type.getDescriptor(SideOnly.class));
    318            ann.values = new ArrayList<Object>();
    319            ann.values.add("value");
    320            ann.values.add(new String[]{ Type.getDescriptor(Side.class), (isClientOnly ? "CLIENT" : "SERVER")});
    321            return ann;
    322        }
    323
    324        @SuppressWarnings("unchecked")
    325        private static Hashtable<String, ZipEntry> getClassEntries(ZipFile inFile, ZipOutputStream outFile) throws IOException
    326        {
    327            Hashtable<String, ZipEntry> ret = new Hashtable<String, ZipEntry>();
    328            for (ZipEntry entry : Collections.list((Enumeration<ZipEntry>)inFile.entries()))
    329            {
    330                if (entry.isDirectory())
    331                {
    332                    outFile.putNextEntry(entry);
    333                    continue;
    334                }
    335                String entryName = entry.getName();
    336                if (!entryName.endsWith(".class") || entryName.startsWith("."))
    337                {
    338                    ZipEntry newEntry = new ZipEntry(entry.getName());
    339                    outFile.putNextEntry(newEntry);
    340                    outFile.write(readEntry(inFile, entry));
    341                }
    342                else
    343                {
    344                    ret.put(entryName.replace(".class", ""), entry);
    345                }
    346            }
    347            return ret;
    348        }
    349        private static byte[] readEntry(ZipFile inFile, ZipEntry entry) throws IOException
    350        {
    351            return readFully(inFile.getInputStream(entry));
    352        }
    353        private static byte[] readFully(InputStream stream) throws IOException
    354        {
    355            byte[] data = new byte[4096];
    356            ByteArrayOutputStream entryBuffer = new ByteArrayOutputStream();
    357            int len;
    358            do
    359            {
    360                len = stream.read(data);
    361                if (len > 0)
    362                {
    363                    entryBuffer.write(data, 0, len);
    364                }
    365            } while (len != -1);
    366
    367            return entryBuffer.toByteArray();
    368        }
    369        private static class ClassInfo
    370        {
    371            public String name;
    372            public ArrayList<FieldNode> cField = new ArrayList<FieldNode>();
    373            public ArrayList<FieldNode> sField = new ArrayList<FieldNode>();
    374            public ArrayList<MethodNode> cMethods = new ArrayList<MethodNode>();
    375            public ArrayList<MethodNode> sMethods = new ArrayList<MethodNode>();
    376            public ClassInfo(String name){ this.name = name; }
    377            public boolean isSame() { return (cField.size() == 0 && sField.size() == 0 && cMethods.size() == 0 && sMethods.size() == 0); }
    378        }
    379
    380        public static byte[] processClass(byte[] cIn, byte[] sIn, ClassInfo info)
    381        {
    382            ClassNode cClassNode = getClassNode(cIn);
    383            ClassNode sClassNode = getClassNode(sIn);
    384
    385            processFields(cClassNode, sClassNode, info);
    386            processMethods(cClassNode, sClassNode, info);
    387
    388            ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    389            cClassNode.accept(writer);
    390            return writer.toByteArray();
    391        }
    392
    393        private static ClassNode getClassNode(byte[] data)
    394        {
    395            ClassReader reader = new ClassReader(data);
    396            ClassNode classNode = new ClassNode();
    397            reader.accept(classNode, 0);
    398            return classNode;
    399        }
    400
    401        @SuppressWarnings("unchecked")
    402        private static void processFields(ClassNode cClass, ClassNode sClass, ClassInfo info)
    403        {
    404            List<FieldNode> cFields = cClass.fields;
    405            List<FieldNode> sFields = sClass.fields;
    406
    407            int sI = 0;
    408            for (int x = 0; x < cFields.size(); x++)
    409            {
    410                FieldNode cF = cFields.get(x);
    411                if (sI < sFields.size())
    412                {
    413                    if (!cF.name.equals(sFields.get(sI).name))
    414                    {
    415                        boolean serverHas = false;
    416                        for (int y = sI + 1; y < sFields.size(); y++)
    417                        {
    418                            if (cF.name.equals(sFields.get(y).name))
    419                            {
    420                                serverHas = true;
    421                                break;
    422                            }
    423                        }
    424                        if (serverHas)
    425                        {
    426                            boolean clientHas = false;
    427                            FieldNode sF = sFields.get(sI);
    428                            for (int y = x + 1; y < cFields.size(); y++)
    429                            {
    430                                if (sF.name.equals(cFields.get(y).name))
    431                                {
    432                                    clientHas = true;
    433                                    break;
    434                                }
    435                            }
    436                            if (!clientHas)
    437                            {
    438                                if  (sF.visibleAnnotations == null) sF.visibleAnnotations = new ArrayList<AnnotationNode>();
    439                                sF.visibleAnnotations.add(getSideAnn(false));
    440                                cFields.add(x++, sF);
    441                                info.sField.add(sF);
    442                            }
    443                        }
    444                        else
    445                        {
    446                            if  (cF.visibleAnnotations == null) cF.visibleAnnotations = new ArrayList<AnnotationNode>();
    447                            cF.visibleAnnotations.add(getSideAnn(true));
    448                            sFields.add(sI, cF);
    449                            info.cField.add(cF);
    450                        }
    451                    }
    452                }
    453                else
    454                {
    455                    if  (cF.visibleAnnotations == null) cF.visibleAnnotations = new ArrayList<AnnotationNode>();
    456                    cF.visibleAnnotations.add(getSideAnn(true));
    457                    sFields.add(sI, cF);
    458                    info.cField.add(cF);
    459                }
    460                sI++;
    461            }
    462            if (sFields.size() != cFields.size())
    463            {
    464                for (int x = cFields.size(); x < sFields.size(); x++)
    465                {
    466                    FieldNode sF = sFields.get(x);
    467                    if  (sF.visibleAnnotations == null) sF.visibleAnnotations = new ArrayList<AnnotationNode>();
    468                    sF.visibleAnnotations.add(getSideAnn(true));
    469                    cFields.add(x++, sF);
    470                    info.sField.add(sF);
    471                }
    472            }
    473        }
    474
    475        private static class MethodWrapper
    476        {
    477            private MethodNode node;
    478            public boolean client;
    479            public boolean server;
    480            public MethodWrapper(MethodNode node)
    481            {
    482                this.node = node;
    483            }
    484            @Override
    485            public boolean equals(Object obj)
    486            {
    487                if (obj == null || !(obj instanceof MethodWrapper)) return false;
    488                MethodWrapper mw = (MethodWrapper) obj;
    489                boolean eq = Objects.equal(node.name, mw.node.name) && Objects.equal(node.desc, mw.node.desc);
    490                if (eq)
    491                {
    492                    mw.client = this.client | mw.client;
    493                    mw.server = this.server | mw.server;
    494                    this.client = this.client | mw.client;
    495                    this.server = this.server | mw.server;
    496                    if (DEBUG)
    497                    {
    498                        System.out.printf(" eq: %s %s\n", this, mw);
    499                    }
    500                }
    501                return eq;
    502            }
    503
    504            @Override
    505            public int hashCode()
    506            {
    507                return Objects.hashCode(node.name, node.desc);
    508            }
    509            @Override
    510            public String toString()
    511            {
    512                return Objects.toStringHelper(this).add("name", node.name).add("desc",node.desc).add("server",server).add("client",client).toString();
    513            }
    514        }
    515        @SuppressWarnings("unchecked")
    516        private static void processMethods(ClassNode cClass, ClassNode sClass, ClassInfo info)
    517        {
    518            List<MethodNode> cMethods = (List<MethodNode>)cClass.methods;
    519            List<MethodNode> sMethods = (List<MethodNode>)sClass.methods;
    520            LinkedHashSet<MethodWrapper> allMethods = Sets.newLinkedHashSet();
    521
    522            int cPos = 0;
    523            int sPos = 0;
    524            int cLen = cMethods.size();
    525            int sLen = sMethods.size();
    526            String clientName = "";
    527            String lastName = clientName;
    528            String serverName = "";
    529            while (cPos < cLen || sPos < sLen)
    530            {
    531                do
    532                {
    533                    if (sPos>=sLen)
    534                    {
    535                        break;
    536                    }
    537                    MethodNode sM = sMethods.get(sPos);
    538                    serverName = sM.name;
    539                    if (!serverName.equals(lastName) && cPos != cLen)
    540                    {
    541                        if (DEBUG)
    542                        {
    543                            System.out.printf("Server -skip : %s %s %d (%s %d) %d [%s]\n", sClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
    544                        }
    545                        break;
    546                    }
    547                    MethodWrapper mw = new MethodWrapper(sM);
    548                    mw.server = true;
    549                    allMethods.add(mw);
    550                    if (DEBUG)
    551                    {
    552                        System.out.printf("Server *add* : %s %s %d (%s %d) %d [%s]\n", sClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
    553                    }
    554                    sPos++;
    555                }
    556                while (sPos < sLen);
    557                do
    558                {
    559                    if (cPos>=cLen)
    560                    {
    561                        break;
    562                    }
    563                    MethodNode cM = cMethods.get(cPos);
    564                    lastName = clientName;
    565                    clientName = cM.name;
    566                    if (!clientName.equals(lastName) && sPos != sLen)
    567                    {
    568                        if (DEBUG)
    569                        {
    570                            System.out.printf("Client -skip : %s %s %d (%s %d) %d [%s]\n", cClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
    571                        }
    572                        break;
    573                    }
    574                    MethodWrapper mw = new MethodWrapper(cM);
    575                    mw.client = true;
    576                    allMethods.add(mw);
    577                    if (DEBUG)
    578                    {
    579                        System.out.printf("Client *add* : %s %s %d (%s %d) %d [%s]\n", cClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
    580                    }
    581                    cPos++;
    582                }
    583                while (cPos < cLen);
    584            }
    585
    586            cMethods.clear();
    587            sMethods.clear();
    588
    589            for (MethodWrapper mw : allMethods)
    590            {
    591                if (DEBUG)
    592                {
    593                    System.out.println(mw);
    594                }
    595                cMethods.add(mw.node);
    596                sMethods.add(mw.node);
    597                if (mw.server && mw.client)
    598                {
    599                    // no op
    600                }
    601                else
    602                {
    603                    if (mw.node.visibleAnnotations == null) mw.node.visibleAnnotations = Lists.newArrayListWithExpectedSize(1);
    604                    mw.node.visibleAnnotations.add(getSideAnn(mw.client));
    605                    if (mw.client)
    606                    {
    607                        info.sMethods.add(mw.node);
    608                    }
    609                    else
    610                    {
    611                        info.cMethods.add(mw.node);
    612                    }
    613                }
    614            }
    615        }
    616
    617        public static byte[] getClassBytes(String name) throws IOException
    618        {
    619            InputStream classStream = null;
    620            try
    621            {
    622                classStream = MCPMerger.class.getResourceAsStream("/" + name.replace('.', '/').concat(".class"));
    623                return readFully(classStream);
    624            }
    625            finally
    626            {
    627                if (classStream != null)
    628                {
    629                    try
    630                    {
    631                        classStream.close();
    632                    }
    633                    catch (IOException e){}
    634                }
    635            }
    636        }
    637    }