(no subject)
Sep. 23rd, 2022 11:55 amMUD player reported that taking gold isn't working. Was a bit slammed and timing was bad so didn't investigate it initially, and besides, that seems kind of impossible... must have been limited to one monster or the area builder or something, and if it really were broken, I would have heard more about it. Eventually I did hear more about it. This player is a great play tester and the bug log is brimming so been trying to get around to dealing with the whole backlog.
So, I test zap'ing (wizards can kill about anything with one command, for testing) some monsters and taking their gold and it works fine. Asking for more info, he can't provide any. It just doesn't work. Eventually get out of him that it says "Taken: 0 gold coins". Snoop him (watch his screen output, another wizard ability) while force'ing (yet another) him to take a pile of coins off the ground, and sure enough, 0 coins, so it's not just something about how many coins a monster has or drops, he really can't pick up gold. Setting my class to barbarian to match his, boom, taking money, it's magically 0 coins instead of whatever it was, so it's definitely barbarian related.
Reading the code for the "take" command, it's perfectly straight forward. Looking at the code for the object that represents piles of money... not so much:
init() runs when an object physically encounters a living thing, either when the living thing moves or is moved to where the object is, or the object is moved to where the living thing is. That could be in the living thing's inventory or in the same room, or the living thing to inside of that object.
The gold object wants to self-destruct when picked up, and add the gold coins to the player's count of coins. This is where it does it... or tries to. living(environment(this_player())) indicates whether a living thing is holding it. this_player() is the current living thing it is in contact with.
But it can't self destruct immediately, or else the "Taken: nnnn gold coins" message in the "take" command will error when the object isn't there and the "take" command is trying to ask the object for its short description. The player will get the classic "Your sensitive mind notices a wrongness in the fabric of space." message. So it waits 0 seconds, which is still waiting until after the current command finishes, then moves the gold and self destructs. From comments in the code, it previously waited > 0 seconds, but players were able to drop and pick it up multiple times before it self destructed, and money would be added repeatedly. This is so classic LPMud 2.4.5 bugginess I had to write about it.
So what's going wrong? I added this just inside the if():
And tried again:
Immediately before init() delays sending the money to Otto, it also delays sending the money to 0. LPMud 2.x (and we're in compat mode here) doesn't have a null or void value, only 0. If you call a method that doesn't exist, you don't get an error or an undef, you get 0. Some other living thing that's not a player or monster "touched" the gold first. Aha.
Notice that it's using two different values... living(environment(this_object())) and this_player(). A while back, in response to another report from this player that the "frenzy" barbarian ability doesn't do anything, I found the barbarian soul was using heart_beat to implement frenzy, setting a "heart beat" in the game driver that calls the heart_beat() function every two seconds. Every player has a heart beat. Class abilities are implemented in invisible objects players hold. In 2.4.5, an object didn't have to be living to have a heart beat, but lots of objects using heart beats to poll for something that rarely happens was bogging down games, and cracking down on that became a priority for game admins, so a rule was instituted that only living things should ever have heart beats. Annoyedly, I mark the barbarian soul "living" with enable_commands() so that it can have a heart beat. It's responsible with the heart beat, turning it on during the duration of the spell and off after the spell runs out, so I'm just working around pointless bureaucracy here. Worse, Amylaar's (game driver fork, since the LPMud driver was long ago abandoned and forks took over) 2.x compat mode allows non-living objects to have heart beats, which is correct back compat, but LDMud, which we're currently running (mostly because it's newer and so more recent work was done to make it actually compile) doesn't get this right. So really, I'm working around Amylaar not compiling. (Someone ported a version of Amylaar to the Amiga, and I did a lot of MUD dev locally on my Amiga 1000, so fond memories of Amylaar.)
Anyway, gold that gets picked up by a barbarian first encounters the now-living barbarian soul. living(environment(this_object))) is the player that picked up the gold... but this_player() is the barbarian soul. Second, it encounters the player. Two call_out()s are run, the first for the soul object, the second for the player. The soul one returns first. The amount of gold is blanked to prevent the double-pickup bug and instead recorded in another variable for use by the delayed code. Gold is transferred to the barbarian soul, whose add_money() method doesn't exist, so absolutely nothing happens from the attempted transfer. Then the object is destroyed. The second invocation of delayed code, transferring the same amount of money to the player, never runs.
There are a bunch of ways this could be fixed... instead of delaying code to transfer money to this_player(), it could be sent to environment(this_object()), so they match. We know environment(this_object()) is actually holding the gold, so it's a pretty good guess as to where to send the money. That could be done without a delay, then the money blanked, but the desc cached, and the delay only used to destroy the object after "take" finishes. "take" could call a method in objects to notify them that it has finished. This would fit the preaction/action/postaction design Infocom used. But instead, we get the incredibly, wonderfully buggy that is 2.4.5 LPMud, and I added to this bug a long time ago and then again just a bit ago.
So, I test zap'ing (wizards can kill about anything with one command, for testing) some monsters and taking their gold and it works fine. Asking for more info, he can't provide any. It just doesn't work. Eventually get out of him that it says "Taken: 0 gold coins". Snoop him (watch his screen output, another wizard ability) while force'ing (yet another) him to take a pile of coins off the ground, and sure enough, 0 coins, so it's not just something about how many coins a monster has or drops, he really can't pick up gold. Setting my class to barbarian to match his, boom, taking money, it's magically 0 coins instead of whatever it was, so it's definitely barbarian related.
Reading the code for the "take" command, it's perfectly straight forward. Looking at the code for the object that represents piles of money... not so much:
/*
* If we are picked up by a player, then move the money to his "purse",
* and destruct this object.
901128: Changed by JnA to not destruct object until surely picked by the
player, i.e. object moved to the players inventory with move_object()
*/
/*
sdw, 1/2000: fixing JnA's code not to dest/change title untill after whole
process of being picked up finishes
*/
init() {
if (living(environment(this_object()))) {
moneywas=money; money=0; /* so they cant drop/get/drop/get us over and over */
call_out("finish", 0, this_player());
}
}
finish(obj) {
call_other(obj, "add_money", moneywas);
destruct(this_object());
}
init() runs when an object physically encounters a living thing, either when the living thing moves or is moved to where the object is, or the object is moved to where the living thing is. That could be in the living thing's inventory or in the same room, or the living thing to inside of that object.
The gold object wants to self-destruct when picked up, and add the gold coins to the player's count of coins. This is where it does it... or tries to. living(environment(this_player())) indicates whether a living thing is holding it. this_player() is the current living thing it is in contact with.
But it can't self destruct immediately, or else the "Taken: nnnn gold coins" message in the "take" command will error when the object isn't there and the "take" command is trying to ask the object for its short description. The player will get the classic "Your sensitive mind notices a wrongness in the fabric of space." message. So it waits 0 seconds, which is still waiting until after the current command finishes, then moves the gold and self destructs. From comments in the code, it previously waited > 0 seconds, but players were able to drop and pick it up multiple times before it self destructed, and money would be added repeatedly. This is so classic LPMud 2.4.5 bugginess I had to write about it.
So what's going wrong? I added this just inside the if():
tell_object( find_player("scrottie"), "XXX: " + this_player()->query_name() + "\n" ); // XXX
And tried again:
> force otto take money
%Scrottie forced you to: take money.
XXX: 0
XXX: Otto
%Taken: 0 gold coins
Otto takes money from here.
You force Otto to: take money.
Immediately before init() delays sending the money to Otto, it also delays sending the money to 0. LPMud 2.x (and we're in compat mode here) doesn't have a null or void value, only 0. If you call a method that doesn't exist, you don't get an error or an undef, you get 0. Some other living thing that's not a player or monster "touched" the gold first. Aha.
Notice that it's using two different values... living(environment(this_object())) and this_player(). A while back, in response to another report from this player that the "frenzy" barbarian ability doesn't do anything, I found the barbarian soul was using heart_beat to implement frenzy, setting a "heart beat" in the game driver that calls the heart_beat() function every two seconds. Every player has a heart beat. Class abilities are implemented in invisible objects players hold. In 2.4.5, an object didn't have to be living to have a heart beat, but lots of objects using heart beats to poll for something that rarely happens was bogging down games, and cracking down on that became a priority for game admins, so a rule was instituted that only living things should ever have heart beats. Annoyedly, I mark the barbarian soul "living" with enable_commands() so that it can have a heart beat. It's responsible with the heart beat, turning it on during the duration of the spell and off after the spell runs out, so I'm just working around pointless bureaucracy here. Worse, Amylaar's (game driver fork, since the LPMud driver was long ago abandoned and forks took over) 2.x compat mode allows non-living objects to have heart beats, which is correct back compat, but LDMud, which we're currently running (mostly because it's newer and so more recent work was done to make it actually compile) doesn't get this right. So really, I'm working around Amylaar not compiling. (Someone ported a version of Amylaar to the Amiga, and I did a lot of MUD dev locally on my Amiga 1000, so fond memories of Amylaar.)
Anyway, gold that gets picked up by a barbarian first encounters the now-living barbarian soul. living(environment(this_object))) is the player that picked up the gold... but this_player() is the barbarian soul. Second, it encounters the player. Two call_out()s are run, the first for the soul object, the second for the player. The soul one returns first. The amount of gold is blanked to prevent the double-pickup bug and instead recorded in another variable for use by the delayed code. Gold is transferred to the barbarian soul, whose add_money() method doesn't exist, so absolutely nothing happens from the attempted transfer. Then the object is destroyed. The second invocation of delayed code, transferring the same amount of money to the player, never runs.
There are a bunch of ways this could be fixed... instead of delaying code to transfer money to this_player(), it could be sent to environment(this_object()), so they match. We know environment(this_object()) is actually holding the gold, so it's a pretty good guess as to where to send the money. That could be done without a delay, then the money blanked, but the desc cached, and the delay only used to destroy the object after "take" finishes. "take" could call a method in objects to notify them that it has finished. This would fit the preaction/action/postaction design Infocom used. But instead, we get the incredibly, wonderfully buggy that is 2.4.5 LPMud, and I added to this bug a long time ago and then again just a bit ago.