Rune Unites are powerful spells triggered by using fourth level spells from two compatible runes. There are two ways to trigger a unite. The simplest is by equipping one magic-user with two compatible runes. As long as he has two, fourth-level magic points, he will be able to select the Unite from the spell list. A somewhat more complicated way is by having two different mages cast compatible spells. The game should combine them into a single spell, cast by the faster mage (determined by the speed stat). Unites always involve some amount of special handling by the game, regardless of how they are initiated. For instance, the offensive power of the spell carries two elements, and this requires a special set of rules for calculating damage.

When you try to use two characters to form a Unite, there is an annoying bug that can come into play. If a character A casts a candidate spell, and two or more others cast compatible spells, then A will unite with all of them. The results you get will depend on your characters' speed and the formation of your party, and can vary widely. In simple terms, you will either get more unites for less cost, or several of your characters will have their actions cancelled.

Affected Versions


All versions of the game are affected by this bug.

Bug Details


When doing a two-person Unite, the game finds them using the following rules, starting with the first character in the formation. Speed and order of action does not matter here.
  1. The current character (primary) must be available, i.e. not already part of a Unite, and casting a fourth level spell.
  2. Each remaining character (candidate), that comes after the primary in the formation is checked in turn. If they are available, and casting a compatible, fourth level spell then the appropriate Unite will trigger.
  3. When a Unite is triggered, the faster character is assigned the action. The slower character has his action cancelled.
  4. Repeat from step two for the next candidate. Regardless of whether or not a unite is triggered for a pair, the game continues to search additional candidates for the current primary character.
  5. When there are no more candidates left, the next primary is evaluated (go to step 1) until the last member of the party is reached.

The bug occurs in step 4 of the list. Since the search is allowed to continue after a Unite is found, the Primary character is allowed to partner with multiple candidates. The effects are determined in step 3. If the Primary is the faster of all pairs found, then he will perform a single action, and all other character's actions will be cancelled. If the Primary is slower in all pairs, then his action will be cancelled, but every faster candidate will perform a Unite.

This bug can be rather confusing. The results are seemingly inconsistent because multiple variables affect its outcomes.

Example 1

Note: Numbers are the characters' speed. The formation number ascends starting on the left and moving right in each row.
port_riou.png
(220)
Guardian Earth
port_nanami.png
(215)
Final Flame
port_luc.png
(205)
Final Flame
port_mazus.png
(180)
Final Flame
port_tengaar.png
(165)
Final Flame
port_viki.png
(140)
Final Flame
Result: Hero casts Scorched Earth with Viki. Nobody else moves.

The game starts by searching for spells compatible with the Hero's. It first finds Nanami's, cancels her action, and assigns the Hero Scorched Earth because he's faster. It repeats this with each character in turn, finding them compatible, cancelling their action, and assigning the hero the appropriate Unite. When it's done, everybody is in a Unite, everyone but the Hero has had their action cancelled, and the Hero's partner is Viki, the last character whose action was compatible.

Example 2

port_riou.png
(220)
Guardian Earth
port_nanami.png
(215)
Final Flame
port_luc.png
(205)
Final Flame
port_mazus.png
(180)
Final Flame
port_tengaar.png
(165)
Final Flame
port_viki.png
(140)
Shining Wind
Result: Hero casts Storm Fang with Viki. Nobody else moves.

This example just obviates the fact that the last Unite found takes precedent.

Example 3

port_viki.png
(140)
Guardian Earth
port_nanami.png
(215)
Final Flame
port_luc.png
(205)
Final Flame
port_mazus.png
(180)
Final Flame
port_tengaar.png
(165)
Final Flame
port_riou.png
(220)
Final Flame
Result: Everyone but Viki casts Scorched Earth. Viki does nothing.

This is the pure exploitation example. As long as your slower characters are earlier in the formation, it's easy to exploit this bug. In this case, the party performs 5 unites for a cost of 6 MP. It should have cost 10 MP, and it shouldn't be possible to do more than three unites anyway. Viki participates in 5 unites for 1 MP.

Example 4

port_riou.png
(220)
Guardian Earth
port_nanami.png
(215)
Final Flame
port_luc.png
(205)
Final Flame
port_mazus.png
(225)
Final Flame
port_tengaar.png
(165)
Final Flame
port_viki.png
(140)
Shining Wind
Result: Hero & Mazus cast Scorched Earth. Nobody else moves.

Mazus is faster than the Hero in this example. The search will start with the Hero. It finds Nanami, cancels her action, and assigns the Hero Scorched Earth. The same occurs with Luc. When it finds Mazus, he draws the action since he's faster, and the Hero's is cancelled. Then the game moves on and finds Tengaar. Her action is cancelled, and the Hero gets assigned Scorched Earth again. Then it does the same for Viki. The end result is that the Hero unites twice for 1 MP. One instance of the spell will be cast by Mazus, and one instance will be cast by the Hero. After the round, Luc, Tengaar, and Nanami's MP will remain unchanged. Mazus, the Hero, and Viki will all be down 1 fourth-level MP.

There are a ridiculous number of combinations with this bug, and it's not worth going into them all.

Cause


As noted above, the cause is that the search does not end when a Unite is triggered. It should immediately move onto the next available character, and begin searching candidates for them, instead of continuing to search for more Unites.

The code involved is in two modules, /CDROM/150_BPRG/BUFF0/BP0_FST.BIN and /CDROM/150_BPRG/BUFF0/BP0_SEC.BIN. The former runs during the first round of battle, and the latter during the second onward. Below is a snippet of the code that runs when the Primary partner is casting a Fire spell. In total, the search takes up around 500 operations in each module.
RAM:8003F030 loc_8003F030:                            # CODE XREF: RAM:8003F0F8j
RAM:8003F030                 lb      $v0, 0x50($s2)   # Current Actor using fire...
RAM:8003F034                 li      $a3, 2
RAM:8003F038                 bne     $v0, $a3, loc_8003F0EC
RAM:8003F03C                 move    $a0, $s5
RAM:8003F040                 jal     sub_8003EDF0     # Get spell level?
RAM:8003F044                 move    $a1, $s1
RAM:8003F048  # ---------------------------------------------------------------------------
RAM:8003F048                 move    $s0, $v0
RAM:8003F04C                 move    $a0, $s5
RAM:8003F050                 jal     sub_8003EE5C     # Get action
RAM:8003F054                 move    $a1, $s1
RAM:8003F058  # ---------------------------------------------------------------------------
RAM:8003F058                 li      $a3, 3
RAM:8003F05C                 bne     $s0, $a3, loc_8003F0EC  # Skip if not a idx 3 (4th level) spell?
RAM:8003F060                 move    $v1, $v0
RAM:8003F064                 slti    $v0, $v1, 0x13   # Other Actor using Earth
RAM:8003F068                 bnez    $v0, loc_8003F0A8
RAM:8003F06C                 move    $a0, $0
RAM:8003F070                 slti    $v0, $v1, 0x15
RAM:8003F074                 beqz    $v0, loc_8003F084
RAM:8003F078                 li      $a0, 0x36        # Scorched Earth
RAM:8003F07C                 j       loc_8003F0A8
RAM:8003F080                 move    $s6, $0
RAM:8003F084  # ---------------------------------------------------------------------------
RAM:8003F084
RAM:8003F084 loc_8003F084:                            # CODE XREF: RAM:8003F074j
RAM:8003F084                 slti    $v0, $v1, 0x1A   # Other Actor using Lightning
RAM:8003F088                 beqz    $v0, loc_8003F0A4
RAM:8003F08C                 slti    $v0, $v1, 0x18
RAM:8003F090                 bnez    $v0, loc_8003F0A8
RAM:8003F094                 move    $a0, $0
RAM:8003F098                 lb      $s6, 0x52($s2)
RAM:8003F09C                 j       loc_8003F0A8
RAM:8003F0A0                 li      $a0, 0x3A        # Blazing Camp
RAM:8003F0A4  # ---------------------------------------------------------------------------
RAM:8003F0A4
RAM:8003F0A4 loc_8003F0A4:                            # CODE XREF: RAM:8003F088j
RAM:8003F0A4                 move    $a0, $0
RAM:8003F0A8
RAM:8003F0A8 loc_8003F0A8:                            # CODE XREF: RAM:8003F068j
RAM:8003F0A8                                          # RAM:8003F07Cj ...
RAM:8003F0A8                 beqz    $a0, loc_8003F0EC
RAM:8003F0AC                 nop
RAM:8003F0B0                 lhu     $v0, 0x3A($s3)   # Speed
RAM:8003F0B4                 lhu     $v1, 0x3A($s2)
RAM:8003F0B8                 nop
RAM:8003F0BC                 sltu    $v0, $v1
RAM:8003F0C0                 bnez    $v0, loc_8003F0DC
RAM:8003F0C4                 nop
RAM:8003F0C8                 sb      $s6, 0x52($s3)
RAM:8003F0CC                 sb      $a0, 0x51($s3)
RAM:8003F0D0                 sb      $s1, 0x45($s3)
RAM:8003F0D4                 j       loc_8003F0EC     # Continue Search???
RAM:8003F0D8                 sb      $fp, 0x50($s2)
RAM:8003F0DC  # ---------------------------------------------------------------------------
RAM:8003F0DC
RAM:8003F0DC loc_8003F0DC:                            # CODE XREF: RAM:8003F0C0j
RAM:8003F0DC                 sb      $fp, 0x50($s3)
RAM:8003F0E0                 sb      $s6, 0x52($s2)
RAM:8003F0E4                 sb      $a0, 0x51($s2)
RAM:8003F0E8                 sb      $s4, 0x45($s2)
RAM:8003F0EC

Fix


The fix is to simply end the search properly. This requires making a bit of room for unconditional branch operations, which is somewhat complicated by the fact that the search has numerous branches, and is part of a larger switch block.

; Suikoden II Rune Unite Fix
; Written by Pyriel
;
; The game initiates a search for each character not already marked as "united".
; When it discovers a compatible spell, it assigns the faster character the unite
; spell, and sets up the slower character to do nothing.
;
; For some bizarre reason, after a compatible spell is found, the search continues
; for the current character. If more compatible spells are found, the unite
; assignment will occur again, and another character could have its actions
; cancelled.
;
; This bug presents itself in multiple ways, depending on the speed of the primary and secondary
; characters in the search/unite.
;
; To fix the problem, the search must be ended after a match is found. This require shuffling
; some operations to make room.
 
.psx
.align 4
 
.openfile BP0_FST.BIN, 0x8002B000
.headersize 0
 
 
; Primary=Fire -- Secondary=Earth or Lightning
.org 0x8003F0A8
.area 0x8003F0F0-.
 
primFire:
 beqz $a0, loc_8003F0EC ; exit if unite not found
 lhu $v0, 0x3A($s3) ; slid this up to replace a nop
 lhu $v1, 0x3A($s2) ; need an extra op for a new J to "next"
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003F0D8
 nop
 sb $s6, 0x52($s3)
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003F4C8 ; end search
 sb $fp, 0x50($s2)
loc_8003F0D8: 
 sb $fp, 0x50($s3)
 sb $s6, 0x52($s2)
 sb $a0, 0x51($s2)
 j 0x8003F4C8 ; end search
 sb $s4, 0x45($s2)
loc_8003F0EC:
 lbu $v0, 0x340($s5)
 
.endarea ; 0x8003F0A8 - 0x8003F0F0
 
 
 
 
 
 
; Primary=Water -- Secondary=Wind or Lightning
; Changes identical to above
 
.org 0x8003F1A8
.area 0x8003F1F0-.
 
primWater:
 beqz $a0, loc_8003F1EC
 lhu $v0, 0x3A($s3)
 lhu $v1, 0x3A($s2)
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003F1D8
 nop
 sb $s6, 0x52($s3)
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003F4C8
 sb $fp, 0x50($s2)
loc_8003F1D8:
 sb $fp, 0x50($s3)
 sb $s6, 0x52($s2)
 sb $a0, 0x51($s2)
 j 0x8003F4C8
 sb $s4, 0x45($s2)
loc_8003F1EC:
 lbu $v0, 0x340($s5)
 
.endarea ; 0x8003F1A8 - 0x8003F1F0
 
 
 
 
 
 
; Primary=Wind -- Secondary=Water or Earth
; Changes identical to above (less one SB here for some reason...)
 
.org 0x8003F29C
.area 0x8003F2DC-.
 
primWind:
 beqz $a0, loc_8003F2D8
 lhu $v0, 0x3A($s3)
 lhu $v1, 0x3A($s2)
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003F2C8
 nop
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003F4C8
 sb $fp, 0x50($s2)
loc_8003F2C8:
 sb $fp, 0x50($s3)
 sb $a0, 0x51($s2)
 j 0x8003F4C8
 sb $s4, 0x45($s2)
loc_8003F2D8:
 lbu $v0, 0x340($s5)
 
 
.endarea ; 0x8003F29C - 0x8003F2D8
 
 
 
 
 
 
; Primary=Earth -- Secondary=Fire or Wind
; Changes identical to above (less one SB here for some reason...)
 
.org 0x8003F380
.area 0x8003F3C0-.
 
primEarth:
 beqz $a0, loc_8003F3BC
 lhu $v0, 0x3A($s3)
 lhu $v1, 0x3A($s2)
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003F3AC
 nop
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003F4C8
 sb $fp, 0x50($s2)
loc_8003F3AC:
 sb $fp, 0x50($s3)
 sb $a0, 0x51($s2)
 j 0x8003F4C8
 sb $s4, 0x45($s2)
loc_8003F3BC:
 lbu $v0, 0x340($s5)
 
.endarea ; 0x8003F380 - 0x8003F3BC
 
 
 
 
 
 
 
; Primary=Lightning -- Secondary=Fire or Water
; Changes identical to above
 
.org 0x8003F470
.area 0x8003F4B8-.
 
primLightning:
 beqz $a0, loc_8003F4B4
 lhu $v0, 0x3A($s3)
 lhu $v1, 0x3A($s2)
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003F4A0
 nop
 sb $s6, 0x52($s3)
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003F4C8
 sb $fp, 0x50($s2)
