- Joined
- Nov 16, 2010
- Messages
- 144
- Reaction score
- 72
I was hesitant to release this or not,but recently i have saw our development community are going up with alot of release to improve the odin server base, so i decided to contribute something to raise up our community too. And this is my contribution, I called it's the "Damage Check System".
*My English is not good, so my sentences may be terrible, plz dont blame me : *
This is the wall of text, and require a lot of knowledge about server and client, so don't read it if you don't know what i'm talking about.
First of all, i want to thank to Fraysa and everyone, who give me a hint about the "damage seed" and how it work on this thread:
[Discussion] How to detect 1 attack line is a critical or not : .
So, with the hint, i decided starting my work on v149 (everything example below is from v149). With the BMS, KMS leaked files and GMS v149 .idb, i have found the formula for calculating the random attack damage for player then tried to implement it in Odin (lithium base). Because of the character stats calculation on my server not good yet, so i only do the damage calculation on normal attack (melee_attack or close_ranged_attack) for testing first.
Each character will have it own random generators and each random generator will have it own seeds, so in MapleCharacter we will need create a new object to hold these random generators. In my case, i create a new object called CalcDamage to hold these Random Generators.
MapleCharacter.java
Here is CalcDamage class and the setSeed method
CalcDamage.java
Like i said above "each random generator will have it own seeds". So we need set seeds for each random generator.
Every time the Random() method is called, it will use the current seeds to make a new seeds and XOR these new seeds to generate a random number.
Here is CRand32.java
Ok! Now we already have a random method for generate a random number like the client side, so what does we have to do with it to check damage done from player?
First, I will explain how the client and server work for checking damage done from player.
Client Side: when player using physical attack skill or press Attack button (normal attack/melee attack), most of them will trace over function CalcDamage:Damage(); ....and CalcDamage::MDamage() for magic attack skill,...etc..
These method will calculate all damage lines for that attack/skill. For example: when a Dual Blade player press attack, they will do 2 line damage, that mean client trace over CalcDamage:Damage() and calculate damage for 2 line damage. After that, client will write attack packet and send it to server, i call it's melee_attack or close_ranged_attack packet
Server Side: We need to create a method called PDamage() for CalcDamage.java on server side. After received meele_attack packet, we handle it as normal, but we will call chr.getCalcDamage().PDamage() to calculate damage for each attack line (same as client side). Compare damage sent from client with damage calculated from server, if it doesn't match, the user is obviously hacking. :
Ok, now let talk about CalcDamage:Damage():
Basically, when you do damage to monster, your damage will be done random in your (minDamage ~ maxDamage) range (the range which you can see in Character Stat Windows in game), and it will be adjusted by many things before display on, i'm just handled some of it. There are some basic things that will be adjusted your damage done like: Monster's Physical Defense Rate, Skill's Damage Rate, Critical Rate, Defense Ignore Rate,...etc. But it is not a big problem if you already have a good system to handle/calculate your character's stats on your server as well. Here is what i have done so far:
CalcDamage:Damage()
How the CalcDamage:Damage() work?
- In CalcDamage:Damage() method, Nexon generate 11 random numbers (11 in GMS v149; 7 in KMS Leak file) using CRand32::Random() method. These numbers will be store in a array:
.
They will use it like a random seed for RandomInRange(randomNum, maxValue, minValue) method.
11 random numbers will be used rotational to prevent same number which generated from RandomInRange method (not sure, but I think so : ). We will need a variable "index"=0, this is the index of current element in array, every time the current element number is used, the index++.
To use rotational the element of the array, we can get the element by this way:
element = array[index % array_length]
For example:
if index=9 -> 9%11=9; // array[index] ~ array[9]
if index=11 -> 11%11=0 // array[index] ~ array[0]
if index=15 -> 15%11=4 // array[index] ~ array[4]
What is the RandomInRange(randomNum, maxValue, minValue) method?
Simply, it's generate a random number in the given range like Randomizer.rand(minValue, maxValue) in java. For example: player have the damage range in the stat windows = (1024 ~ 2048), the RandomInRange will pick a random number in that range: RandomInRange(rndNum, 2048, 1024). The "rndNum" is a result of Crand32::Random(), RandomInRange will use it for random algorithm like a randomSeed.
*I have found this method when using IDA and figure out the algorithm of this method by debugging the client with ollyDbg. This method is the most importal part to calculate damage, because of this will help you to generate a true random number in the given range*
Here is the RandomInRange's algorithm of v149 client:
And here is the result:
Ok, That is all what I have done till now. : there are alot of things to do for complete system. Remember that all of this system can be work correct or not depend on how you handle your character's stats on your server and the version you are working on, so good luck with it.
If you have any suggetion/contribution for me, plz comment : . I hope some day, this system will be implemented completely on every MS source. Finally, Thanks for reading. :
*My English is not good, so my sentences may be terrible, plz dont blame me : *
This is the wall of text, and require a lot of knowledge about server and client, so don't read it if you don't know what i'm talking about.
First of all, i want to thank to Fraysa and everyone, who give me a hint about the "damage seed" and how it work on this thread:
[Discussion] How to detect 1 attack line is a critical or not : .
So, with the hint, i decided starting my work on v149 (everything example below is from v149). With the BMS, KMS leaked files and GMS v149 .idb, i have found the formula for calculating the random attack damage for player then tried to implement it in Odin (lithium base). Because of the character stats calculation on my server not good yet, so i only do the damage calculation on normal attack (melee_attack or close_ranged_attack) for testing first.
Like Fraysa said, we need to generates 3 random unsigned integers send it to client and set it from server first. The seeds are sent in the SetField packet or Warp_To_Map packet:When you log in to the game, the server generates 3 random unsigned integers that are called "damage seeds". These seeds are used by both the client and the server to calculate the randomized damage the player attacks. The client will use the same random numbers as well. When a player initiates the attack, the client generates a random number by calculating his attack (or magic attack) using the random generated seeds, and sends the damage to the server.
PHP:
MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
mplew.writeShort(SendPacketOpcode.WARP_TO_MAP.getValue());
mplew.writeShort(2);
mplew.writeInt(1);
mplew.writeInt(0);
mplew.writeInt(2);
mplew.writeInt(0);
mplew.writeInt(chr.getClient().getChannel() - 1);
mplew.write(0);
mplew.writeInt(0);
mplew.write(1);
mplew.writeInt(0);
mplew.write(1);// true/false
mplew.writeShort(0);//loop size
//Random Seed
int seed1 = Randomizer.nextInt();
int seed2 = Randomizer.nextInt();
int seed3 = Randomizer.nextInt();
chr.getCalcDamage().SetSeed(seed1, seed2, seed3);
mplew.writeInt(seed1);
mplew.writeInt(seed2);
mplew.writeInt(seed3);
//End random seed
PacketHelper.addCharacterInfo(mplew, chr);
PacketHelper.addLuckyLogoutInfo(mplew, chr.getId());
mplew.write(0);
mplew.writeLong(PacketHelper.getTime(System.currentTimeMillis()));
mplew.writeInt(100);
mplew.write(0);
mplew.write(0);
mplew.write(1);
return mplew.getPacket();
MapleCharacter.java
PHP:
public class MapleCharacter {
...
...
private transient CalcDamage calcDamage;
public static MapleCharacter ReconstructChr(....) {
...
...
ret.calcDamage = new CalcDamage();
...
...
}
public static MapleCharacter loadCharFromDB(....) {
...
ret.calcDamage = new CalcDamage();
...
}
public CalcDamage getCalcDamage() {
return calcDamage;
}
}
Here is CalcDamage class and the setSeed method
CalcDamage.java
PHP:
public class CalcDamage {
CRand32 rndGenForCharacter;
//CRand32 rndForCheckDamageMiss;//not implement yet
//CRand32 rndGenForMob;//not implement yet
//int invalidCount;
public CalcDamage() {
rndGenForCharacter = new CRand32();
invalidCount = 0;
}
public void SetSeed(int seed1, int seed2, int seed3) {
rndGenForCharacter.Seed(seed1, seed2, seed3);
//rndForCheckDamageMiss.Seed(seed1, seed2, seed3);//not implement yet
//rndGenForMob.Seed(seed1, seed2, seed3);//not implement yet
}
}
Like i said above "each random generator will have it own seeds". So we need set seeds for each random generator.
Every time the Random() method is called, it will use the current seeds to make a new seeds and XOR these new seeds to generate a random number.
Here is CRand32.java
PHP:
public class CRand32 {
private long seed1;
private long seed2;
private long seed3;
private long oldSeed1;
private long oldSeed2;
private long oldSeed3;
public CRand32() {//constructor
int randInt = Randomizer.nextInt();//idk, just make a random number to use as default seeds
Seed(randInt, randInt, randInt);
}
public long Random() {
long seed1 = this.seed1;
long seed2 = this.seed2;
long seed3 = this.seed3;
this.oldSeed1 = seed1;
this.oldSeed2 = seed2;
this.oldSeed3 = seed3;
long newSeed1 = (seed1 << 12) ^ (seed1 >> 19) ^ ((seed1 >> 6) ^ (seed1 << 12)) & 0x1FFF;
long newSeed2 = 16 * seed2 ^ (seed2 >> 25) ^ ((16 * seed2) ^ (seed2 >> 23)) & 0x7F;
long newSeed3 = (seed3 >> 11) ^ (seed3 << 17) ^ ((seed3 >> 8) ^ (seed3 << 17)) & 0x1FFFFF;
this.seed1 = newSeed1;
this.seed2 = newSeed2;
this.seed3 = newSeed3;
return (newSeed1 ^ newSeed2 ^ newSeed3) & 0xffffffffL;//& 0xffffffffL will help you convert long to unsigned int
}
public void Seed(int s1, int s2, int s3) {
this.seed1 = s1 | 0x100000;
this.oldSeed1 = s1 | 0x100000;
this.seed2 = s2 | 0x1000;
this.oldSeed2 = s2 | 0x1000;
this.seed3 = s3 | 0x10;
this.oldSeed3 = s3 | 0x10;
}
}
First, I will explain how the client and server work for checking damage done from player.
Client Side: when player using physical attack skill or press Attack button (normal attack/melee attack), most of them will trace over function CalcDamage:Damage(); ....and CalcDamage::MDamage() for magic attack skill,...etc..
These method will calculate all damage lines for that attack/skill. For example: when a Dual Blade player press attack, they will do 2 line damage, that mean client trace over CalcDamage:Damage() and calculate damage for 2 line damage. After that, client will write attack packet and send it to server, i call it's melee_attack or close_ranged_attack packet
Server Side: We need to create a method called PDamage() for CalcDamage.java on server side. After received meele_attack packet, we handle it as normal, but we will call chr.getCalcDamage().PDamage() to calculate damage for each attack line (same as client side). Compare damage sent from client with damage calculated from server, if it doesn't match, the user is obviously hacking. :
Ok, now let talk about CalcDamage:Damage():
Basically, when you do damage to monster, your damage will be done random in your (minDamage ~ maxDamage) range (the range which you can see in Character Stat Windows in game), and it will be adjusted by many things before display on, i'm just handled some of it. There are some basic things that will be adjusted your damage done like: Monster's Physical Defense Rate, Skill's Damage Rate, Critical Rate, Defense Ignore Rate,...etc. But it is not a big problem if you already have a good system to handle/calculate your character's stats on your server as well. Here is what i have done so far:
CalcDamage:Damage()
PHP:
private int numRand = 11;//A number of random number for calculate damage
public List<Pair<Integer, Boolean>> PDamage(MapleCharacter chr, AttackInfo attack) {
List<Pair<Integer, Boolean>> realDamageList = new ArrayList<>();
for (AttackPair eachMob : attack.allDamage) {//For each monster
MapleMonster monster = chr.getMap().getMonsterByOid(eachMob.objectid);
//we need save it as long type to store unsigned int
long rand[] = new long[numRand];
for (int i = 0; i < numRand; i++) {
rand[i] = rndGenForCharacter.Random();
}
byte index = 0;
for (Pair<Integer, Boolean> att : eachMob.attack) {//For each attack
double realDamage = 0.0;
boolean critical = false;
index++;
long unkRand1 = rand[index++ % numRand];
//Adjusted Random Damage
int maxDamage = (int) chr.getStat().getCurrentMaxBaseDamage();
int minDamage = (int) chr.getStat().getCurrentMinBaseDamage();
double adjustedRandomDamage = RandomInRange(rand[index++ % numRand], maxDamage, minDamage);
realDamage += adjustedRandomDamage;
//Adjusted Damage By Monster's Physical Defense Rate
double monsterPDRate = monster.getStats().getPDRate();
double percentDmgAfterPDRate = Math.max(0.0, 100.0 - monsterPDRate);
realDamage = percentDmgAfterPDRate / 100.0 * realDamage;
//Adjusted Damage By Skill
MapleStatEffect skillEffect = null;
if (attack.skill > 0) {
skillEffect = SkillFactory.getSkill(attack.skill).getEffect(chr.getTotalSkillLevel(attack.skill));
}
if (skillEffect != null) {
realDamage = realDamage * (double) skillEffect.getDamage() / 100.0;
}
//Adjusted Critical Damage
if (RandomInRange(rand[index++ % numRand], 100, 0) < chr.getStat().getCritRate()) {
critical = true;
int maxCritDamage = chr.getStat().getMaxCritDamage();
int minCritDamage = chr.getStat().getMinCritDamage();
int criticalDamageRate = (int) RandomInRange(rand[index++ % numRand], maxCritDamage, minCritDamage);
//nexon convert realDamage to int when multiply with criticalDamageRate
realDamage = realDamage + (criticalDamageRate / 100.0 * (int) realDamage);
}
realDamageList.add(new Pair<>((int) realDamage, critical));
}
}
return realDamageList;
}
How the CalcDamage:Damage() work?
- In CalcDamage:Damage() method, Nexon generate 11 random numbers (11 in GMS v149; 7 in KMS Leak file) using CRand32::Random() method. These numbers will be store in a array:
PHP:
int index = 0;
long randomArray[] = new long[11];
for (int i = 0; i < 11; i++) {
randomArray[i] = CRand32::Random();
}
...
randomNum = randomArray[index % 11];
They will use it like a random seed for RandomInRange(randomNum, maxValue, minValue) method.
11 random numbers will be used rotational to prevent same number which generated from RandomInRange method (not sure, but I think so : ). We will need a variable "index"=0, this is the index of current element in array, every time the current element number is used, the index++.
To use rotational the element of the array, we can get the element by this way:
element = array[index % array_length]
For example:
if index=9 -> 9%11=9; // array[index] ~ array[9]
if index=11 -> 11%11=0 // array[index] ~ array[0]
if index=15 -> 15%11=4 // array[index] ~ array[4]
What is the RandomInRange(randomNum, maxValue, minValue) method?
Simply, it's generate a random number in the given range like Randomizer.rand(minValue, maxValue) in java. For example: player have the damage range in the stat windows = (1024 ~ 2048), the RandomInRange will pick a random number in that range: RandomInRange(rndNum, 2048, 1024). The "rndNum" is a result of Crand32::Random(), RandomInRange will use it for random algorithm like a randomSeed.
*I have found this method when using IDA and figure out the algorithm of this method by debugging the client with ollyDbg. This method is the most importal part to calculate damage, because of this will help you to generate a true random number in the given range*
Here is the RandomInRange's algorithm of v149 client:
PHP:
public double RandomInRange(long randomNum, int max, int min) {
//java not have unsigned long, so i used BigInteger
BigInteger ECX = new BigInteger("" + randomNum);//random number from Crand32::Random()
BigInteger EAX = new BigInteger("1801439851");//0x6B5FCA6B; <= this is const
//ECX * EAX = EDX:EAX (64bit register)
BigInteger multipled = ECX.multiply(EAX);
//get EDX from EDX:EAX
long highBit = multipled.shiftRight(32).longValue();//get 32bit high
long rightShift = highBit >>> 22;//SHR EDX,16
double newRandNum = randomNum - (rightShift * 10000000.0);
double value;
if (min != max) {
if (min > max) {//swap
int temp = max;
max = min;
min = temp;
}
value = (max - min) * newRandNum / 9999999.0 + min;
} else {
value = max;
}
return value;
}
And here is the result:
Ok, That is all what I have done till now. : there are alot of things to do for complete system. Remember that all of this system can be work correct or not depend on how you handle your character's stats on your server and the version you are working on, so good luck with it.
If you have any suggetion/contribution for me, plz comment : . I hope some day, this system will be implemented completely on every MS source. Finally, Thanks for reading. :
Attachments
You must be registered for see attachments list
Last edited: