Posted: 19 Nov 2022
A set of very specific circumstances leads to a very specific, already obscure line of dialogue becoming even more obscure.
Recently, I was made aware of this video, which shows off a very obscure line of dialogue for using the watering can on the black sedans in The Milkman Conspiracy. But the video also makes note that there’s actually a bug with this line of dialogue
The video explains in a note that if you reload the level, the line will no longer play, and Raz will instead just play his normal watering can line. Pretty mysterious, and very odd. How could I resist the chance to debug it?
So, I tested it myself to make sure it was reproducible. Sure enough, if you get the watering can and then use it on a sedan, Raz has a special line. If you then reload the level (i.e if you pass through any loading screen and then come back), he just speaks his default line.
I found this to be a fascinating bug and I immediately set out to see if I could find the cause. The first order of business before doing any coding is to collect as much information as we can about the mechanisms involved.
In this case, those mechanisms are, from most likely to least likely (in my mind) to be the point of failure:
Starting at the Sedan, it’s not very complex. Interaction is handled by the function
onItem which takes the name of the item that is being used on the sedan. It just runs a simple check and plays the appropriate dialogue for the item in question.
Now this already had me forming theories - the watering can’s name could somehow be different after a reload. It wouldn’t be the only case where naming is an issue - Boyd has dialogue for the hedge trimmers that goes unused because his script is looking for the wrong item name.
It is very, very easy to check for this issue though. I whipped up a quick and dirty mod that modifies the
onItem function of the Sedans to print which item is being used on them. No dice though, the Sedan is registering the right item and even hits the code to play the line.
So, the plot thickens. It’s unlikely the Sedan code is the issue. I take a quick detour to the watering can’s handler for being activated & it raises further questions. It checks if the thing Raz is looking at is a GMan, and if not, has him say his default line. Huh?
Out of curiosity, I checked the code for the other Milkman props. Sure enough, the others will check if Raz is looking at anything before firing off their unique line. The watering can is the only one that checks for GMen specifically. That’s quite suspect.
It’s also quite confusing but before I did anything else though, I headed over to check Raz’s interaction logic as well to see if it can answer any questions. Here’s that function in its entirety, let’s run through it.
That first check is just there so that if Raz is holding an item that is marked as only being useable when he’s on the ground, he can’t use it in the air. It’s not important to this issue so we can ignore it.
Next, the game checks if Raz is looking at something and if he is it starts running the interaction logic for it. First, invis shuts off. Then the game checks if Raz is holding the Psycho-Portal and employs a fairly dirty hack to handle using it.
That hack exists because jumping into minds was originally handled by an ability called “Basic Braining”, and this code ensures the Psycho-Portal will work with old code that was set up for that ability. Interesting, but still not important to this issue.
If Raz is not holding the Psycho-Portal, we do the normal interaction logic. The game checks Raz isn’t holding an item, or if he is, it checks if
bUseOnly (a flag that stops items from sending interaction messages to other objects when used) is set.
If Raz is empty-handed, or the item he’s using is allowed to be used on other interactibles, then the game will send the message to the look target to tell it that it was just interacted with and what item was used on it, if any.
A quick explanation of messages, they allow scripts to communicate with each other. You define a message in your script with a function named something like
onMyMessage and then that function will be called if another script sends the
MyMessage message to yours.
This is how that
onItem function works - Raz sends the
Item message to his look target which then passes the information sent to the
onItem function if it exists on the target. If it doesn’t exist, nothing happens.
Alright so that all makes sense - in our scenario, Raz is holding the watering can and looking at the Sedan. It doesn’t have
bUseOnly set, so the game sends a message to the Sedan telling it that the watering can was used on it. Presumably it then has Raz say his line, right?
But clearly there is more to this. If we look at the rest of the code, we see that further down, outside of the check for a look target, is the code that sends the
ActivateFromInventory message to Raz’s current item. This code always runs if Raz is holding something.
But the latest dialogue given to Raz should stomp whatever he was last saying. With that in mind, you would expect him to always say the “I’m watering” line because it’d always be played after the Sedan tries to play its line, right? Well, there’s one more layer to this story.
This layer would reveal itself once I added more logging to keep track of exactly what’s going on. On top of the logging I already added to the Sedan, I added additional logging to both the watering can and Raz’s interaction function, allowing us to track what’s called and when.
So, starting from fresh. I loaded into Milkman Conspiracy and grabbed the watering can. Then I used it on a Sedan. As expected, on the first load Raz played his unique line for using the watering can on the Sedan. What did the log reveal? Well, here’s the log. See what happens?
Raz sends the message to the Sedan telling it that the watering can was used on it. Then he sends the message to the watering can telling it was used.
Then… the watering can handles its message first. It does its weird GMan check and tries to play the line. Then the Sedan handles the message and overrides the watering can’s default idle response with its unique one.
And that’s the key - the messaging system in the engine does not guarantee that messages arrive in the same order that they are sent. Sure, Raz may send the message to the Sedan first, but it receives it last, which allows it to stomp the watering can dialogue.
And what do we see when we reload the level? We see that after getting the watering can and reloading the level, the messages are sent the other way around. The Sedan receives the message first, allowing the watering can to stomp the Sedan’s line.
So, the point of failure with this bug is absolutely with the watering can’s interaction code. It shouldn’t just be checking for GMen, it should of course just be using the same base check that all the other props use.
But, this quirk in the messaging system, alongside this line’s obscurity, is what allowed it to slip past. It’d have been easy to spot if the messages always happened in the order they were sent.
But because that’s not guaranteed, this bug would easily evade the eyes of someone who’s just quickly loading into the level and checking the line works. They wouldn’t imagine that it’d inexplicably stop working on a revisit.
So… why does the order of the messages get reversed? Well, I already had a suspicion about that but just to confirm it, I added 2 final pieces of logging - printing a message when the watering can and the Sedan spawn.
Here’s what gets logged whenever you load the level before getting the watering can, when the level is in a state where the line works properly (the Sedan receives the message last) The watering can spawns, then the 5 sedans.
Meanwhile, here’s what happens when we enter the level with the watering can already collected. The watering can, being in Raz’s inventory, is spawned after the Sedans which are part of the level!
From this we can conclude that messages probably reach entities in the order they were loaded - entities loaded first receive messages first. With this, the final piece of the puzzle is in place to explain why this bug manifests in the manner it does.
It’s a fairly specific set of circumstances that affect a very specific, obscure line of dialogue and ultimately I’m not shocked this bug remained in the final game - most people only recently learned the Sedans even had interaction lines any of the items.
It’s also a fairly valuable experience - the knowledge of which order the game processes messages in is very good to know and I’m sure that having that knowledge will save modders some headaches when it comes to edge cases like this in the future.
So, to conclude - the root cause of this issue is the fact that the watering can’s interaction logic seems to be a potentially older version of the code that checks for GMen specifically instead of if Raz is looking at anything.
But the bug’s existence is obscured by the way the game’s script message queue works - as long as you don’t reload the level the line plays normally because it gets stomped by the Sedan’s line.
When reloading though, the load order of the entities changes and thus so does the order in which they receive messages. The watering can now loads last, meaning it gets the message last, meaning its dialogue takes priority!
Phew! That’s an absolute fascinating bug for such a small line. It was very interesting to debug it and a lot of fun to realise what was going on. This would be fairly easy for modders to fix with a very basic script patch.
You just need to remove the watering can’s unique dialogue handler and instead let it fall back to the base version which is used for all the other objects.
I hope this was an interesting breakdown. I did already post about this in both the comments of the video that mentions the bug and shows the line, as well as the DF Discord (where I saw the video originally) but I wanted to share it here, as well as the process of debugging it.
Psychonauts, associated logos, branding and game assets are copyright Double Fine Productions and Xbox Game Studios.