Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCI32: Fix QFG4 cave tentacle #1463

Merged
merged 1 commit into from Jan 1, 2019
Merged

SCI32: Fix QFG4 cave tentacle #1463

merged 1 commit into from Jan 1, 2019

Conversation

Vhati
Copy link
Contributor

@Vhati Vhati commented Dec 27, 2018

Fixes wriggling and retraction when hero travels over the pit, bug #10615

Wriggle

// When crossing the cave's tightrope in room 710, a tentacle emerges, and then
// its animation freezes - in ScummVM, not the original interpreter. This
// happens because of an extraneous argument passed to setCycle().
//
// (tentacle setCycle: RandCycle tentacle)
//
// RandCycle can accept an optional arg, but it expects a number, not an
// object. ScummVM doesn't catch the faulty arithmetic and behaves abnormally.
// We remove the bad arg.

This supersedes an arithmetic workaround added in commit 259f262.

Fighter

// When crossing the cave's tightrope in room 710, a tentacle emerges. The
// tentacle is supposed to reach a state where it waits indefinitely for an
// external cue() to retract. If the speed slider is too high, a fighter
// reaches the other side and sends a cue() before the tentacle is ready to
// receive it. The tentacle never retracts.
//
// The fighter script (crossByHand) drains stamina in state 2 as hero moves
// across. A slower speed would cost extra stamina. We add a delay after that
// part is over, in state 3, just as hero is about to dismount. When state 4
// cues, the tentacle script (sTentacleDeath) will be ready (state 3).
//
// To create that delay we set the "cycles" property for a countdown and remove
// all other advancement mechanisms. State 3 had a cue from say() and a
// self-cue(). The former's "self" arg becomes null. The latter is erased.
//
// Crossing from the left (crossByHandLeft) doesn't require fixing.

Mage

// As above, mages at high speed can get across the pit in room 710 before
// tentacle is ready to receive a cue().
//
// As luck would have it, hero's speed is cached and restored. Twice actually.
//
// Overview of the mage script (sLevitateOverPit)
//  State 1-3:  Cache hero's slider-based speed.
//              Set a temporary speed as hero unfurls the cloth.
//              Restore the original value.
//              Call handsOn(). Wait for an external cue().
//  State 4:    Call handsOff().
//              If cued by the Levitate spell (script 21), go to 5-7.
//              A plain cue() from anywhere else, leads to state 8.
//  State 5-7:  Move across the pit. Skip to state 9.
//  State 8:    An abort message.
//  State 9-10: Cache hero's speed again.
//              Set a temporary speed as hero folds up the cloth.
//              Restore the original value, and normalize hero. Call handsOn().
//
// Patch 1: We overwrite some derelict code in state 5, caching the
// slider-based speed again, in case the player adjusted it before casting
// Levitate, then setting a fixed speed of our own for the crossing.
//
// Patch 2: Patch 1 already cached and clobbered the speed. We remove the
// original attempt to cache again in state 9.
//
// The result is caching/restoration at the beginning, aborting or caching and
// crossing with our fixed value, and a restoration at the end (whichever value
// was last cached). The added travel time has no side effect for mages.
//
// Mages have no other script to levitate across from left to right. At some
// point in development, the meaning of "register" changed. The derelict
// state 5 code thought 0/1 meant move right/left. Whereas state 4 decides 0/1
// means abort/cross, only ever moving left. The rightward MoveTo never runs.

 

To test the room at the beginning of the game...

  • Teleport to the pit.
    • room 710
  • Fighter
    • Click HAND on the rope or stalagmite.
    • Choose "Cross hand-over-hand".
  • Mage
    • Acquire the cloth item.
      • send hero get 35
    • Click the cloth on hero.
    • Bring up the spells list (star). Cast Levitate (2nd row, 5th icon).
  • Rogue
    • Rogues didn't need patching.
    • Click HAND on the rope, and choose "Walk the tightrope".
    • They have scripts for both directions.

 

  • Fighters and rogues get a nag when stamina runs out, but they can continue crossing back and forth.
  • For mages, it's a one-way trip. The debugger can place them back on the east side.
    • send hero posn 265 46

 

As I experimented with different patches, I found I got different results after playing through the first two rooms to get there naturally (or restored a natural savegame). And medium speeds could be symptomatic when fast and slow were fine. I kept adjusting the patches' delay/speed value until I got consistent results.

 

