SCI32: Fix QFG4 Ad Avis end-game bugs

Fixes bugs #10835, #10844, #10989
This commit is contained in:
sluicebox 2019-06-20 18:42:35 -07:00 committed by Filippos Karapetis
parent b9e29cedfc
commit a486438c10

View file

@ -11424,6 +11424,260 @@ static const uint16 qfg4DomovoiInnPatch[] = {
PATCH_END
};
// During the final battle with Ad Avis his initial timer is never stopped,
// reducing the intended time the player has to complete the sequence by more
// than half, and bringing Ad Avis back to life after he's killed.
//
// sTimeItOut state 0 sets a timeout when the battle starts. Its length depends
// on the detected cpu speed and game version. In the floppy versions this was
// a minimum of 400 seconds, which is so long that it masked the bug, but in CD
// it was reduced to 20 seconds. This is supposed to be how long the player has
// to tell the joke, after which sUltimakeJoke sets a second timeout in which
// the character-specific actions are to be done, but sTimeItOut finishes first
// and forces the player to complete both phases during the first shorter one.
// When Ad Avis is killed his death scripts only stop sUltimakeJoke, as they
// don't expect sTimeItOut to be running, and so when sTimeItOut times out it
// kills the player unless the death script has already called avis:dispose.
//
// We fix this by patching sTimeItOut state 1 to abort the script if the joke
// has been told. This is equivalent to the NRS patch that ships with the GOG
// version, which disposes sTimeItOut when telling the joke, and so this patch
// is applied to all versions except that one.
//
// Applies to: All versions
// Responsible method: sTimeItOut:changeState(1)
// Fixes bug: #10844
static const uint16 qfg4AdAvisTimeoutSignature[] = {
0x30, SIG_UINT16(0x002c), // bnt 002c [ state 1 ]
SIG_ADDTOOFFSET(+0x29),
SIG_MAGICDWORD,
0x32, SIG_UINT16(0x00ae), // jmp 00ae [ end of method ]
0x3c, // dup
0x35, 0x01, // ldi 01
0x1a, // eq?
0x30, SIG_UINT16(0x0027), // bnt 0027 [ state 2 ]
SIG_END
};
static const uint16 qfg4AdAvisTimeoutPatch[] = {
0x30, PATCH_UINT16(0x0029), // bnt 0029 [ state 1 ]
PATCH_ADDTOOFFSET(+0x29),
0x3c, // dup
0x35, 0x01, // ldi 01
0x1a, // eq?
0x31, 0x2b, // bnt 2b [ state 2 ]
0x83, 0x04, // lal 04 [ has joke been told? ]
0x2f, 0x27, // bt 27 [ abort script if joke has been told ]
PATCH_END
};
// During the final battle with Ad Avis if the player casts a non-fatal spell at
// him then they can't cast any more spells. Instead they receive a message
// about being too busy or it not being a good place and must wait to die.
// Another symptom of this bug is that fighters and thieves don't get to see
// the staff transform if they've previously thrown a weapon.
//
// SpellItem:doVerb(4) determines if a spell is allowed. If hero:view is the
// wrong value then it says "This isn't a good place..." and if the room has
// a script it says "You're too busy...". avis:getHurt breaks one or both of
// these conditions by setting the room script to sMessages. If the game speed
// is set to less than high then avis:getHurt runs while the "project" room
// script is animating hero. Setting the room script to sMessages interrupts
// this and hero is left on view 14, breaking the first spell condition. Even
// if the game speed is set to high and hero's animation completes, sMessages
// fails to dispose itself, leaving it as the room script when it's complete
// and breaking the second spell condition.
//
// We fix this by reassigning sMessages from the room's script to midBlast, an
// arbitrary Prop that no scripts depend on. project is no longer interrupted,
// hero's animation completes at all speeds, and it no longer matters that
// sMessage fails to dispose itself. Due to script changes, this patch is only
// applied once to floppy and twice to CD.
//
// We also include a version of this for the offsets in the NRS patch, which is
// important as that ships with the GOG version.
//
// Applies to: All versions
// Responsible method: avis:getHurt
// Fixes bug: #10835
static const uint16 qfg4AdAvisSpellsFloppySignature[] = {
SIG_MAGICDWORD,
0x72, SIG_UINT16(0x0096), // lofsa sMessages
0x36, // push
0x81, 0x02, // lag 02
0x4a, SIG_UINT16(0x0006), // send 06 [ rm730 setScript: sMessages ]
SIG_END
};
static const uint16 qfg4AdAvisSpellsFloppyPatch[] = {
0x74, PATCH_ADDTOOFFSET(+2), // lofss sMessages
0x72, PATCH_UINT16(0x0668), // lofsa midBlast
SIG_END
};
static const uint16 qfg4AdAvisSpellsCDSignature[] = {
SIG_MAGICDWORD,
0x72, SIG_UINT16(0x00a6), // lofsa sMessages
0x36, // push
0x81, 0x02, // lag 02
0x4a, SIG_UINT16(0x0006), // send 06 [ rm730 setScript: sMessages ]
SIG_END
};
static const uint16 qfg4AdAvisSpellsCDPatch[] = {
0x74, PATCH_ADDTOOFFSET(+2), // lofss sMessages
0x72, PATCH_UINT16(0x06b6), // lofsa midBlast
SIG_END
};
static const uint16 qfg4AdAvisSpellsNrsSignature[] = {
SIG_MAGICDWORD,
0x72, SIG_UINT16(0x00a8), // lofsa sMessages
0x36, // push
0x81, 0x02, // lag 02
0x4a, SIG_UINT16(0x0006), // send 06 [ rm730 setScript: sMessages ]
SIG_END
};
static const uint16 qfg4AdAvisSpellsNrsPatch[] = {
0x74, PATCH_ADDTOOFFSET(+2), // lofss sMessages
0x72, PATCH_UINT16(0x06b8), // lofsa midBlast
SIG_END
};
// If the magic user defeats Ad Avis with the game speed set to less than high
// then they aren't allowed to cast the final summon staff spell and complete
// the game. Instead they receive "You're too busy to cast a spell right now."
//
// Spells can't be cast if a room script is set, as described in the above patch
// notes. When Ad Avis is killed, avis:getHurt sets hero's view and loop before
// running sAdavisDies. If the game speed isn't set to high then the "project"
// script that deployed the final projectile spell is still running and waiting
// on hero's animation to complete. By changing the view and loop, avis:getHurt
// prevents project from advancing to its next state and completing, leaving it
// stuck as the room script and blocking the final spell.
//
// We can't prevent project from being the room script as that's game-wide
// behavior, and we can't prevent it from being interrupted since avis:getHurt
// needs to set hero's final view/loop, but we can still fix the bug by setting
// sAdavisDies as the room's script instead of hero's. This disposes project if
// it's still running and guarantees that the room script is cleared since
// sAdavisDies always disposes of itself.
//
// Applies to: All versions
// Responsible method: avis:getHurt
// Fixes bug: #10835
static const uint16 qfg4AdAdvisLastSpellSignature[] = {
0x38, SIG_SELECTOR16(setScript), // pushi setScript
0x78, // push1
0x72, SIG_ADDTOOFFSET(+2), // lofsa sAdavisDies
SIG_MAGICDWORD,
0x36, // push
0x81, 0x00, // lag 00
0x4a, SIG_UINT16(0x0006), // send 06 [ hero setScript: sAdavisDies ]
SIG_END
};
static const uint16 qfg4AdAdvisLastSpellPatch[] = {
PATCH_ADDTOOFFSET(+8),
0x81, 0x02, // lag 02 [ rm730 ]
SIG_END
};
// When throwing a weapon or casting a spell at Ad Avis in room 730, sMessages
// tests the projectile type incorrectly and transposes the message responses.
//
// Applies to: All versions
// Responsible method: sMessages:changeState(2)
// Fixes bug: #10989
static const uint16 qfg4AdAvisMessageSignature[] = {
0x8b, SIG_MAGICDWORD, 0x01, // lsl 01 [ 0 if weapon thrown, else a spell ]
0x35, 0x00, // ldi 00
0x1a, // eq?
SIG_END
};
static const uint16 qfg4AdAvisMessagePatch[] = {
PATCH_ADDTOOFFSET(+4),
0x1c, // ne?
PATCH_END
};
// Throwing a rock or dagger at Ad Avis after telling the joke kills him.
// avis:getHurt fails to test the projectile type correctly in CD, or at all in
// floppy, and so all versions mistake this for casting a spell with the staff.
//
// We fix this by testing the projectile type and not allowing a thrown weapon
// to kill Ad Avis. This replaces an unnecessary hero:script test.
//
// Applies to: All versions
// Responsible method: avis:getHurt
// Fixes bug: #10989
static const uint16 qfg4AdAvisThrowWeaponSignature[] = {
SIG_MAGICDWORD,
0x38, SIG_SELECTOR16(script), // pushi script
0x76, // push0
0x81, 0x00, // lag 00
0x4a, SIG_UINT16(0x0004), // send 04 [ hero script? ]
0x18, // not
0x30, SIG_ADDTOOFFSET(+2), // bnt [ projectile doesn't kill ad avis ]
0x39, SIG_SELECTOR8(view), // pushi view
SIG_END
};
static const uint16 qfg4AdAvisThrowWeaponPatch[] = {
0x83, 0x01, // lal 01 [ 0 if weapon thrown, else a spell ]
0x33, 0x06, // jmp 06 [ throwing a weapon doesn't kill ad avis ]
PATCH_END
};
// When a fighter or paladin selects the staff in the final battle with Ad Avis
// after throwing a rock or dagger they enter an infinite animation loop due to
// not clearing hero:cycler. Multiple bugs in this room prevented getting this
// far, but we fixed those, so we also fix this by clearing the cycler.
//
// Applies to: All versions
// Responsible method: sDoTheStaff:changeState(3)
// Fixes bug: #10835
static const uint16 qfg4FighterSpearSignature[] = {
0x39, SIG_SELECTOR8(view), // pushi view [ start of fighter code, same as paladin ]
SIG_ADDTOOFFSET(0x3e),
0x3c, // dup
0x35, SIG_MAGICDWORD, 0x03, // ldi 03
0x1a, // eq? [ is paladin? (last condition so always true) ]
0x30, SIG_UINT16(0x0022), // bnt 0022
0x39, SIG_SELECTOR8(view), // pushi view
0x78, // push1
0x39, 0x0a, // pushi 0a
0x38, SIG_SELECTOR16(setLoop), // pushi setLoop
0x7a, // push2
0x76, // push0
0x78, // push1
0x38, SIG_SELECTOR16(setCel), // pushi setCel
SIG_ADDTOOFFSET(+13),
0x4a, SIG_UINT16(0x001c), // send 1c [ hero view: 10 setLoop 0 1 setCel: 0 ... ]
SIG_END
};
static const uint16 qfg4FighterSpearPatch[] = {
0x33, 0x3e, // jmp 3e [ use patched paladin code for fighter ]
PATCH_ADDTOOFFSET(0x3e),
0x39, PATCH_SELECTOR8(view), // pushi view
0x78, // push1
0x39, 0x0a, // pushi 0a
0x38, PATCH_SELECTOR16(setLoop), // pushi setLoop
0x7a, // push2
0x76, // push0
0x78, // push1
0x38, PATCH_SELECTOR16(setCel), // pushi setCel
0x39, 0x01, // pushi 01
0x39, 0x00, // pushi 00
0x38, PATCH_SELECTOR16(setCycle), // pushi setCycle
PATCH_ADDTOOFFSET(+13),
0x4a, PATCH_UINT16(0x0022), // send 22 [ hero view: 10 setLoop 0 1 setCel: 0 setCycle: 0 ... ]
PATCH_END
};
// The script that determines how much money a revenant has is missing the first
// parameter to kRandom, which should be zero as it is with other monsters.
// Instead of awarding the intended 15 to 40 kopeks, it always awards 15 and
@ -11508,6 +11762,14 @@ static const SciScriptPatcherEntry qfg4Signatures[] = {
{ true, 710, "fix tentacle retraction for fighter", 1, qfg4PitRopeFighterSignature, qfg4PitRopeFighterPatch },
{ true, 710, "fix tentacle retraction for mage (1/2)", 1, qfg4PitRopeMageSignature1, qfg4PitRopeMagePatch1 },
{ true, 710, "fix tentacle retraction for mage (2/2)", 1, qfg4PitRopeMageSignature2, qfg4PitRopeMagePatch2 },
{ true, 730, "fix ad avis timeout", 1, qfg4AdAvisTimeoutSignature, qfg4AdAvisTimeoutPatch },
{ true, 730, "Floppy: fix casting spells at ad avis", 1, qfg4AdAvisSpellsFloppySignature, qfg4AdAvisSpellsFloppyPatch },
{ true, 730, "CD: fix casting spells at ad avis", 2, qfg4AdAvisSpellsCDSignature, qfg4AdAvisSpellsCDPatch },
{ true, 730, "NRS: fix casting spells at ad avis", 2, qfg4AdAvisSpellsNrsSignature, qfg4AdAvisSpellsNrsPatch },
{ true, 730, "fix casting last spell at ad avis", 1, qfg4AdAdvisLastSpellSignature, qfg4AdAdvisLastSpellPatch },
{ true, 730, "fix ad avis projectile message", 1, qfg4AdAvisMessageSignature, qfg4AdAvisMessagePatch },
{ true, 730, "fix throwing weapons at ad avis", 1, qfg4AdAvisThrowWeaponSignature,qfg4AdAvisThrowWeaponPatch },
{ true, 730, "fix fighter's spear animation", 1, qfg4FighterSpearSignature, qfg4FighterSpearPatch },
{ true, 800, "fix setScaler calls", 1, qfg4SetScalerSignature, qfg4SetScalerPatch },
{ true, 800, "fix grapnel removing hero's scaler", 1, qfg4RopeScalerSignature, qfg4RopeScalerPatch },
{ true, 801, "fix runes puzzle (1/2)", 1, qfg4RunesPuzzleSignature1, qfg4RunesPuzzlePatch1 },