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!

Rolling the dice - Is getting 13 possible?

Moderator
Staff member
Moderator
Joined
Jul 30, 2012
Messages
1,103
Reaction score
432
Hi,

For a couple of years there has been a long, but not so useful discussion within a few Private Servers: Is rolling a 13 with the dice roll possible when creating a character?

Based on running a v62 server for 5 years my personal opinion is no. However, some are saying yes, and some are saying it depends on the client version (yes, for example, v1 but no for v62)

Dice rolling itself got completely removed in v0.64.

- We know that in the BMS-leak server files there are no real checks preventing you from getting 13, which for some people already support it based on that getting a 13 is possible.

- In January of this year, we have added an auto-ban for anyone getting a 13 and creating their character. Nobody has triggered it yet.

- A person that mods the client a lot claims the chance of getting a 13 is 0.14% (probably meant 0.0014%). However, there's still no proof of anyone ever getting a 13 outside of using Cheat Engine.

So here comes the question for anyone that is good with client reverse engineering: Is rolling a 13 by checking dice rolling client code possible? And if possible, if it was possible in all versions (v1-v63)?

Maybe Eric has any ideas.

If 0.0014% ends up being true I am honestly very surprised nobody has gotten it yet with proof.
 
Last edited:
Newbie Spellweaver
Joined
Sep 27, 2018
Messages
93
Reaction score
20
They changed the system and formula for the dice rolling in v56, I remember playing v55 back in the day and was able to roll 13/4/4/4 just fine, but as soon as v56 kicked in, it was seemingly impossible.

What you could do is create a new account, set it at character creation and leave an auto-clicker on the dice to see if it ever triggers your autoban. I'd say after a 24 hour period of it not triggering your autoban, then it'd be safe to assume it's downright impossible or the probability is just that low.
 
Upvote 0
Moderator
Staff member
Moderator
Joined
Jul 30, 2012
Messages
1,103
Reaction score
432
What you could do is create a new account, set it at character creation and leave an auto-clicker on the dice to see if it ever triggers your autoban. I'd say after a 24 hour period of it not triggering your auto ban, then it'd be safe to assume it's downright impossible or the probability is just that low.

Unfortunately, dice rolling is completely client-sided, and you only send a packet when you actually create a character. A memory reader that alerts could be an option, but we have a timeout check on our server that disconnects you after 30 minutes of not sending any packets (which are not sent at the dice rolling screen)
 
Upvote 0
Newbie Spellweaver
Joined
Sep 27, 2018
Messages
93
Reaction score
20
Unfortunately, dice rolling is completely client-sided, and you only send a packet when you actually create a character. A memory reader that alerts could be an option, but we have a timeout check on our server that disconnects you after 30 minutes of not sending any packets (which are not sent at the dice rolling screen)
Ah, well that throws a wrench into my idea! But yeah, from my understanding, when v56 was released is when the 13/4/4/4 roll was seemingly impossible. v56 was Pirates, correct? Maybe it has something to do that? Though, if I recall, you can pretty much play Pirates in v55 with some minor edits.
 
