Issue with various battle related rand() calls

A forum to ask questions if you are stuck in the The Gate Rune Wars; or wish for more clarity on the gameplay systems.
Post Reply
Julian
Posts: 87
Joined: Wed Jan 29, 2014 9:17 pm

Issue with various battle related rand() calls

Post by Julian »

Hey all,

I've been looking at some battle code in the PSP version, and I'm seeing some major issues with how rand() is being used. Curious to see if anyone would be able to check on the PS1 side to see if it's the same..?

Code: Select all

((rand() * 100) / 0x7fff) % 100) >= iVar2
iVar2 being I believe a parameter of the status effect, being the success/fail rate. I've been looking at the Beauty Attack unite and Winds of Sleep, both sleep enemies. The value used for iVar2 in this case is 0x1E/30, which seems to imply a 70% success rate.
Assembly version below --

Code: Select all

                             LAB_09ebafc4                                    XREF[1]:     09ebaf8c(j)  
        09ebafc4 fd 28 24 0e     jal        rand                                             
        09ebafc8 00 00 00 00     _nop
        09ebafcc 80 18 02 00     sll        v1,v0,0x2
        09ebafd0 21 18 62 00     addu       v1,v1,v0
        09ebafd4 80 10 03 00     sll        v0,v1,0x2
        09ebafd8 21 10 62 00     addu       v0,v1,v0
        09ebafdc 80 28 02 00     sll        a1,v0,0x2
        09ebafe0 01 80 02 3c     lui        v0,0x8001
        09ebafe4 03 00 42 34     ori        v0,v0,0x3
        09ebafe8 18 00 45 00     mult       v0,a1
        09ebafec c2 27 05 00     srl        a0,a1,0x1f
        09ebaff0 10 18 00 00     mfhi       v1
        09ebaff4 21 18 65 00     addu       v1,v1,a1
        09ebaff8 83 1b 03 00     sra        v1,v1,0xe
        09ebaffc 64 00 02 24     li         v0,0x64
        09ebb000 21 18 64 00     addu       v1,v1,a0
        09ebb004 1a 00 62 00     div        v1,v0
        09ebb008 10 10 00 00     mfhi       v0
        09ebb00c 2a 08 50 00     slt        at,v0,s0
        09ebb010 3a 00 20 14     bne        at,zero,LAB_09ebb0fc
        09ebb014 00 00 00 00     _nop
        09ebb018 21 20 60 02     move       a0,s3
        09ebb01c 84 ed 7a 0e     jal        set_status                                      
        09ebb020 21 28 40 02     _move      a1,s2
        09ebb024 35 00 00 10     b          LAB_09ebb0fc
        09ebb028 00 00 00 00     _nop
This code... makes my head hurt. I have no idea why this is being done. I see normal "rand % 100" and stuff all over the code, but someone really did a number here...

So, on the PSP at least, what happens is: (This logic might be slightly off, this stuff hurts my brain.)

rand returns a number between 0 and 0x7FFFFFFF. The x100 is done via shifts, so numbers over the 32 bits overflow and get lost. (Still a random number I guess) Then the division of 0x7FFF (??? lol) is done through multiplication, does some shifting, and then extends the signed bit. So, if the original number was greater than 0x7FFF0000, the division by 0x7FFF results in a number greater than 0xFFFF and it sign extends it to 0xFFFF----. If its less, you get a normal number between 0 and 0x7FFF.

Essentially, before the modulo operator, we have a value in the range of (-0x7FFF, 0x7FFF), which means the modulo 100 gives us a result with the range of (-100,100), instead of the likely desired [0,100).

Since the success is checking for values greater than the rate given, for 30, a successful roll would be a number from 30 to 99. An unsuccessful roll would be 0 to 29, but since we have an extra 99 negative numbers, its -99 to 29. Essentially... cutting the success rate of all status effects in half.

Now, as if this wasn't enough... I was looking at some of the AI/monster functions, and there's even weirder stuff in here.

In the module files, where the monster data is, at least on the PSP, 0x30 of that structure contains a index that gets converted into a function pointer when the data is loaded. This monster function has... idk what all, but it also includes "AI" determination of which attack to use, for enemies with multiple attacks.

I was looking at the function for the zombie dragon. There's a different kind of malformed rand call in there:

Code: Select all

        if ((rand()* 100) / 0x7fff < 0x47) {
          uVar2 = 0xffffffff;
        }
        else {
          *(code **)(param_1 + 0xc) = FUN_09e13f30;
          uVar2 = 1;
        }
If the rand result is >= 71(/0x47), it sets a function pointer that sets up and does the breath attack. If that pointer isn't set, it defaults to a normal attack. (As far as I could tell...) And the assembly:

Code: Select all

                             LAB_09e13eb0                                    XREF[1]:     09e13e98(j)  
        09e13eb0 fd 28 24 0e     jal        rand                                             
        09e13eb4 00 00 00 00     _nop
        09e13eb8 80 18 02 00     sll        v1,v0,0x2
        09e13ebc 21 18 62 00     addu       v1,v1,v0
        09e13ec0 80 10 03 00     sll        v0,v1,0x2
        09e13ec4 21 10 62 00     addu       v0,v1,v0
        09e13ec8 80 20 02 00     sll        a0,v0,0x2
        09e13ecc 01 80 02 3c     lui        v0,0x8001
        09e13ed0 03 00 42 34     ori        v0,v0,0x3
        09e13ed4 18 00 44 00     mult       v0,a0
        09e13ed8 c2 1f 04 00     srl        v1,a0,0x1f
        09e13edc 10 10 00 00     mfhi       v0
        09e13ee0 21 10 44 00     addu       v0,v0,a0
        09e13ee4 83 13 02 00     sra        v0,v0,0xe
        09e13ee8 21 10 43 00     addu       v0,v0,v1
        09e13eec 47 00 41 28     slti       at,v0,0x47
        09e13ef0 05 00 20 54     bnel       at,zero,LAB_09e13f08
        09e13ef4 ff ff 02 24     _li        v0,-0x1
        09e13ef8 e1 09 02 3c     lui        v0,0x9e1
        09e13efc 30 3f 42 24     addiu      v0,v0,0x3f30
        09e13f00 0c 00 a2 ae     sw         v0=>FUN_09e13f30,0xc(s5)
        09e13f04 01 00 02 24     li         v0,0x1
So, this is exactly the same as the other scenario, but there is no modulo operator. This means the result of the left side has a range of (-0x7FFF, 0x7FFF). The chance that number is >= 71, is... pretty freaking close to 50%, rather than 28%.

What the heck? Scrolling through I saw a lot of rands like this, though most of them seemed to be 50/50 checks, which the code... surprisingly actually accomplishes. :lol:

So it looks like the zombie dragon should have had about a ~30% chance of using the breath attack, but in actuality uses it around ~50%. No wonder that fight can be annoyingly difficult at times.

Anyway, I was wondering if anyone could check on the PS1 and see if that plays out identically? Or is this a bug specific to the PSP port (and likely therefore the remaster as well)? I was thinking of loading it up and checking myself, but figured it would be faster to type this out since others are more familiar with the PS1 code.
User avatar
Pyriel
Webmaster
Posts: 1229
Joined: Wed Aug 18, 2004 1:20 pm

Re: Issue with various battle related rand() calls

Post by Pyriel »

On a cursory review, I see stuff vaguely similar to that, but nothing exactly like it. It could be buried somewhere, and I didn't go through the rigmarole of playing the game to that point so I could check. I saw something very like the first one, but with some of the inexplicable operations in the middle cut out. It's maybe 20% shorter. The library rand() they use on the PS1 returns a 15-bit value, so the code is always going to be different from the jump.

I don't have a ready explanation for why they'd take a potentially huge random number and manipulate it like that in the first place, though. They might have thought it was important on the PS1, and the company doing the port just carried that over.

The biggest chunk of the "head-hurting" part of this is compiler optimization. It's pretty unlikely any person wrote C that does any of that directly. Shifts and adds are so much less costly that if you write "x * 5" the compiler will generally tell the MULT operation to get lost, and do (x<<2)+x instead. The DIV operation is such a drag that compilers will use magic numbers, e.g. 0x66666667 (HI will contain approximately 40% of the input value), in multiply operations just to avoid it. I don't remember the cycle timings for MIPS R3000 or 5900 off the top of my head, but I think MULT is 16 - 32, and DIV varies from like 50 - 128 cycles. Shifts and adds should be one cycle apiece, so you can squeeze in a lot of them before you're competing with the longer operations. And no concern over register stalls.

This is mainly an issue on older hardware. Newer units have been so optimized that there's little to no benefit. Although, it can still be handy on embedded systems. Certain ARM processors don't even have a divide operation to save space in the chip layout, so it's always good to keep magic like 0x51EB851F in the back pocket.

I'd guess the rest of the oddness is manual bumbling, an automated conversion that didn't get reviewed properly, or both. Combined with the optimizations, it is a little hard to parse.
Julian
Posts: 87
Joined: Wed Jan 29, 2014 9:17 pm

Re: Issue with various battle related rand() calls

Post by Julian »

Yeah, I know it's compiler optimization, it just makes it really difficult to read, heh.

I was just curious if the original has the "same issue" (status success rates cut in half, some AI wrong), or if it was fine for the PS1 (which I'm not super familiar with) but this just came about from the poor port job.

I guess I could take a look myself. >.<
User avatar
Pyriel
Webmaster
Posts: 1229
Joined: Wed Aug 18, 2004 1:20 pm

Re: Issue with various battle related rand() calls

Post by Pyriel »

I thought you might, but it's kind of unclear, and I figured Konami doesn't need to eat the blame for inventing a new kind of math on top of everything they actually deserve blame for.

I haven't spent nearly as much time crawling through this game, as compared to 2 and 3. It's generally regarded as pretty sound, relative to 2 anyway. Looking back, I think that just comes from it being simpler. The way they screwed up rune pieces is weird, and I remember the intent being unclear. There are at least two broken spells. The system for drops just smacks of not understanding how people play games, or how painful dependent probabilities can get. The invisible NPCs glitch is widespread and pretty heinous. If this game had as many features as they tried to put into 2, they probably would have found a way to brick PS1s when you fish.

All that to say, it's probably broken somehow in the original.

I packed away most of my stuff when I redid my home office, and upgraded my PC, and I'm annoyingly busy with work. All I have to hand is some old disassemblies from working on the bribe glitch. I'd kind of like to help more, but I just really can't muster much energy for it right now. Sorry.
Julian
Posts: 87
Joined: Wed Jan 29, 2014 9:17 pm

Re: Issue with various battle related rand() calls

Post by Julian »

No worries, I appreciate the insight. Thank you! :)

I'll try to take some time in the next week or so to take a look, and will report back if I find anything.
Post Reply