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("bct")) //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 }