Upvote 0
Custom Title Activated
Loyal Member
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
sorry, been MIA for a while :(

To answer your question, yes getting 13 is possible. If you've ever cheated the dice roll back in the day through CE , you would do so by manipulating the randomizer to always return the index of the stat (e.g 0 = str, 1 = dex, etc). What this did was when the client processes through the RNG, the value will always be added to that stat index +1 (all stats are always initialized to 4).

The way it works:
1. Initializes STR/DEX/INT/LUK to 4/4/4/4.
2. Executes a 9-iteration forloop.
3. Within the forloop, an index is randomized: rand() / 4, rand() % 4.
4. On the chosen index, add to the stat base +1.
Therefore, if you have 4 different stats, and can increase the stat 9 additional times, then to roll a 13 is a 1 in 36 chance (1/36 => 0.02%) . Not sure where your friend got 0.14 or 0.0014 from, of which is either too high or too low. Unless Nexon changed it at some other point in time, 1/36/2 = 0.0138 = 0.014, but idk.

EDIT: Confirmed same formula from v1-v63. It's the same from v62 down to v49 down to v28, so it's never changed across versions.

Since you're working with v62 Kimberly in case you're curious, the address to dice roll processing in the client is 005827AB.

Pseudocode:
PHP:
int __thiscall CUINewCharAvatarSelect::OnDiceRoll(int this)
{
  int v1; // esi@1
  signed int v2; // edi@1
  int v3; // eax@2
  int result; // eax@2
  int v5; // edx@2
  signed __int64 v6; // qtt@2
  int v7; // edx@3
  int v8; // edx@4
  unsigned int v9; // eax@6
  unsigned int v10; // eax@7
  unsigned int v11; // eax@8
  unsigned int v12; // eax@9
  signed int v13; // [sp+10h] [bp-4h]@1

  v1 = this;
  v2 = this + 148;
  *(this + 156) = ZtlSecureTear_int_(4, this + 148);// STR
  *(v1 + 168) = ZtlSecureTear_int_(4, v1 + 160);// DEX
  *(v1 + 180) = ZtlSecureTear_int_(4, v1 + 172);// INT
  *(v1 + 192) = ZtlSecureTear_int_(4, v1 + 184);// LUK
  v13 = 9;
  do
  {
    v3 = rand();
    v6 = v3;
    result = v3 / 4;
    v5 = v6 % 4;
    if ( v5 )
    {
      v7 = v5 - 1;
      if ( v7 )
      {
        v8 = v7 - 1;
        if ( v8 )
        {
          if ( v8 == 1 )
          {
            v9 = ZtlSecureFuse_int_(v1 + 184, *(v1 + 192));
            result = ZtlSecureTear_int_(v9 + 1, v1 + 184);
            *(v1 + 192) = result;
          }
        }
        else
        {
          v10 = ZtlSecureFuse_int_(v1 + 172, *(v1 + 180));
          result = ZtlSecureTear_int_(v10 + 1, v1 + 172);
          *(v1 + 180) = result;
        }
      }
      else
      {
        v11 = ZtlSecureFuse_int_(v1 + 160, *(v1 + 168));
        result = ZtlSecureTear_int_(v11 + 1, v1 + 160);
        *(v1 + 168) = result;
      }
    }
    else
    {
      v12 = ZtlSecureFuse_int_(v2, *(v1 + 156));
      result = ZtlSecureTear_int_(v12 + 1, v2);
      *(v1 + 156) = result;
    }
    --v13;
  }
  while ( v13 );
  return result;
}

I agree though, would be cool to see someone who's gotten this legitimately; that's crazy lucky! Anywho, hope this helps :)
 
Last edited:
Upvote 0
Newbie Spellweaver
Joined
Sep 27, 2018
Messages
93
Reaction score
20
Hmm, I swear I remember reading back then that they changed the dice system and the probabilities also changed. I guess not, it was 12 or so years ago now. LOL! Thanks for the info, Eric!
 
Upvote 0
Moderator
Staff member
Moderator
Joined
Jul 30, 2012
Messages
1,103
Reaction score
432
Not sure where your friend got 0.14 or 0.0014 from, of which is either too high or too low. Unless Nexon changed it at some other point in time, 1/36/2 = 0.0138 = 0.014, but idk.

It was someone else from a different server where I heard it. Since I always believed it to be impossible, I wanted to know the truth ;p

I agree though, would be cool to see someone who's gotten this legitimately; that's crazy lucky! Anywho, hope this helps :)

One hope someone is bored enough to run a vanilla localhost server for the funnies and leave a bot running with memory checks :')

Thanks for the info!
 
Upvote 0
Banned
Banned
Joined
Jun 8, 2007
Messages
165
Reaction score
8
sorry, been MIA for a while :(

To answer your question, yes getting 13 is possible. If you've ever cheated the dice roll back in the day through CE , you would do so by manipulating the randomizer to always return the index of the stat (e.g 0 = str, 1 = dex, etc). What this did was when the client processes through the RNG, the value will always be added to that stat index +1 (all stats are always initialized to 4).

The way it works:
1. Initializes STR/DEX/INT/LUK to 4/4/4/4.
2. Executes a 9-iteration forloop.
3. Within the forloop, an index is randomized: rand() / 4, rand() % 4.
4. On the chosen index, add to the stat base +1.
Therefore, if you have 4 different stats, and can increase the stat 9 additional times, then to roll a 13 is a 1 in 36 chance (1/36 => 0.02%) . Not sure where your friend got 0.14 or 0.0014 from, of which is either too high or too low. Unless Nexon changed it at some other point in time, 1/36/2 = 0.0138 = 0.014, but idk.

EDIT: Confirmed same formula from v1-v63. It's the same from v62 down to v49 down to v28, so it's never changed across versions.

Since you're working with v62 @Kimberly in case you're curious, the address to dice roll processing in the client is 005827AB.

Pseudocode:
PHP:
int __thiscall CUINewCharAvatarSelect::OnDiceRoll(int this)
{
  int v1; // esi@1
  signed int v2; // edi@1
  int v3; // eax@2
  int result; // eax@2
  int v5; // edx@2
  signed __int64 v6; // qtt@2
  int v7; // edx@3
  int v8; // edx@4
  unsigned int v9; // eax@6
  unsigned int v10; // eax@7
  unsigned int v11; // eax@8
  unsigned int v12; // eax@9
  signed int v13; // [sp+10h] [bp-4h]@1

  v1 = this;
  v2 = this + 148;
  *(this + 156) = ZtlSecureTear_int_(4, this + 148);// STR
  *(v1 + 168) = ZtlSecureTear_int_(4, v1 + 160);// DEX
  *(v1 + 180) = ZtlSecureTear_int_(4, v1 + 172);// INT
  *(v1 + 192) = ZtlSecureTear_int_(4, v1 + 184);// LUK
  v13 = 9;
  do
  {
    v3 = rand();
    v6 = v3;
    result = v3 / 4;
    v5 = v6 % 4;
    if ( v5 )
    {
      v7 = v5 - 1;
      if ( v7 )
      {
        v8 = v7 - 1;
        if ( v8 )
        {
          if ( v8 == 1 )
          {
            v9 = ZtlSecureFuse_int_(v1 + 184, *(v1 + 192));
            result = ZtlSecureTear_int_(v9 + 1, v1 + 184);
            *(v1 + 192) = result;
          }
        }
        else
        {
          v10 = ZtlSecureFuse_int_(v1 + 172, *(v1 + 180));
          result = ZtlSecureTear_int_(v10 + 1, v1 + 172);
          *(v1 + 180) = result;
        }
      }
      else
      {
        v11 = ZtlSecureFuse_int_(v1 + 160, *(v1 + 168));
        result = ZtlSecureTear_int_(v11 + 1, v1 + 160);
        *(v1 + 168) = result;
      }
    }
    else
    {
      v12 = ZtlSecureFuse_int_(v2, *(v1 + 156));
      result = ZtlSecureTear_int_(v12 + 1, v2);
      *(v1 + 156) = result;
    }
    --v13;
  }
  while ( v13 );
  return result;
}

I agree though, would be cool to see someone who's gotten this legitimately; that's crazy lucky! Anywho, hope this helps :)

So after some reasonable doubt from Kimmy I decided to revisit this using the actual client and try to deep dive into it a bit more. Using a v40b client I called the function responsible for generating these RNG numbers into dummy allocated memory and looked at the results by fusing the ZtlSecure longs. Multiply this by a loooooot of iterations and these were my results:

Kimberly - Rolling the dice - Is getting 13 possible? - RaGEZONE Forums