To test the room at the end of the game...

  • Create a new mage character.
  • Set the debug flag.
    • vv g 201 1
  • Acquire the cloth.
    • send hero get 35
  • Teleport to the pit.
    • room 710
  • The pit room will fake an endgame situation, setting flag 101 and spoofing the previous room number.
  • There is no tentacle at this point. Instead there's grub-like monster that needs to be pacified.
    • Cast Calm from the spell list (2nd row, 1st icon).

You can then levitate down & up, standing on either side of the pit floor.

Clicking the cloth on hero only works if standing on the upper right.
Casting Levitate while the cloth is unfurled sends hero left.

Using the cloth at any other position yields a generic message, accomplishing nothing.
"You spread out the square of cloth for a moment."

@Vhati Vhati force-pushed the qfg4_tentacle branch 5 times, most recently from dc48fe2 to 8d84f34 Compare December 27, 2018 07:17
@Vhati
Copy link
Contributor Author

Vhati commented Dec 27, 2018

Grr. Separate caching wasn't redundant. After a mage player uses the cloth, the handsOn() allows them to adjust the slider to an inappropriate speed before casting levitate.

Not an obvious exploit or a dangerous one. It'd just cause hero to outpace the tentacle and get it stuck in an edge case.

@Vhati
Copy link
Contributor Author

Vhati commented Dec 27, 2018

Fixed. The patch sets its own speed for crossing after handsOff().

Caching takes place when the cloth is used.
If the speed slider is adjusted between then and Levitate, the new value will be discarded.
After crossing, hero's speed will be whatever it was when the cloth was used.

@Vhati
Copy link
Contributor Author

Vhati commented Dec 27, 2018

*sigh* Bytes were available,..

Caching takes place when the cloth is used AND just before crossing.
If the speed slider is adjusted between using the cloth and Levitate, the new value will be preserved.
After crossing, hero's speed will be whatever was last cached.

Technically if the slider is adjusted between using the cloth and an abort... the second cache in state 5 is skipped, and the new value will be discarded, restoring the original speed instead.

I think I've sharpened that edge case enough. =)

@Vhati Vhati force-pushed the qfg4_tentacle branch 2 times, most recently from 0f99b7d to 616299e Compare December 28, 2018 10:37
@Vhati
Copy link
Contributor Author

Vhati commented Dec 29, 2018

@bluegr:

// Mages have no other script to levitate across from left to right.

Are you sure about this statement?

Yes.

Vertical levitation scripts exist, but they do not carry hero across from left to right. With those, hero levitates down to the pit floor, manually walks to the other side, then levitates up via a separate casting.

From the wiki

  • In the opening of the game, Levitate is used, along with the Sheet, to cross the pit leading out of the Dark One's Cave.

Fighters and rogues each have a second script (crossByHandLeft, tightRopeLeft) to travel *horizontally* over the pit in the opposite direction, from the left side.

Mages do not.

  • hero::doVerb(cloth) explicitly requires standing on the upper right side before it schedules sLevitateOverPit.
  • sLevitateOverPit is a requirement for the Levitate spell to set "register" and cue the script.
  • sLevitateOverPit::changeState(4) advances to state 5 if register == 1.
    • Otherwise, it skips to an abort state (8).
  • sLevitateOverPit::changeState(5) travels leftward when register == 1.
    • If state 5 were somehow reached with register == 0, it would go right. State 4 never allows this.
  • I've quoted some of that code over at the ticket.

When sLevitateOverPit starts, it advances through states 0-3 then does handsOn().
State 4 does handsOff() and cleans up after state 3. It is a necessary step.

script 710 - sLevitateOverPit::changeState()

(3
	3
	(g0_hero cycleSpeed: local2 setLoop: 1 1 setCycle: Fwd)
	# Add sLevitateOverPit to EventHandler Lists.
	(g72_keyDownHandler addToFront: self)
	(g73_mouseDownHandler addToFront: self)
	(global1 handsOn:)
)
(4
	4
	(g1_Glory handsOff:)
	(g72_keyDownHandler delete: self)
	(g73_mouseDownHandler delete: self)
	(if register (= cycles 1) else (self changeState: 8))
)

So skipping directly to state 5 with register=0 set for rightward movement (from the left side) should never happen.

 

script 710 - sLevitateOverPit::handleEvent()

(method (handleEvent param1)
	(if
	(and (== state 3) (proc64999_5 (param1 type?) 4 32 1))
		(if (== (param1 message?) 89)
			# 89 is a message from Levitate. More on that later.
			(= register 1)
		else
			(= register 0)
		)
		(self cue:)
		(param1 claimed: 1)
		(return)
	else
		(super handleEvent: param1)
	)
)

