Welcome!

Join our community of MMO enthusiasts and game developers! By registering, you'll gain access to discussions on the latest developments in MMO server files and collaborate with like-minded individuals. Join us today and unlock the potential of MMO server development!

Join Today!

Implement Damage Calculation/Damage Check for Odin (Lithium base)

Junior Spellweaver
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 :p: *
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.

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.
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:
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();
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
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;
   }
}
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::pDamage(); ....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::pDamage() 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::pDamage():
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::pDamage()
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::pDamage() work?
- In CalcDamage::pDamage() 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 :p: ). 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:
JJx6t7k - Implement Damage Calculation/Damage Check for Odin (Lithium base) - RaGEZONE Forums

Ok, That is all what I have done till now. :p: 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:
Elite Diviner
Joined
Mar 24, 2015
Messages
426
Reaction score
416
I haven't read all of it since it's complicated stuff but good job!
 
Joined
Mar 14, 2010
Messages
5,363
Reaction score
1,343
In my files (and BMS) the seed is, but I guess it doesn't really matter since it works :p anhtanh95

Code:
private final int uSeed = 5;
...

public Rand32() {
        this.Seed(Randomizer.nextInt(), 1170746341 * uSeed - 755606699, 1170746341 * uSeed - 755606699);
    }
 
Junior Spellweaver
Joined
Nov 16, 2010
Messages
144
Reaction score
72
In my files (and BMS) the seed is, but I guess it doesn't really matter since it works :p anhtanh95

Code:
private final int uSeed = 5;
...

public Rand32() {
        this.Seed(Randomizer.nextInt(), 1170746341 * uSeed - 755606699, 1170746341 * uSeed - 755606699);
    }

this is just create a default seeds for for CRand32, but you always have a new seeds when send WARP_TO_MAP packet, so it doesn't matter :p
 
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
In my files (and BMS) the seed is, but I guess it doesn't really matter since it works :p anhtanh95

Code:
private final int uSeed = 5;
...

public Rand32() {
        this.Seed(Randomizer.nextInt(), 1170746341 * uSeed - 755606699, 1170746341 * uSeed - 755606699);
    }

Where did you get the seed of 5? The only dword used when seeding the RNG in the CRand32 constructor is the C++ RNG of time(0). Which, @OP, is your "Randomizer.nextInt()" function (if you were to do it the Nexon way, that is).

@OP Great work, though. I never used this for checking damage, I only did it for 3rd person criticals. Remember this though, I did not see you use the CalcDamage Skip method. You will have to Skip the RNG just like the client does in order to be 100% exact when displaying your crits. Anyway, I'm glad to see that more and more people are referring and using the BMS leak <3.

Oh, and, I'm not on my PC right now so I can't see.. but, what is the RandomInRange? I don't recall seeing that in any v55 or below client when I was coding crits (don't recall it in BMS either). Is this something new since you're doing GMS v149+, or did I never notice it?
 
Joined
Mar 14, 2010
Messages
5,363
Reaction score
1,343
Where did you get the seed of 5? The only dword used when seeding the RNG in the CRand32 constructor is the C++ RNG of time(0). Which, @OP, is your "Randomizer.nextInt()" function (if you were to do it the Nexon way, that is).
Code:
  CRand32 *v1; // esi@1
  int v2; // eax@1

  v1 = this;
  this->m_lock.baseclass_0._m_pTIB = 0;
  v2 = dword_6A80EC();
  CRand32::Seed(v1, 1170746341 * v2 - 755606699, 1170746341 * v2 - 755606699, 1170746341 * v2 - 755606699);
I made it up. It's random seed so it doesn't matter what it is.

Just like how in WvsShop it's

Code:
  CRand32 *v1; // ST0C_4@1
  unsigned int uSeed; // [sp+4h] [bp-10h]@1
  int v3; // [sp+10h] [bp-4h]@1

  v1 = this;
  ZCriticalSectionEx<4000>::ZCriticalSectionEx<4000>(&this->m_lock);
  v3 = 0;
  uSeed = dword_560824();
  CRand32::CrtRand(&uSeed);
  CRand32::CrtRand(&uSeed);
  CRand32::CrtRand(&uSeed);
  CRand32::Seed(v1, uSeed, uSeed, uSeed);

In the end, uSeed is most likely random anyway, so if I wanted to, I should change my 5 to a randomint, but in the end it's a project that's over so it's all good.