Clearly something is wrong here, since after more than 4 billion results we still have no 13s rolled. I tried double checking the pseudocode and even reimplementing it using stdlib's rand(), and it still came up with 0 thirteens (I also specified it to only report results for STR):
Kimberly - Rolling the dice - Is getting 13 possible? - RaGEZONE Forums


To verify I also called MapleStory.exe's rand() by calling it in the import table, and got similar results:
Kimberly - Rolling the dice - Is getting 13 possible? - RaGEZONE Forums


The algorithm looks correct, so to test my theory that the PRNG is off I switched out the random generator by nexon's own CRand32:
Kimberly - Rolling the dice - Is getting 13 possible? - RaGEZONE Forums


yay! Seems like it's the use of rand() that makes rolling a 13 impossible. For completeness I also tried a mersenne twister PRNG using std::mt19937:

Kimberly - Rolling the dice - Is getting 13 possible? - RaGEZONE Forums


These values also confirm the chance of rolling a 13 for a specific stat in theory being 1/262144, and for a specific stat 1/65535.

So it is literally impossible to roll 13s. You can autoban people for it if you want, or fix the function by using a proper PRNG.
 
Upvote 0
Newbie Spellweaver
Joined
Oct 11, 2015
Messages
5
Reaction score
8
To expand on the above ^ the 1/36 statistic is wrong. If we treat each stat assignment as a 4-sided die roll then it's a probability of 1/4 each time. Knowing this we can compute the probability of finding n additional points in any stat, let's say strength. To get n additional strength, you must roll strength (1/4 prob) n times and not strength (3/4 prob) 9-n times. There are 9 choose n (= 9!/(n!(9-n)!)) ways this can happen. The rolls are independent. For the stastically inclined, this formula is exactly the definition of a binomial distribution:

P(n) = (1/4)^n * (3/4)^(9-n) * (9 choose n)

So for 9 additional strength (for a total of 13 strength once added to the base value of 4), P(n) = (1/4)^9 * (3/4)^0 * (9 choose 9) = (1/4)^9 = 1/262144.

The issue, though, is rand() % n is not really random. Yes, it produces an even distribution when run many times, but the issue is there is a "predictability" to it that seems to stop it from generating the same value twice in a row. Stated mathematically, P(str|str) != P(str)P(str) = 1/16 for two repetitions of the algorithm in a row. In fact for rand(), it appears to be almost zero! For a more intuitive example, let's say I have two coins: a normal penny, and a magical penny that always comes up heads if the last flip was tails, and vice versa. Both have the same distribution (even heads and tails in the limit of many coin flips), which might convince you both are random, but your magical penny can never flip a heads twice in a row and is obviously not random.

Depending on your compiler, especially old ones, rand() can have this fatal "magic penny" flaw which makes rolling a 13 impossible, since doing that requires getting the same value 9 times in a row, something rand() is not good at. Some quick googling reveals this issue may be fixed based on compiler/platform, however. So the answer to "can you roll a 13?" is "theoretically yes, but it depends on nexon's compiler for the client you're looking at". So it may be the case that in some later versions before dice were removed, it worked, and in earlier ones, it did not, even if no line of code was changed over that time. As foxpat showed, switching to a better rng algorithm - even nexon's crand32 - gives almost exactly the theoretical prediction of
1/262144. Even in that case though, you can clearly see that it is basically impossible to roll a 13 legitimately because you'd have to roll on average several hundred thousand times. But there may be some credibility to people saying it's happened before.

If you suspect your client is capable of doing it (ie. has a not completely broken rand() implementation) then you can design an anticheat method statistically instead of just autobanning when someone rolls a 13, which is less likely to ban someone who gets it after many rolls compared to someone who gets it right away. Here's a sketch of how you could do that:

1. Collect a player's dice rolls
2. Use some measure to compute the distance or divergence between his distribution of rolls to the expected binomial one (KL Divergence, KS test, chi-squared, etc.)
3. Set some threshold for autoban. You now have an automatic ban with tunable confidence. This will work for anyone manipulating rolls, not just for rolling 13
 
Last edited:
Upvote 0
Back
Top