sLevitateOverPit only responds to events in state 3, when the cloth has been unfurled.
It checks for Levitate's 89 message, setting register=1, so state 4 will advance and cause leftward movement.
Any other event sets register=0, causing state 4 to abort.

 

@bluegr

There are about 4-5 places where levitate can be used
[...]
Are you sure that this patch won’t affect these locations?

Levitate delegates its effects to scripts within each room.
Modifying one room's Levitate effect will not affect any other locations.

Room 710 does schedule other levitation scripts, distinct from the horizontal crossing.
Modifying sLevitateOverPit will not affect them either.

 

From the wiki

  • After returning to the Dark One's Cave with the Rituals, Levitate will get the Hero down to the bottom of the pit to where the Pit Horror is, as well as up the other side.

script 21 - levitateSpell::doVerb(4)

(cond 
	((or (== g11_myCurrentRoomNum 750) (== g11_myCurrentRoomNum 340))
			(g2_myCurrentRoom notify: 89)
	)
	(
		(and
			(== g11_myCurrentRoomNum 710)
			(== (g0_hero script?) (ScriptID 710 1))  # sLevitateOverPit
			(== ((g0_hero script?) state?) 3)
		)
		((g0_hero script?) register: 1)
		((g0_hero script?) cue:)
		# Cross leftward horizontally.
	)
	((super doVerb: 4)  # Spells ask super if spellcasting is possible.
		(if
			# Test if param1 is found among candidates param2 ... paramN.
			(proc64999_5
				g11_myCurrentRoomNum
				# Lots of room numbers...
				710
				# Lots of room numbers...
			)
			((= temp0 (Event new:)) type: 1 message: 89)
			(if (not (g73_mouseDownHandler handleEvent: temp0))
				(g6_regions handleEvent: temp0)
			)
			(temp0 dispose:)
			(return 1)
		else
			# Show one of these messsages.
			#   "This isn't a good place to use that spell."
			#   "This is definitely not an appropriate time
			#     to cast your Levitate spell."
			#   "Now is not a good time to cast your Levitate spell."
# ...
		)
	)
	(else (return 1))
)

If not specifically using the cloth to float leftward, g73_mouseDownHandler and g6_regions get sent event 89.

regions is an EventHandler List with one element, rm710. (ancestry: Rgn/Room/GloryRm)
In script 64994, Rgn::handleEvent() translates that to rm710::doVerb(89).

 

script 710 - rm710::doVerb(89)

(cond 
	((proc0_4 101)  # Test an endgame plot flag set at the castle gate (600) or cave entrance (790).
		(cond 
			((and (> (g0_hero x?) 190) (< (g0_hero y?) 125))    # Standing at upper right.
				(g2_myCurrentRoom setScript: (ScriptID 711 4))  # sLeviDownRt
			)
			((and (< (g0_hero x?) 190) (< (g0_hero y?) 125))    # Standing at upper left.
				(g2_myCurrentRoom setScript: (ScriptID 711 2))  # sLeviDown
			)
			((and (not (< (global0 y?) 125)) (> (global0 x?) 160))  # Standing at lower right.
				(g2_myCurrentRoom setScript: (ScriptID 711 5))      # sLeviUpRt
			)
			(else                                               # Standing at lower left
				(global2 setScript: (ScriptID 711 3))  # sLeviUp
			)
		)
	)
	((and (> (global0 x?) 190) (< (global0 y?) 125))  # Start of game, standing at upper right.
		# leviCode, generic hover, walking to (222,45) first.
		((ScriptID 31 0) init: 222 45 30 0 2)
	)
	(else                                             # Start of game, standing at upper left.
		# leviCode, generic hover, walking to (119,79) first.
		((ScriptID 31 0) init: 119 79 35 0 2)
	)
)

None of those scripts reference sLevitateOverPit. They send hero up and down independently.

Fixes wriggling and retraction when hero travels over the pit, bug #10615
Supersedes commit 259f262
@Vhati
Copy link
Contributor Author

Vhati commented Dec 29, 2018

I've added instructions in the OP to test the room under endgame conditions - when the pit floor is accessible, along with all those other vertical Levitate effects.

@bluegr
Copy link
Member

bluegr commented Jan 1, 2019

Great work, well done! :)

Merging

@bluegr bluegr merged commit aa9a1ab into scummvm:master Jan 1, 2019
@Vhati Vhati deleted the qfg4_tentacle branch January 3, 2019 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants