Author | Message | Time |
---|---|---|
iago | I was eventually going to release this with a Java-based binary bot, but I'll post this early: [code]/* * CheckRevision.java * * Created on March 10, 2004, 9:05 AM */ package bot.bnet.util; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.MappedByteBuffer; import java.nio.ByteOrder; import java.util.StringTokenizer; import timer.Timer; /** * * @author iago */ public class CheckRevision { private static final int hashcodes[] = { 0xE7F4CB62, 0xF6A14FFC, 0xAA5504AF, 0x871FCDC2, 0x11BF6A18, 0xC57292E6, 0x7927D27E, 0x2FEC8733 }; /** This class simply does the version check on the three main files. */ public static int checkRevision(String versionString, String[] files, int mpqNum) throws FileNotFoundException, IOException { // First, parse the versionString to name=value pairs and put them // in the appropriate place int[] values = new int[4]; int[] opValueDest = new int[4]; int[] opValueSrc1 = new int[4]; char[] operation = new char[4]; int[] opValueSrc2 = new int[4]; // Break this apart at the spaces StringTokenizer s = new StringTokenizer(versionString, " "); int currentFormula = 0; while(s.hasMoreTokens()) { String thisToken = s.nextToken(); // As long as there is an '=' in the string if(thisToken.indexOf('=') > 0) { // Break it apart at the '=' StringTokenizer nameValue = new StringTokenizer(thisToken, "="); if(nameValue.countTokens() != 2) return 0; int variable = getNum(nameValue.nextToken().charAt(0)); String value = nameValue.nextToken(); //System.out.println((int)variable + " = " + value); // If it starts with a number, assign that number to the appropriate variable if(Character.isDigit(value.charAt(0))) { values[variable] = Integer.parseInt(value); } else { opValueDest[currentFormula] = variable; opValueSrc1[currentFormula] = getNum(value.charAt(0)); operation[currentFormula] = value.charAt(1); opValueSrc2[currentFormula] = getNum(value.charAt(2)); currentFormula++; } } } // Now we actually do the hashing for each file // Start by hashing A by the hashcode values[0] ^= hashcodes[mpqNum]; for(int i = 0; i < files.length; i++) { Timer thisFile = new Timer(); File currentFile = new File(files[i]); int roundedSize = (int)((currentFile.length() / 1024) * 1024); // Load the file into memory FileInputStream input = new FileInputStream(new File(files[i])); MappedByteBuffer file = input.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, roundedSize); file.order(ByteOrder.LITTLE_ENDIAN); file.load(); //System.out.println("Processing " + currentFile.toString() + " (" + (roundedSize/4) + " DWords)"); //System.out.print("Processing " + currentFile.toString() + "........"); for(int j = 0; j < roundedSize; j += 4) { values[3] = file.getInt(j); //values[3] = (file.get() & 0xFF) | // ((file.get() & 0xFF) << 8) | // ((file.get() & 0xFF) << 16) | // ((file.get() & 0xFF) << 24); for(int k = 0; k < currentFormula; k++) { switch(operation[k]) { case '+': values[opValueDest[k]] = values[opValueSrc1[k]] + values[opValueSrc2[k]]; break; case '-': values[opValueDest[k]] = values[opValueSrc1[k]] - values[opValueSrc2[k]]; break; case '^': values[opValueDest[k]] = values[opValueSrc1[k]] ^ values[opValueSrc2[k]]; break; default: return 0; } } } //System.out.println(thisFile.getTime() + "ms."); } //System.out.println(Integer.toHexString(values[0])); //System.out.println(Integer.toHexString(values[1])); //System.out.println(Integer.toHexString(values[2])); return values[2]; } private static int getNum(char c) { c = Character.toUpperCase(c); if(c == 'S') return 3; else return c - 'A'; } public static void main(String args[]) throws Exception { String[] files = { "/home/iago/Projects/Java/bot/starcraft.exe", "/home/iago/Projects/Java/bot/storm.dll", "/home/iago/Projects/Java/bot/battle.snp" } ; long totalTime = 0; final int MAX = 100; long min = -1; long max = -1; for(int i = 0; i < MAX; i++) { int val1 = (int) (Math.random() * (double)Integer.MAX_VALUE); int val2 = (int) (Math.random() * (double)Integer.MAX_VALUE); int val3 = (int) (Math.random() * (double)Integer.MAX_VALUE); String testString = "A=" + val1 + " B=" + val2 + " C=" + val3 + " 4 A=A^S B=B-C C=C+A A=A+B"; Timer checkRevision = new Timer(); checkRevision(testString, files, 6); long elapsed = checkRevision.getTime(); System.out.println("checkRevision took " + elapsed + "ms."); totalTime += elapsed; if(elapsed < min || min == -1) min = elapsed; if(elapsed > max || max == -1) max = elapsed; } System.out.println("For " + MAX + "runs:"); System.out.println("Total time: " + totalTime + "ms"); System.out.println("Average time: " + (totalTime / MAX) + "ms"); System.out.println("Min time: " + min + "ms"); System.out.println("Max time: " + max + "ms"); //System.out.println(Integer.toHexString(a)); } } [/code] | March 15, 2004, 5:49 PM |
iago | Note: Yes, I hardcoded in the value "4", but I was going to go back and change it :) | March 15, 2004, 5:54 PM |
snowstorm | Hey umm where can I get the timer.Timer package? When I compile it's telling me it doesn't exist. by the way, thanks for the code | April 11, 2005, 3:51 AM |
Lenny | The timer package isn't necessary unless you want to benchmark... Has anyone timed checkrevision implementations in different languages yet? I would like to see how this would perform against VB/C++/.NET implementation. I know it won't be accurate as far as a language comparison goes since the code wouldn't be the same. But just curious as how well a VB/C++/.NET/Java programmer would do as far as code efficiency goes factoring in language. ;D | April 11, 2005, 5:05 AM |
Kp | [quote author=Lenny link=topic=5788.msg108138#msg108138 date=1113195953]Has anyone timed checkrevision implementations in different languages yet?[/quote] There've been some informal tests, but I don't know if anyone's run all the different languages on one host. IIRC, one of Sky's C++ implementations is down around 10ms for Brood War (or maybe D2, I never asked). I'll time mine sometime, but I expect it to be similar since we're running very similar functions (just that mine is a Linux version whereas his is Windows). As a pure guess: I'd expect that C++(jit version) will beat out everything, followed by C++(non-jit) and Java (order uncertain, though Java's reliance on bytecode will probably bring it in third), then .NET languages, then VB. As Myndfyre so often reminds us, .NET only needs to be JIT'd once, but it's still going to incur the nasty looping constructs that the non-jit C++ version will use. | April 11, 2005, 2:00 PM |
Myndfyr | [quote author=Kp link=topic=5788.msg108163#msg108163 date=1113228056] As Myndfyre so often reminds us, .NET only needs to be JIT'd once [/quote] Hahaha, I only mention it when someone is knocking .NET. :P As a matter of fact, I'm considering writing a JIT version of this in C#. It'll have to run along a different path, though (dynamic compilation or reflection emit), so we'll see how well that all works out. It *may* work out quite well considering that an emitted assembly only needs to be JITted once and can then be cached for the duration of the string coming from Battle.net. :P *goes to reboot to start up VS* | April 11, 2005, 5:41 PM |
shout | My C# CheckRevision (based off this) gets ~80 ms. | April 12, 2005, 3:13 AM |
iago | FINE you all convinced me!! Here's my current code which is about twice as fast as the old code: [code]/* * CheckRevision.java * * Created on March 10, 2004, 9:05 AM */ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteOrder; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.util.Hashtable; import java.util.StringTokenizer; /** This takes care of the CheckRevision() for the main game files of any program. * This is done to prevent tampering and to make sure the version is correct. * <P> * This function is generally slow because it has to read through the entire * files. The majority of the time is spent in i/o, but I've tried to optimize * this as much as possible. * @author iago */ public class CheckRevision { /** These are the hashcodes for the various .mpq files. */ private static final int hashcodes[] = { 0xE7F4CB62, 0xF6A14FFC, 0xAA5504AF, 0x871FCDC2, 0x11BF6A18, 0xC57292E6, 0x7927D27E, 0x2FEC8733 }; /** Stores some past results */ private static Hashtable crCache = new Hashtable(); private static int crCacheHits = 0; private static int crCacheMisses = 0; /** Does the actual version check. * @param versionString The version string. This is recieved from Battle.net in 0x50 (SID_AUTH_INFO) and * looks something like "A=5 B=10 C=15 4 A=A+S B=B-A A=S+B C=C-B". * @param files The array of files we're checking. Generally the main game files, like * Starcraft.exe, Storm.dll, and Battle.snp. * @param mpqNum The number of the mpq file, from 1..7. * @throws FileNotFoundException If the datafiles aren't found. * @throws IOException If there is an error reading from one of the datafiles. * @return The 32-bit CheckRevision hash. */ public static int checkRevision(String versionString, String[] files, int mpqNum) throws FileNotFoundException, IOException { Integer cacheHit = (Integer) crCache.get(versionString + mpqNum + files[0]); if(cacheHit != null) { crCacheHits++; return cacheHit.intValue(); } crCacheMisses++; // Break this apart at the spaces StringTokenizer tok = new StringTokenizer(versionString, " "); // Get the values for a, b, and c int a = Integer.parseInt(tok.nextToken().substring(2)); int b = Integer.parseInt(tok.nextToken().substring(2)); int c = Integer.parseInt(tok.nextToken().substring(2)); tok.nextToken(); String formula; formula = tok.nextToken(); if(formula.matches("A=A.S") == false) return checkRevisionSlow(versionString, files, mpqNum); char op1 = formula.charAt(3); formula = tok.nextToken(); if(formula.matches("B=B.C") == false) return checkRevisionSlow(versionString, files, mpqNum); char op2 = formula.charAt(3); formula = tok.nextToken(); if(formula.matches("C=C.A") == false) return checkRevisionSlow(versionString, files, mpqNum); char op3 = formula.charAt(3); formula = tok.nextToken(); if(formula.matches("A=A.B") == false) return checkRevisionSlow(versionString, files, mpqNum); char op4 = formula.charAt(3); // Now we actually do the hashing for each file // Start by hashing A by the hashcode a ^= hashcodes[mpqNum]; for(int i = 0; i < files.length; i++) { File currentFile = new File(files[i]); int roundedSize = (int)((currentFile.length() / 1024) * 1024); MappedByteBuffer fileData = new FileInputStream(currentFile).getChannel().map(FileChannel.MapMode.READ_ONLY, 0, roundedSize); fileData.order(ByteOrder.LITTLE_ENDIAN); for(int j = 0; j < roundedSize; j += 4) { int s = fileData.getInt(j); // A=1054538081 B=741521288 C=797042342 4 A=A^S B=B-C C=C^A A=A+B switch(op1) { case '^': a = a ^ s; break; case '-': a = a - s; break; case '+': a = a + s; break; } switch(op2) { case '^': b = b ^ c; break; case '-': b = b - c; break; case '+': b = b + c; break; } switch(op3) { case '^': c = c ^ a; break; case '-': c = c - a; break; case '+': c = c + a; break; } switch(op4) { case '^': a = a ^ b; break; case '-': a = a - b; break; case '+': a = a + b; break; } } } crCache.put(versionString + mpqNum + files[0], new Integer(c)); return c; } /** This is an alternate implementation of CheckRevision. It it slower (about 2.2 times slower), but it can handle * weird version strings that Battle.net would never send. Battle.net's version strings are _always_ in the form: * A=x B=y C=z 4 A=A?S B=B?C C=C?A A=A?B: * * A=1054538081 B=741521288 C=797042342 4 A=A^S B=B-C C=C^A A=A+B * * If, for some reason, the string in checkRevision() doesn't match up, this will run. * * @param versionString The version string. This is recieved from Battle.net in 0x50 (SID_AUTH_INFO) and * looks something like "A=5 B=10 C=15 4 A=A+S B=B-A A=S+B C=C-B". * @param files The array of files we're checking. Generally the main game files, like * Starcraft.exe, Storm.dll, and Battle.snp. * @param mpqNum The number of the mpq file, from 1..7. * @throws FileNotFoundException If the datafiles aren't found. * @throws IOException If there is an error reading from one of the datafiles. * @return The 32-bit CheckRevision hash. */ private static int checkRevisionSlow(String versionString, String[] files, int mpqNum) throws FileNotFoundException, IOException { System.out.println("Warning: using checkRevisionSlow for version string: " + versionString); Integer cacheHit = (Integer) crCache.get(versionString + mpqNum + files[0]); if(cacheHit != null) { crCacheHits++; System.out.println("CheckRevision cache hit"); System.out.println(" --> " + crCacheHits + " hits, " + crCacheMisses + " misses."); return cacheHit.intValue(); } crCacheMisses++; System.out.println("CheckRevision cache miss"); System.out.println("--> " + crCacheHits + " hits, " + crCacheMisses + " misses."); // First, parse the versionString to name=value pairs and put them // in the appropriate place int[] values = new int[4]; int[] opValueDest = new int[4]; int[] opValueSrc1 = new int[4]; char[] operation = new char[4]; int[] opValueSrc2 = new int[4]; // Break this apart at the spaces StringTokenizer s = new StringTokenizer(versionString, " "); int currentFormula = 0; while(s.hasMoreTokens()) { String thisToken = s.nextToken(); // As long as there is an '=' in the string if(thisToken.indexOf('=') > 0) { // Break it apart at the '=' StringTokenizer nameValue = new StringTokenizer(thisToken, "="); if(nameValue.countTokens() != 2) return 0; int variable = getNum(nameValue.nextToken().charAt(0)); String value = nameValue.nextToken(); // If it starts with a number, assign that number to the appropriate variable if(Character.isDigit(value.charAt(0))) { values[variable] = Integer.parseInt(value); } else { opValueDest[currentFormula] = variable; opValueSrc1[currentFormula] = getNum(value.charAt(0)); operation[currentFormula] = value.charAt(1); opValueSrc2[currentFormula] = getNum(value.charAt(2)); currentFormula++; } } } // Now we actually do the hashing for each file // Start by hashing A by the hashcode values[0] ^= hashcodes[mpqNum]; for(int i = 0; i < files.length; i++) { File currentFile = new File(files[i]); int roundedSize = (int)((currentFile.length() / 1024) * 1024); MappedByteBuffer fileData = new FileInputStream(currentFile).getChannel().map(FileChannel.MapMode.READ_ONLY, 0, roundedSize); fileData.order(ByteOrder.LITTLE_ENDIAN); for(int j = 0; j < roundedSize; j += 4) { values[3] = fileData.getInt(j); for(int k = 0; k < currentFormula; k++) { switch(operation[k]) { case '+': values[opValueDest[k]] = values[opValueSrc1[k]] + values[opValueSrc2[k]]; break; case '-': values[opValueDest[k]] = values[opValueSrc1[k]] - values[opValueSrc2[k]]; break; case '^': values[opValueDest[k]] = values[opValueSrc1[k]] ^ values[opValueSrc2[k]]; } } } } crCache.put(versionString + mpqNum + files[0], new Integer(values[2])); return values[2]; } /** Converts the parameter to which number in the array it is, based on A=0, B=1, C=2, S=3. * @param c The character letter. * @return The array number this is found at. */ private static int getNum(char c) { c = Character.toUpperCase(c); if(c == 'S') return 3; return c - 'A'; } } [/code] | April 13, 2005, 2:56 AM |