loc_8003F4A0:
 sb $fp, 0x50($s3)
 sb $a0, 0x51($s2)
 sb $s6, 0x52($s2)
 j 0x8003F4C8
 sb $s4, 0x45($s2)
loc_8003F4B4:
 lbu $v0, 0x340($s5)
 
.endarea ; 0x8003F470 - 0x8003F4B4
.close
 
 
 
 
 
 
 
;###################### Begin Second File ###################
 
 
.openfile BP0_SEC.BIN, 0x8002B000
.headersize 0
 
 
; Primary=Fire -- Secondary=Earth or Lightning
.org 0x8003E860
.area 0x8003E8A8-.
 
primFire2:
 beqz $a0, loc_8003E8A4 ; exit if unite not found
 lhu $v0, 0x3A($s3) ; slid this up to replace a nop
 lhu $v1, 0x3A($s2) ; need an extra op for a new J to "next"
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003E890
 nop
 sb $s6, 0x52($s3)
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003EC80 ; end search
 sb $fp, 0x50($s2)
loc_8003E890: 
 sb $fp, 0x50($s3)
 sb $s6, 0x52($s2)
 sb $a0, 0x51($s2)
 j 0x8003EC80 ; end search
 sb $s4, 0x45($s2)
loc_8003E8A4:
 lbu $v0, 0x340($s5)
 
.endarea ; 0x8003E860 - 0x8003E8A8
 
 
 
 
 
 
; Primary=Water -- Secondary=Wind or Lightning
; Changes identical to above
 
.org 0x8003E960
.area 0x8003E9A8-.
 
primWater2:
 beqz $a0, loc_8003E9A4
 lhu $v0, 0x3A($s3)
 lhu $v1, 0x3A($s2)
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003E990
 nop
 sb $s6, 0x52($s3)
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003EC80
 sb $fp, 0x50($s2)
loc_8003E990:
 sb $fp, 0x50($s3)
 sb $s6, 0x52($s2)
 sb $a0, 0x51($s2)
 j 0x8003EC80
 sb $s4, 0x45($s2)
loc_8003E9A4:
 lbu $v0, 0x340($s5)
 
.endarea ; 0x8003E960 - 0x8003E9A8
 
 
 
 
 
 
; Primary=Wind -- Secondary=Water or Earth
; Changes identical to above (less one SB here for some reason...)
 
.org 0x8003EA54
.area 0x8003EA94-.
 
primWind2:
 beqz $a0, loc_8003EA90
 lhu $v0, 0x3A($s3)
 lhu $v1, 0x3A($s2)
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003EA80
 nop
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003EC80
 sb $fp, 0x50($s2)
loc_8003EA80:
 sb $fp, 0x50($s3)
 sb $a0, 0x51($s2)
 j 0x8003EC80
 sb $s4, 0x45($s2)
loc_8003EA90:
 lbu $v0, 0x340($s5)
 
 
.endarea ; 0x8003F29C - 0x8003F2D8
 
 
 
 
 
 
; Primary=Earth -- Secondary=Fire or Wind
; Changes identical to above (less one SB here for some reason...)
 
.org 0x8003EB38
.area 0x8003EB78-.
 
primEarth2:
 beqz $a0, loc_8003EB74
 lhu $v0, 0x3A($s3)
 lhu $v1, 0x3A($s2)
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003EB64
 nop
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003EC80
 sb $fp, 0x50($s2)
loc_8003EB64:
 sb $fp, 0x50($s3)
 sb $a0, 0x51($s2)
 j 0x8003EC80
 sb $s4, 0x45($s2)
loc_8003EB74:
 lbu $v0, 0x340($s5)
 
.endarea ; 0x8003EB38 - 0x8003EB78
 
 
 
 
 
 
 
; Primary=Lightning -- Secondary=Fire or Water
; Changes identical to above
 
.org 0x8003EC28
.area 0x8003EC70-.
 
primLightning2:
 beqz $a0, loc_8003EC6C
 lhu $v0, 0x3A($s3)
 lhu $v1, 0x3A($s2)
 nop
 sltu $v0, $v1
 bnez $v0, loc_8003EC58
 nop
 sb $s6, 0x52($s3)
 sb $a0, 0x51($s3)
 sb $s1, 0x45($s3)
 j 0x8003EC80
 sb $fp, 0x50($s2)
loc_8003EC58:
 sb $fp, 0x50($s3)
 sb $a0, 0x51($s2)
 sb $s6, 0x52($s2)
 j 0x8003EC80
 sb $s4, 0x45($s2)
loc_8003EC6C:
 lbu $v0, 0x340($s5)
 
.endarea ; 0x8003EC28 - 0x8003EC70
.close