Remote linking to avatars
Do not link to Suikosource images on forums, webpages, or anyplace else. If you wish to use the images, place them in your own webspace. We do not allow you to use them from our server.
Some characters can be recruited at level 99.
Join at 99 Bug
Bug Details
System(s) Playstation,
Bug TypeGameplay
Region Introduced
Patch Version1.01.02b

Regions Affected
The Join at 99 Bug occurs when certain characters, whose initial level is set based on another character, join the 108 Stars. Due to an error retrieving the reference character's level, the game may calculate an extremely high level for the incoming character, which is then set to 99 by limit checks. This is one of the less burdensome bugs in Suikoden II, and one that can actually benefit the player when it occurs. It is also intermittent, requiring unrelated code to create conditions favorable to it by chance. It may have never shown up during debugging and play-testing, and many players never encounter it. This seems especially true on real hardware (a Playstation or PSOne), as most reports have come from players using emulators.

Affected Characters
Any character whose level on joining is set based on another character is prone to this bug. Notably, these characters join at a level derived from the Hero's current level.
  • Georg
  • Hoi
  • Mazus
  • Pesmerga
  • Vincent
  • Anita (when recruited in Muse or else without Valeria/Kasumi)
Addtional characters who are prone are:
  • L.C. Chan (Long Chan Chan) who will be set based on Wakaba's level.
  • Anita's level can be set based on Valeria's (possibly Kasumi's as well) if she is recruited in Banner Village with Valeria in the party.

The bug is caused by bad management of variable sizes. In the original source, the problem is likely a casting issue, with a 32-bit integer pointer being cast to an 8-bit pointer in a function call. The assembly below shows where the actual error occurs. (This is taken from the PAL-E/European English version of the game.)

TEXT:800A23B0 BuildCharaToLV: # DATA XREF: TEXT:800ECE64o
TEXT:800A23B0 var_18 = -0x18
TEXT:800A23B0 var_10 = -0x10
TEXT:800A23B0 var_C = -0xC
TEXT:800A23B0 var_8 = -8
TEXT:800A23B0 var_4 = -4
TEXT:800A23B0 addiu $sp, -0x28
TEXT:800A23B4 andi $a1, 0xFF
TEXT:800A23B8 sll $a1, 4
TEXT:800A23BC addu $t0, $a0, $a1
TEXT:800A23C0 move $a0, $0
TEXT:800A23C4 move $a1, $a0
TEXT:800A23C8 sw $ra, 0x28+var_4($sp)
TEXT:800A23CC sw $s2, 0x28+var_8($sp)
TEXT:800A23D0 sw $s1, 0x28+var_C($sp)
TEXT:800A23D4 sw $s0, 0x28+var_10($sp)
TEXT:800A23D8 lw $v0, 0x7C($t0)
TEXT:800A23DC addiu $a3, $sp, 0x28+var_18 <-------
TEXT:800A23E0 lbu $s2, 0($v0)
TEXT:800A23E4 addiu $v0, 1
TEXT:800A23E8 sw $v0, 0x7C($t0)
TEXT:800A23EC lbu $a2, 0($v0)
TEXT:800A23F0 addiu $v1, $v0, 1
TEXT:800A23F4 sw $v1, 0x7C($t0)
TEXT:800A23F8 lbu $s0, 1($v0)
TEXT:800A23FC addiu $v1, $v0, 2
TEXT:800A2400 sw $v1, 0x7C($t0)
TEXT:800A2404 lbu $s1, 2($v0)
TEXT:800A2408 addiu $v0, 3
TEXT:800A240C jal MP_Level_Sub
TEXT:800A2410 sw $v0, 0x7C($t0)
TEXT:800A2414 bnez $s0, loc_800A2428
TEXT:800A2418 nop
TEXT:800A241C lw $v0, 0x28+var_18($sp) <-------
TEXT:800A2420 j loc_800A2440
TEXT:800A2424 addu $v0, $s1, $v0
TEXT:800A2428 # ---------------------------------------------------------------------------
TEXT:800A2428 loc_800A2428: # CODE XREF: BuildCharaToLV+64j
TEXT:800A2428 lw $v0, 0x28+var_18($sp) <-------
TEXT:800A242C nop
TEXT:800A2430 subu $v0, $s1
TEXT:800A2434 bgtz $v0, loc_800A2440
TEXT:800A2438 nop
TEXT:800A243C li $v0, 1
TEXT:800A2440 loc_800A2440: # CODE XREF: BuildCharaToLV+70j
TEXT:800A2440 # BuildCharaToLV+84j
TEXT:800A2440 sw $v0, 0x28+var_18($sp)
TEXT:800A2444 li $a0, 3
TEXT:800A2448 lw $a1, 0x28+var_18($sp)
TEXT:800A244C move $a2, $s2
TEXT:800A2450 jal MP_Level_Sub
TEXT:800A2454 move $a3, $0
TEXT:800A2458 lw $ra, 0x28+var_4($sp)
TEXT:800A245C lw $s2, 0x28+var_8($sp)
TEXT:800A2460 lw $s1, 0x28+var_C($sp)
TEXT:800A2464 lw $s0, 0x28+var_10($sp)
TEXT:800A2468 addiu $sp, 0x28
TEXT:800A246C jr $ra
TEXT:800A2470 nop

TEXT:800A2470 # End of function BuildCharaToLV
The lines indicated with arrows are the proximate cause of the bug. The MP_Level_Sub is multi-purpose. It can add levels to a character, retrieve a particular character's current level, etc. In the function above, it is used in both capacities. The first call retrieves a character's current level. In most cases this will be the Hero. When called for this purpose, it expects to be handed a pointer to a signed 8-bit integer that it can store the requested character's level on. Within the function, it actually executes a Store Byte command on this address. The problem is, the BuildCharaToLV subroutine has passed a 32-bit pointer to an address on the stack, and has not taken the trouble to initialize the storage. When it subsequently loads the value given back by MP_Level_Sub, as 32-bits, it will get the byte stored, along with three bytes that will contain whatever values were left in them. When it calls MP_Level_Sub the second time, it passes an argument that could be the desired level, or anything up to 0x7FFFFF00 + the desired level. Luckily, other functions will prevent the level from going over 99, or otherwise doing something that would let this result in a crash.

Ideally, the calling function would initialize the space to zero before making the call. It is not terribly practical to edit the routine to do this, as none of the operations preceding the call are easily replaceable. The fix selected was to replace the first two Load Word operations that retrieve the stored level with Load Byte Unsigned operations. The end result is that the value is retrieved as it was stored, operated on, and then stored again as a word, clearing the upper bits of their stale values.