The most obscure bug fix yet

After finishing the lighting task, I had some time to focus on fixing bugs before moving on to my after mid-term tasks.

One of my GSoC mentors, chkr brought up a bug regarding the candles in Governor’s mansion. In ResidualVM, the candle sprites didn’t show up at all. Look at the chandelier in the screenshots below to see the difference. ResidualVM on the left, original on the right.

This seemed simple enough to quickly fix before moving on to other tasks I thought, so I got to debugging.

When looking at the original Lua code that sets up the candles, my first observation was that the candles were first being attached to the chandelier actor and their position was set afterwards. Perhaps something was going wrong with the offsetting of attached actors even after JoseJX’s recent fixes? This turned out to be a false lead when I noticed that no calls were being made to sprite drawing functions at all for the candle sprites. Even if the sprites were positioned out of view, the current ResidualVM implementation should attempt to draw them. Later it turned out that the candle actors were in fact in the correct positions.

In the EMI engine, sprites are components of chores. A chore is basically a track of instructions for the components that are executed in sequence. Only the components of active, playing chores are updated and drawn. Chores are components of costumes, which in turn are components of an actor. I found out the candle actors and their costumes were present like they should be, but the costumes were not playing any chores with sprite components, so no sprites were drawn.

This seemed weird to me. The Lua code is supposed to start the “flame12fps” chore for the candles, but the call to start the chore was never made to ResidualVM. The next step was to figure out why.

The original Lua only starts the chore in the case if no chores are already playing. But surely there shouldn’t be any chores playing if the actor was just created? Well, it turned out that when the game is run in ResidualVM, there is.

By adding a breakpoint to the IsChorePlaying engine opcode that the check in Lua calls for each chore, I found out that the opcode returns true because the “wear_default” chore of the costume “dumbshadow” is playing. Because of this, the sprite animation is never started.

In the original engine the chore is not playing. This is easy to check by running custom Lua code with Ctrl+E after enabling the hidden debug functions in the game (I could explain how to do this in a later blog post). If the chore is forced to play, IsChorePlaying will also return true in the original engine, so there is no special handling in the engine to always return false for this particular chore.

Ok, so what is the “wear_default” chore of “dumbshadow” and why is it started then? The “dumbshadow” costume is used to produce fake shadow effects such as the ones that can be seen under Guybrush and Elaine in the above screenshot of the original game. The shadows currently don’t show up in ResidualVM, but even if they did, we probably wouldn’t want the candles to have shadows. Thus it seems likely that the correct behavior would be to not activate the shadow, like the original engine seems to do.

The Lua code that starts the shadow chore can be found in _actors.lua. The chore is started as soon as the actor is put in a set, if certain conditions are fulfilled. Among these conditions is one peculiar one. If the opcode GetActorPuckVector returns nil, the shadow won’t be activated. When GetActorPuckVector is run on the candles in the original engine, the returned value is nil. In ResidualVM the opcode always returns a non-nil value.

Finally we’re getting to the gist of the bug, after seemingly drifting so far. GetActorPuckVector is an opcode used to get the forward vector of an actor projected to the actor’s current sector (or walkplane) to be used for movement. It appears that in the original engine the GetActorPuckVector opcode always return nil until a call is made to SetActorFollowBoxes, which constrains the actor’s movement to the walkable sectors. Most likely the purpose of the nil-check is to only activate shadows for actors that are constrained to walkable sectors.

Replicating this behavior in ResidualVM’s implementation of GetActorPuckVector fixes the candles, as expected. Phew!