zarayon wrote: Thu Apr 13, 2023 7:24 pm
I believe that some players don't want to reveal this kind of glitch and I respect that.
Well, what are the reasons they don't want to reveal it? It seems strange to want to keep something like this secret, since if it's been discovered once it can be discovered again, but I'd like to hear both sides of the argument…
–––
Anyway, having
resolved the mystery of the disappearing Shell in
Rodeo_40 (i.e. Menhir Hills 2), I've decided to turn my attention to another glitch that was discovered in the same segment: the so-called "Ultimate Rayman 2 Glitch", aka
Unlimited Flying. I started out looking at the decompiled bytecode in Raymap, but soon it was wrecking my head so much that I downloaded the
CPA Editor and started looking at the "MODEL ANALYSIS" HTML files in there. (In fact, the HTML is so malformed that I had to dust off my Windows 7 VM and force IE11 to load them in "Compatibility View" to make them fully readable

)
There are two components to this glitch, although it was later shown that
only the second one is necessary:

When Rayman catches a barrel after being hit by a Pirate's shots, it takes off in flight,
but only if a barrel from the same dispenser has already been flown.

When Rayman's flying barrel blows up during his damage invulnerability phase, he stays flying with a phantom barrel.
Before going into the details of these components, I think the first thing to note is that the barrel generator does not actually create a new barrel when the old one is destroyed. It merely
teleports the old one back to its starting location and reactivates it. This can be seen in its behaviour function
SUN_Trap_Normal:
Code: Select all
Si
(Non(ACT_estactif(DES_Tonneau)) et
ObjetValide(DES_Tonneau))
Alors
ACT_FixepositionPerso(DES_Tonneau, Pos_Tonneau)
DES_Tonneau.ACT_ChangePositionAbsoluePerso(Pos_Tonneau)
DES_Tonneau.ACT_ChangeViseePerso(ACT_ViseePerso())
DES_Tonneau.ChangeAction("tonneau_boum@MIC_Tono_Inerte")
ChangeComportementreflexe(DES_Tonneau, "DS1_ReflexeTonneauParTerre")
ChangeComportement(DES_Tonneau, "MIC_Init_Generateur")
ActiveObjet(DES_Tonneau)
Executeaction("OLD_cage_mur_paf")
Changeaction("OLD_cage_mur_Close")
Finsi
It detects if its barrel (
DES_Tonneau) exists but is inactive, and if so, it moves the barrel back to its original position (
Pos_tonneau), resets its behaviour and reflex functions, and reactivates it. This means that the barrel is never actually destroyed, and it can (theoretically) remember everything that happened to any other barrel from the same generator.
–––
So, how does this lead to the two components of the Ultimate Glitch? Well, let me start with the second component, since it's easier to explain and is the only one really necessary for the desired outcome (infinite flying). I think the Wiki already explains it quite well:
RayWiki wrote:The reason this happens is because when a keg is destroyed in mid-flight, the keg object is removed and damage is dealt to Rayman which puts him in the "damage" state. However, if the keg is destroyed while Rayman is invulnerable, the keg object is still removed but Rayman is kept in the "flying" state.
Indeed, a cursory scan of Rayman's reflex function
BNT_TonneauFuséeReflexe shows that this is exactly what happens:
Code: Select all
; on verifie Si on touche quelque chose qui nous fait mal
BNT_RaymanEstIlImprudentEnLair
;Rayman en phase de PAF
BNT_RaymanEstIlInvulnerable
;Teste Si Rayman a ete pafe
BNT_RaymanEstIlPafe
; on teste les collisions, sans tenir compte des collisions contre le tonneau qui nous propulse...
Si2
Col_CollisionAvecSol()
Ou
Col_CollisionAvecMur()
Ou Col_CollisionAvecPlafond()
Alors
INTERN_TmpVector := COL_VecteurNormalCollision()
Si ( VEC_VecteurCos( ACT_ViseePerso(), INTERN_TmpVector ) < -0.5 )
Alors
Si ObjetValide(persogenerated)
Alors
PersoGenerated.ACT_ChangeCustomBit(31, Vrai)
FinSi
Sinon ; on rebondit contre la face touchee
ACT_ReorientePerso(
COL_CalculVecteurRebond(ACT_ViseePerso(), INTERN_TmpVector, 1.0),
Vecteur(0,0,0),
0 ; impose Y, propose Z inchange
FinSi
FinSi
The only place in this code where Rayman can be snapped out of the "flying barrel" state is in the macro
BNT_RaymanEstIlPafé, which does nothing if Rayman is in his invulnerable phase:
Code: Select all
;===================================================================
; on verifie Si RM a recu un message de collision
Si2 COL_CollisionneurTouche()
Alors
; qd RM est deja en paf, on ne prend en compte que les pafs mortels
; Si RM n'est pas en paf, on les prend tous
SiPas ( ? RAY_RayIsHit Et ( COL_LitTypeDeCollisionneur() < PAF_ValeurPremierPafMortel ) )
Alors
RAY_RayIsHit := Vrai
; on passe en comportement PAF, Et on stoppe tout reflexe residuel!
ChangeMonComportementEtMonReflexe("CHG_RaymanPaf", "YLT_ReinitReflexe")
FinSi
FinSi
The boolean
RAY_RayIsHit indicates invulnerability, and if it is set then the content of the inner if statement (
SiPas) does not get executed, so Rayman's behaviour and reflex functions do not get changed. That is to say, he stays in the "flying barrel" state. And that's that, but there are a few other interesting things to note here.
The other thing that happens in that reflex is that Rayman checks if he's bashed into the ground, or wall, or ceiling. If so, it sets Custom Bit 31 on
PersoGenerated, which is the (somewhat misleading) name for the pointer to the barrel he's carrying. Custom Bit 31 in turn instructs the barrel to explode and injure Rayman. But again, that injury only works if he's not invulnerable.
Note also that there's a null-pointer check (
ObjetValide) before setting Custom Bit 31, but even this doesn't do anything, because when the glitch occurs,
PersoGenerated never gets reset to null! This means that Rayman still thinks he's holding the barrel, even though it's been teleported back to its generator and is sitting on the ground! As a result, he actually sets Custom Bit 31 on it whenever he crashes into anything in his infinite-flying state, but in its "on ground" state it doesn't react to that in any way…
–––
Right, that's the straightforward bit over! The other aspect of the glitch, where getting shot by a pirate can ignite the barrel, is more intriguing, and seems to involve a confluence of quite a few questionable decisions in the code… First, it's useful to discuss how the game handles collisions between objects that can hurt each other.
There are three functions that can be called on an object to tell it "you've been hurt". Each one takes a priority and a parameter. The collision is only registered if the object hasn't been hit by something with a higher priority since it last checked. The functions are
COL_ChangeTypeDeCollisionneur,
COL_ChangeVecteurDeCollisionneur and
COL_ChangeRéelDeCollisionneur, which tell the object, respectively: what hit it, what direction to get knocked back, and how much damage to take (a real number, hence "
Réel"). Any one of these will register a collision, which the hit object can then detect by calling
COL_CollisionneurTouché. This latter function returns "True" if the last registered collision priority is non-zero, and then it resets the priority to allow for the next collision.
After establishing that it has been hit, an object can query the data passed by the three functions above, using the inverse functions
COL_LitTypeDeCollisionneur,
COL_LitVecteurDeCollisionneur and
COL_LitRéelDeCollisionneur.
However, there is no guarantee that all three pieces of information were set by the most recent collision! Remember,
only one of them needs to be set in order to register that a collision has taken place.
This turns out to be the basic reason that the barrel can get ignited by something other than a flame, but
only after being ignited by a flame first. When Rayman holds a barrel near a
BNT_EnferDeDante (yes, that's what they called the flames!), it calls all three functions, including setting the collider type to 3, i.e.
PAF_Feu. Then, later, if Rayman gets injured while carrying a barrel, his behaviour function
CHG_RaymanPaf executes, including this little nugget:
Code: Select all
;-------------------------------------------------------------------
;Special Mode Tonneau: il faut le lacher
;-------------------------------------------------------------------
Si (((Ray_Etat = Etat_TonneauPorte) Ou (Ray_Etat = Etat_TonneauFusee)) Ou (Ray_Etat = Etat_TonneauLance))
Alors
Si ObjetValide(PersoGenerated)
Alors
PersoGenerated.COL_ChangeVecteurDeCollisionneur(255, 0, Vecteur(0,2,6))
PersoGenerated.ACT_ChangeCustomBit(8,Vrai)
FinSi
FinSi
Rayman calls
COL_ChangeVecteurDeCollisionneur on the barrel, to tell it to get thrown backward (Y = +2) and up (Z = +6). However, he doesn't call either of the other functions, so when the barrel registers that it has been hit, it still remembers that the last thing that hit it was
PAF_Feu, and acts accordingly! Again, it remembers this because when the previous barrel got destroyed, it really just teleported back to its spawn location, and so retained all its data.
But, that's not the whole story. Immediately after telling the barrel it's been "hit", Rayman sets Custom Bit 8, which ordinarily should make it ignore that "collision", as per its reflex action
BNT_ReflexeTonneauRamassé:
Code: Select all
Si
ACT_TestCustomBit(8) ; des qu"on est a nouveau autonome
Alors
Si
COL_CollisionneurTouche() ; on annule le message de collision
Alors
NeRienFaire()
FinSi
; on prend une meca qui a une gravite
Changeaction("BNT_TonneauEnChuteLibre")
; je me remets bien droit, en facant comme RM
ACT_ChangeViseePerso("Rayman".ACT_ViseePerso())
; ACT_ReorientePerso(vecteur(0,0,1), 'rayman'.ACT_ViseePerso(), 2)
; RM a mis la direction de tir ici
memo_vect := VEC_VecteurRelatif(COL_LitVecteurDeCollisionneur(0))
Si
(memo_vect.y < > 0)
Alors
memo_vect *= 0.6
memo_vect.z *= 2
pTmpPerso :=Act_CibleLaPlusProche(32,20,2,1,ACT_ViseePerso())
Si
((("rayman".Ray_etat < > etat_paf) et
ObjetValide(pTmpPerso)) et
("Rayman".ACT_DistanceAuPerso(pTmpPerso) < 30))
Alors
; on determine l'impulsion pour parcourir une parabole qui nous
; fait retomber sur le perso detecte
memo_vect := pTmpPerso.Position
memo_vect := VEC_VecteurRelatif(MEC_ImpulsionSaut(Position, memo_vect, 3.0))
; pas d'intertie, sinon le calcul est faux
MEC_FixeInertie(1,1,1)
Sinon
pTmpPerso := moi
FinSi
Sinon
Act_ChangeCustomBit(9,Vrai)
FinSi
; on fait en sorte que la limite de vitesse ne gene pas le lancer
rTempReel1 := VEC_VecteurNorme(memo_vect)
MEC_FixeVitesseMax(vecteur(rTempReel1, rTempReel1, -1))
; on s'envole vers la cible
ACT_ImposeVecteurVitesse(memo_vect)
iTempsAvantExplosionEnVol := TEMPS_Obtenir()
rTempReel1 := Math_ReelAuHasard(-10,10)
Temps_AttenteFigee(1)
;MEC_ActiveCollision(Vrai)
ChangeMonComportementEtMonReflexe("BNT_EnChuteLibre","BNT_ReflexeTonneauRamassable")
FinSi
Note that, if Custom Bit 8 is set, indicating that the barrel is no longer held, it immediately calls
COL_CollisionneurTouche, specifically for the purpose of "cancelling the collision message".
But, if Rayman only gets injured
after throwing the barrel in the air, then this reflex doesn't get called, because the barrel's reflex function has already been switched to
BNT_ReflexeTonneauRamassable. Instead, the barrel retains the collision message indefinitely. Note that this collision message gets sent despite the barrel being in the air, because again
PersoGenerated doesn't get reset to null.
If Rayman manages to catch the barrel again while in his injured state, then is behaviour function switches back to
BNT_TonneauPorté, which looks like this:
Code: Select all
; on est ramasse, donc on ne fait rien, ou presque
MIC_MacroSuitLePorteur
; on detecte si une flamme nous a touche pour partir en mode fusee
Si
COL_CollisionneurTouche()
Alors
Si
("rayman".Ray_Etat < >Etat_TonneauLance)
alors
;si une flamme a touche la meche
Si
( COL_LitTypeDeCollisionneur() = PAF_Feu )
Alors
EnvoieRequeteSon("snd_Play_alumton_EXPLOS")
; Si
; ? DES_TypeDeMusique
; Alors
; Si
; (DES_TypeDeMusique = 1)
; Alors
; EnvoieRequeteMusique('SND_Play_CaskTH1_CaskTH')
; Sinon
; Si
; (DES_TypeDeMusique = 2)
; Alors
; EnvoieRequeteMusique('SND_Play_CaskTH2_CaskTH')
; Sinon
; Si
; (DES_TypeDeMusique = 3)
; Alors
; EnvoieRequeteMusique('SND_Play_CaskTH3_CaskTH')
; Sinon
; Si
; (DES_TypeDeMusique = 4)
; Alors
; EnvoieRequeteMusique('SND_Play_CaskTH4_CaskTH')
; Sinon
; Si
; (DES_TypeDeMusique = 5)
; Alors
; EnvoieRequeteMusique('SND_Play_CaskTH5_CaskTH')
; Sinon
; Si
; (DES_TypeDeMusique = 6)
; Alors
; EnvoieRequeteMusique('SND_Play_CaskTH6_CaskTH')
; Sinon
; Si
; (DES_TypeDeMusique = 7)
; Alors
; EnvoieRequeteMusique('SND_Play_CaskTH7_CaskTH')
; FinSi
; FinSi
; FinSi
; FinSi
; FinSi
; FinSi
; FinSi
; FinSi
pTmpPerso := genereObjet("ALW_texteMenu", position)
Si
ALW_estaMoi(pTmpPerso)
Alors
changecomportement(pTmpPerso, "STN_texte")
pTmpPerso."CHG_TexteMenu"@trans := 1
Finsi
; EnvoieRequeteSon('SND_Play_TONNFEU_feu')
; orientation du tonneau comme on veut
Si Reseau_WaypointValide(Reseau_WpLePlusProche(DES_ReseauStarter,0))
Alors
"Rayman".ACT_ChangeViseequelconquePerso(Reseau_PositionAbsolueWP(Reseau_WpLePlusProche(DES_ReseauStarter,0)) - Position)
TEMPS_AttenteFigee(1)
rTempReel1 := 1
Sinon
rTempReel1 := 0
FinSi
; rayman ziva!!!
ChangeComportementReflexe("rayman", "BNT_TonneauFuseeReflexe")
ChangeComportement("rayman", "YLT_TonneauFuseeComport")
"rayman".ChangeAction("YLT_TonneauRoket")
"rayman".INTERN_Saut_VX := 0
; et moi ziva aussi!!!
iTempsAvantExplosionEnVol := DES_DureeAvantExplosionEnVol
ChangeAction("BNT_AFondLesManettes")
ChangeMonComportementEtMonReflexe("BNT_TonneauDragsterAerien","BNT_ReflexeTonneauFusee")
FinSi
Finsi
FinSi
One of the first things it does is call
COL_CollisionneurTouche, which returns "True", since the barrel still has the collision message from when Rayman got injured a moment ago! It then checks if the collider type is
PAF_Feu, which is also true, because nobody has reset that since it got ignited by the flame! So, everything is good, and it starts the music, has Rayman shout "GO!" and puts itself and Rayman in flying-barrel mode! (Note that all the music-related lines are commented out since this came from the CPA Editor Community Edition (based on Rayman 4 DS), but you can see in Raymap that they are all active in Rayman 2 itself.)
Honestly, I'm not sure why the barrel then explodes immediately. Seemingly, Custom Bit 31 does get set, so presumably Rayman thinks he's smashed into the floor (since he's standing on it and walking forward) immediately after the barrel ignites.
EDIT: Actually, rereading the code, I've realized that when the barrel ignites, it also orients Rayman towards the nearest "starter" WayPoint, which in this case is below the floor beside the flame. So of course he crashes into the floor!
So, to recap, here's the sequence:

By taking a barrel from the dispenser for an initial "test flight", you ensure the flame sends it a collision message, setting the collider type to
PAF_Feu.

When the dispenser "respawns" the barrel, it is in fact teleporting and reactivating the original one, ensuring that the collider type information is remembered by the "new" barrel.
(Questionable Programming Choice #1)

When you throw the barrel in the air in front of the Pirate, you put it in "freefall" mode, which means it won't immediately react to any new collision messages (Custom Bit 8 also gets set).
But this does
not unset Rayman's (misleadingly-named)
PersoGenerated pointer, so he is still linked to the barrel!
(Questionable Programming Choice #2)

When Rayman gets injured by the Pirate's shot, he sends a collision message to the freefalling barrel, which does not check for or react to it. This collision message
only sets the vector,
not the collider type!

Rayman's injury also sets Custom Bit 8 on the barrel, which has no effect since it was already set when you threw it in the air.

When Rayman catches the barrel, Custom Bit 8 gets unset and it goes back into "carried" mode, in which it begins checking for collision messages again.

The barrel immediately sees the collision message that Rayman sent when he got injured. It checks the collider type, which is still
PAF_Feu because nothing updated it since the flame's message earlier.
(Questionable Programming Choice #3)

Since the barrel believes all conditions have been met, it ignites and Rayman shouts "GO!"

Attempting to reach a WayPoint in the alcove below, Rayman immediately collides with the floor, setting Custom Bit 31 on the barrel and causing it to explode.

This explosion doesn't stop the flight since Rayman is still in damage-invulnerability mode after getting shot by the Pirate, as explained above.

Rayman can fly indefinitely!
Once again, figuring out this sequence took a lot of work, using a combination of GDB and Raymap. After this I feel maybe I should write a set of GDB scripts specifically for Rayman 2 (so you could set breakpoints in the scripts and put watchpoints on the game objects and their properties, and things like that). But I don't know where I'd find the time to make them robust and polished…
Anyway, I hope you also found this interesting! And perhaps somebody else had already worked it out, in which case I apologize for reinventing the wheel
EDIT: A corollary of this seems to be that if you throw the barrel in the air, get injured, and let it fall, then pick up a "new" barrel from the dispenser, it will immediately ignite when you pick it up. I think this actually happened to me at one point during the investigation, but I couldn't be sure that it's for the reason I'm now thinking…