BMS revolution is beginning and it was very late.
 
Junior Spellweaver
Joined
Nov 16, 2010
Messages
144
Reaction score
72
@OP Great work, though. I never used this for checking damage, I only did it for 3rd person criticals. Remember this though, I did not see you use the CalcDamage Skip method. You will have to Skip the RNG just like the client does in order to be 100% exact when displaying your crits. Anyway, I'm glad to see that more and more people are referring and using the BMS leak <3.
Wow, i never notice that have CalcDamage Skip method, i will look into it when i have a free time. Thank for your mention

Oh, and, I'm not on my PC right now so I can't see.. but, what is the RandomInRange? I don't recall seeing that in any v55 or below client when I was coding crits (don't recall it in BMS either). Is this something new since you're doing GMS v149+, or did I never notice it?
I don't use so much BMS leaked file because these stuff is out of date and very different with my version that i'm working on. otherwise, i use KMS leak file and compare with v149 IDB, so i have found the RandomInRange method. it simply generate a random number in the given range like Randomizer.rand(minValue, maxValue). 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 like a randomSeed for it algorithm
 
Last edited:
Flow
Joined
Nov 9, 2012
Messages
608
Reaction score
164
Thread reverted.
My account has been hacked by someone, and the thread was deleted by him...:mad:
Special thanks to kevintjuh93 , who supported me to get back my account and reverted my thread :love:

It seems like a lot of people's account are getting hacked on ragezone lately.
 
Initiate Mage
Joined
Jun 15, 2013
Messages
31
Reaction score
0
Ok, the RandomInRange function is just absolutely ridiculously written. There are 2 issues:

This huge complicated procedure of rand - ((1801439851 * rand) >> 54 * 10000000) is actually mathematically equivalent to rand % 10000000 (lol). I guess this is what you saw in olly, but decompiled code actually shows the % 10000000.

What is this swap min and max crap? Seriously you could just reverse the signs.

Other than that, i really appreciate the work you've done here its been very helpful.
 
Joined
Nov 21, 2013
Messages
3
Reaction score
0
Hello. This source code is very surprising !!
I tried to use this source code, but it failed.
KMS, but it is clear that the source code is connected to lithium, but RandomInRange determines that a different value from the client is output.
What is the function name in IDB Leak for RandomInRange? Or how do I customize it for my other version?

I used a translator because I was not good at English. T-T
 
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
Hello. This source code is very surprising !!
I tried to use this source code, but it failed.
KMS, but it is clear that the source code is connected to lithium, but RandomInRange determines that a different value from the client is output.
What is the function name in IDB Leak for RandomInRange? Or how do I customize it for my other version?

I used a translator because I was not good at English. T-T

The function is actually called get_rand. The seed value has been the same in the client since v92, anything before v92 doesn't use that because they calculate chance a bit differently.
 
Junior Spellweaver
Joined
Nov 16, 2010
Messages
144
Reaction score
72
Ok, the RandomInRange function is just absolutely ridiculously written. There are 2 issues:

This huge complicated procedure of rand - ((1801439851 * rand) >> 54 * 10000000) is actually mathematically equivalent to rand % 10000000 (lol). I guess this is what you saw in olly, but decompiled code actually shows the % 10000000.

What is this swap min and max crap? Seriously you could just reverse the signs.

Other than that, i really appreciate the work you've done here its been very helpful.

Thanks for point out the issues of my release. Yep, you right the "huge complicated procedure of rand" that you talking about is what i saw in ollyDBG, cause my v149'IDB cannot display(decompiled) that part :( Oh, and the swap min/max codes is from the decompiled code in IDA too. i just write all the things i saw in IDA to java without optimized it yet, for sure it working correctly.
.
qjagnsdldi : Unfortunately, this article not apply to old version below v92 like Eric said. In older version, it using a different things to calculate damage, You may need to re-analyze the client to find the formula to calculate damage for older version. And sure, it still using the Xorshift RNG algorithms with the seeds.
 
Initiate Mage
Joined
Jun 15, 2013
Messages
31
Reaction score
0
Just a warning to anyone wanting to apply this to the higher versions (v117+ maybe):

Just computing the expected maximum hit can be a really painful process because of how complicated the damage formulas for various skill become in the higher versions. That in of itself is a very time consuming process to implement.

My opinion is that it is not worth stepping through olly to figure out how the random numbers are used for all these skills.
 
Back
Top