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 boolean toClient = line.charAt(0) == '<';
138 line = line.substring(1);
139 if (toClient) copyToClient.add(line);
140 else copyToServer.add(line);
141 }
142
143 in.close();
144 return true;
145 }
146 catch (Exception e)
147 {
148 System.err.println("Error: " + e.getMessage());
149 return false;
150 }
151 }
152
153 public static void processJar(File clientInFile, File serverInFile, File clientOutFile, File serverOutFile) throws IOException
154 {
155 ZipFile cInJar = null;
156 ZipFile sInJar = null;
157 ZipOutputStream cOutJar = null;
158 ZipOutputStream sOutJar = null;
159
160 try
161 {
162 try
163 {
164 cInJar = new ZipFile(clientInFile);
165 sInJar = new ZipFile(serverInFile);
166 }
167 catch (FileNotFoundException e)
168 {
169 throw new FileNotFoundException("Could not open input file: " + e.getMessage());
170 }
171 try
172 {
173 cOutJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(clientOutFile)));
174 sOutJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(serverOutFile)));
175 }
176 catch (FileNotFoundException e)
177 {
178 throw new FileNotFoundException("Could not open output file: " + e.getMessage());
179 }
180 Hashtable<String, ZipEntry> cClasses = getClassEntries(cInJar, cOutJar);
181 Hashtable<String, ZipEntry> sClasses = getClassEntries(sInJar, sOutJar);
182 HashSet<String> cAdded = new HashSet<String>();
183 HashSet<String> sAdded = new HashSet<String>();
184
185 for (Entry<String, ZipEntry> entry : cClasses.entrySet())
186 {
187 String name = entry.getKey();
188 ZipEntry cEntry = entry.getValue();
189 ZipEntry sEntry = sClasses.get(name);
190
191 if (sEntry == null)
192 {
193 if (!copyToServer.contains(name))
194 {
195 copyClass(cInJar, cEntry, cOutJar, null, true);
196 cAdded.add(name);
197 }
198 else
199 {
200 if (DEBUG)
201 {
202 System.out.println("Copy class c->s : " + name);
203 }
204 copyClass(cInJar, cEntry, cOutJar, sOutJar, true);
205 cAdded.add(name);
206 sAdded.add(name);
207 }
208 continue;
209 }
210
211 sClasses.remove(name);
212 ClassInfo info = new ClassInfo(name);
213 shared.put(name, info);
214
215 byte[] cData = readEntry(cInJar, entry.getValue());
216 byte[] sData = readEntry(sInJar, sEntry);
217 byte[] data = processClass(cData, sData, info);
218
219 ZipEntry newEntry = new ZipEntry(cEntry.getName());
220 cOutJar.putNextEntry(newEntry);
221 cOutJar.write(data);
222 sOutJar.putNextEntry(newEntry);
223 sOutJar.write(data);
224 cAdded.add(name);
225 sAdded.add(name);
226 }
227 for (Entry<String, ZipEntry> entry : sClasses.entrySet())
228 {
229 if (!copyToClient.contains(entry.getKey()))
230 {
231 copyClass(sInJar, entry.getValue(), null, sOutJar, false);
232 }
233 else
234 {
235 if (DEBUG)
236 {
237 System.out.println("Copy class s->c : " + entry.getKey());
238 }
239 copyClass(sInJar, entry.getValue(), cOutJar, sOutJar, false);
240 }
241 }
242
243 for (String name : new String[]{SideOnly.class.getName(), Side.class.getName()})
244 {
245 String eName = name.replace(".", "/");
246 byte[] data = getClassBytes(name);
247 ZipEntry newEntry = new ZipEntry(name.replace(".", "/").concat(".class"));
248 if (!cAdded.contains(eName))
249 {
250 cOutJar.putNextEntry(newEntry);
251 cOutJar.write(data);
252 }
253 if (!sAdded.contains(eName))
254 {
255 sOutJar.putNextEntry(newEntry);
256 sOutJar.write(data);
257 }
258 }
259
260 }
261 finally
262 {
263 if (cInJar != null)
264 {
265 try { cInJar.close(); } catch (IOException e){}
266 }
267
268 if (sInJar != null)
269 {
270 try { sInJar.close(); } catch (IOException e) {}
271 }
272 if (cOutJar != null)
273 {
274 try { cOutJar.close(); } catch (IOException e){}
275 }
276
277 if (sOutJar != null)
278 {
279 try { sOutJar.close(); } catch (IOException e) {}
280 }
281 }
282 }
283
284 private static void copyClass(ZipFile inJar, ZipEntry entry, ZipOutputStream outJar, ZipOutputStream outJar2, boolean isClientOnly) throws IOException
285 {
286 ClassReader reader = new ClassReader(readEntry(inJar, entry));
287 ClassNode classNode = new ClassNode();
288
289 reader.accept(classNode, 0);
290
291 if (!classNode.name.equals("bcs")) //Special case CodecMus so I dont have to make a new patch, anyone who uses this in production code is.. bad.
292 {
293 if (classNode.visibleAnnotations == null) classNode.visibleAnnotations = new ArrayList<AnnotationNode>();
294 classNode.visibleAnnotations.add(getSideAnn(isClientOnly));
295 }
296
297 ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
298 classNode.accept(writer);
299 byte[] data = writer.toByteArray();
300
301 ZipEntry newEntry = new ZipEntry(entry.getName());
302 if (outJar != null)
303 {
304 outJar.putNextEntry(newEntry);
305 outJar.write(data);
306 }
307 if (outJar2 != null)
308 {
309 outJar2.putNextEntry(newEntry);
310 outJar2.write(data);
311 }
312 }
313
314 private static AnnotationNode getSideAnn(boolean isClientOnly)
315 {
316 AnnotationNode ann = new AnnotationNode(Type.getDescriptor(SideOnly.class));
317 ann.values = new ArrayList<Object>();
318 ann.values.add("value");
319 ann.values.add(new String[]{ Type.getDescriptor(Side.class), (isClientOnly ? "CLIENT" : "SERVER")});
320 return ann;
321 }
322
323 @SuppressWarnings("unchecked")
324 private static Hashtable<String, ZipEntry> getClassEntries(ZipFile inFile, ZipOutputStream outFile) throws IOException
325 {
326 Hashtable<String, ZipEntry> ret = new Hashtable<String, ZipEntry>();
327 for (ZipEntry entry : Collections.list((Enumeration<ZipEntry>)inFile.entries()))
328 {
329 if (entry.isDirectory())
330 {
331 outFile.putNextEntry(entry);
332 continue;
333 }
334 String entryName = entry.getName();
335 if (!entryName.endsWith(".class") || entryName.startsWith("."))
336 {
337 ZipEntry newEntry = new ZipEntry(entry.getName());
338 outFile.putNextEntry(newEntry);
339 outFile.write(readEntry(inFile, entry));
340 }
341 else
342 {
343 ret.put(entryName.replace(".class", ""), entry);
344 }
345 }
346 return ret;
347 }
348 private static byte[] readEntry(ZipFile inFile, ZipEntry entry) throws IOException
349 {
350 return readFully(inFile.getInputStream(entry));
351 }
352 private static byte[] readFully(InputStream stream) throws IOException
353 {
354 byte[] data = new byte[4096];
355 ByteArrayOutputStream entryBuffer = new ByteArrayOutputStream();
356 int len;
357 do
358 {
359 len = stream.read(data);
360 if (len > 0)
361 {
362 entryBuffer.write(data, 0, len);
363 }
364 } while (len != -1);
365
366 return entryBuffer.toByteArray();
367 }
368 private static class ClassInfo
369 {
370 public String name;
371 public ArrayList<FieldNode> cField = new ArrayList<FieldNode>();
372 public ArrayList<FieldNode> sField = new ArrayList<FieldNode>();
373 public ArrayList<MethodNode> cMethods = new ArrayList<MethodNode>();
374 public ArrayList<MethodNode> sMethods = new ArrayList<MethodNode>();
375 public ClassInfo(String name){ this.name = name; }
376 public boolean isSame() { return (cField.size() == 0 && sField.size() == 0 && cMethods.size() == 0 && sMethods.size() == 0); }
377 }
378
379 public static byte[] processClass(byte[] cIn, byte[] sIn, ClassInfo info)
380 {
381 ClassNode cClassNode = getClassNode(cIn);
382 ClassNode sClassNode = getClassNode(sIn);
383
384 processFields(cClassNode, sClassNode, info);
385 processMethods(cClassNode, sClassNode, info);
386
387 ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
388 cClassNode.accept(writer);
389 return writer.toByteArray();
390 }
391
392 private static ClassNode getClassNode(byte[] data)
393 {
394 ClassReader reader = new ClassReader(data);
395 ClassNode classNode = new ClassNode();
396 reader.accept(classNode, 0);
397 return classNode;
398 }
399
400 @SuppressWarnings("unchecked")
401 private static void processFields(ClassNode cClass, ClassNode sClass, ClassInfo info)
402 {
403 List<FieldNode> cFields = cClass.fields;
404 List<FieldNode> sFields = sClass.fields;
405
406 int sI = 0;
407 for (int x = 0; x < cFields.size(); x++)
408 {
409 FieldNode cF = cFields.get(x);
410 if (sI < sFields.size())
411 {
412 if (!cF.name.equals(sFields.get(sI).name))
413 {
414 boolean serverHas = false;
415 for (int y = sI + 1; y < sFields.size(); y++)
416 {
417 if (cF.name.equals(sFields.get(y).name))
418 {
419 serverHas = true;
420 break;
421 }
422 }
423 if (serverHas)
424 {
425 boolean clientHas = false;
426 FieldNode sF = sFields.get(sI);
427 for (int y = x + 1; y < cFields.size(); y++)
428 {
429 if (sF.name.equals(cFields.get(y).name))
430 {
431 clientHas = true;
432 break;
433 }
434 }
435 if (!clientHas)
436 {
437 if (sF.visibleAnnotations == null) sF.visibleAnnotations = new ArrayList<AnnotationNode>();
438 sF.visibleAnnotations.add(getSideAnn(false));
439 cFields.add(x++, sF);
440 info.sField.add(sF);
441 }
442 }
443 else
444 {
445 if (cF.visibleAnnotations == null) cF.visibleAnnotations = new ArrayList<AnnotationNode>();
446 cF.visibleAnnotations.add(getSideAnn(true));
447 sFields.add(sI, cF);
448 info.cField.add(cF);
449 }
450 }
451 }
452 else
453 {
454 if (cF.visibleAnnotations == null) cF.visibleAnnotations = new ArrayList<AnnotationNode>();
455 cF.visibleAnnotations.add(getSideAnn(true));
456 sFields.add(sI, cF);
457 info.cField.add(cF);
458 }
459 sI++;
460 }
461 if (sFields.size() != cFields.size())
462 {
463 for (int x = cFields.size(); x < sFields.size(); x++)
464 {
465 FieldNode sF = sFields.get(x);
466 if (sF.visibleAnnotations == null) sF.visibleAnnotations = new ArrayList<AnnotationNode>();
467 sF.visibleAnnotations.add(getSideAnn(true));
468 cFields.add(x++, sF);
469 info.sField.add(sF);
470 }
471 }
472 }
473
474 private static class MethodWrapper
475 {
476 private MethodNode node;
477 public boolean client;
478 public boolean server;
479 public MethodWrapper(MethodNode node)
480 {
481 this.node = node;
482 }
483 @Override
484 public boolean equals(Object obj)
485 {
486 if (obj == null || !(obj instanceof MethodWrapper)) return false;
487 MethodWrapper mw = (MethodWrapper) obj;
488 boolean eq = Objects.equal(node.name, mw.node.name) && Objects.equal(node.desc, mw.node.desc);
489 if (eq)
490 {
491 mw.client = this.client | mw.client;
492 mw.server = this.server | mw.server;
493 this.client = this.client | mw.client;
494 this.server = this.server | mw.server;
495 if (DEBUG)
496 {
497 System.out.printf(" eq: %s %s\n", this, mw);
498 }
499 }
500 return eq;
501 }
502
503 @Override
504 public int hashCode()
505 {
506 return Objects.hashCode(node.name, node.desc);
507 }
508 @Override
509 public String toString()
510 {
511 return Objects.toStringHelper(this).add("name", node.name).add("desc",node.desc).add("server",server).add("client",client).toString();
512 }
513 }
514 @SuppressWarnings("unchecked")
515 private static void processMethods(ClassNode cClass, ClassNode sClass, ClassInfo info)
516 {
517 List<MethodNode> cMethods = (List<MethodNode>)cClass.methods;
518 List<MethodNode> sMethods = (List<MethodNode>)sClass.methods;
519 LinkedHashSet<MethodWrapper> allMethods = Sets.newLinkedHashSet();
520
521 int cPos = 0;
522 int sPos = 0;
523 int cLen = cMethods.size();
524 int sLen = sMethods.size();
525 String clientName = "";
526 String lastName = clientName;
527 String serverName = "";
528 while (cPos < cLen || sPos < sLen)
529 {
530 do
531 {
532 if (sPos>=sLen)
533 {
534 break;
535 }
536 MethodNode sM = sMethods.get(sPos);
537 serverName = sM.name;
538 if (!serverName.equals(lastName) && cPos != cLen)
539 {
540 if (DEBUG)
541 {
542 System.out.printf("Server -skip : %s %s %d (%s %d) %d [%s]\n", sClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
543 }
544 break;
545 }
546 MethodWrapper mw = new MethodWrapper(sM);
547 mw.server = true;
548 allMethods.add(mw);
549 if (DEBUG)
550 {
551 System.out.printf("Server *add* : %s %s %d (%s %d) %d [%s]\n", sClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
552 }
553 sPos++;
554 }
555 while (sPos < sLen);
556 do
557 {
558 if (cPos>=cLen)
559 {
560 break;
561 }
562 MethodNode cM = cMethods.get(cPos);
563 lastName = clientName;
564 clientName = cM.name;
565 if (!clientName.equals(lastName) && sPos != sLen)
566 {
567 if (DEBUG)
568 {
569 System.out.printf("Client -skip : %s %s %d (%s %d) %d [%s]\n", cClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
570 }
571 break;
572 }
573 MethodWrapper mw = new MethodWrapper(cM);
574 mw.client = true;
575 allMethods.add(mw);
576 if (DEBUG)
577 {
578 System.out.printf("Client *add* : %s %s %d (%s %d) %d [%s]\n", cClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
579 }
580 cPos++;
581 }
582 while (cPos < cLen);
583 }
584
585 cMethods.clear();
586 sMethods.clear();
587
588 for (MethodWrapper mw : allMethods)
589 {
590 if (DEBUG)
591 {
592 System.out.println(mw);
593 }
594 cMethods.add(mw.node);
595 sMethods.add(mw.node);
596 if (mw.server && mw.client)
597 {
598 // no op
599 }
600 else
601 {
602 if (mw.node.visibleAnnotations == null) mw.node.visibleAnnotations = Lists.newArrayListWithExpectedSize(1);
603 mw.node.visibleAnnotations.add(getSideAnn(mw.client));
604 if (mw.client)
605 {
606 info.sMethods.add(mw.node);
607 }
608 else
609 {
610 info.cMethods.add(mw.node);
611 }
612 }
613 }
614 }
615
616 public static byte[] getClassBytes(String name) throws IOException
617 {
618 InputStream classStream = null;
619 try
620 {
621 classStream = MCPMerger.class.getResourceAsStream("/" + name.replace('.', '/').concat(".class"));
622 return readFully(classStream);
623 }
624 finally
625 {
626 if (classStream != null)
627 {
628 try
629 {
630 classStream.close();
631 }
632 catch (IOException e){}
633 }
634 }
635 }
636 }