Valhalla Legends Forums Archive | Java Programming | Java CheckRevision code

AuthorMessageTime
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

Search