Monday, July 30, 2012

Just a reminder that there will be a quest tonight, in 90 minutes of the time of this posting.

It's gonna be a good one but we need a good turnout, so make it if you can!


Friday, April 27, 2012

Fixing What Ain't Broke

We rolled out a bunch of new code during the middle of this past week, and the change I posted contained these lines:
The following features have been rewritten but should behave identically to how they always have behaved. Please report any abnormalities with them:

1. all damage-over-time spells (necro spells, headache, poison, plague)
2. unholy fire, reflect (does anyone still have this spell?), and strawberry fulfillment
Upon reading this, a very astute player asked a very sensible question: "When you say DOTs have been rewritten but act the same, why rewrite them?"  After all, if the code ain't broke, why fix it?

While I cannot discuss the specific case of the recently rewritten DOT spells (I rewrote them as a part of a still-secret new feature), I'll use another recently rewritten spell to illustrate.  Back in early April I rewrote the mirror images/astral projection/shield of skulls spells.  Briefly, the reasoning was this:
  1. We were going to make pinch always land.  This prompted a rewrite of pinch
  2. Pinch is effectively a DOT spell; it wakes you up on the tick by dealing a small amount of damage to you
  3. Pinch did this damage by calling the damage() routine, whose function is to apply damage to a victim after checking for sanctuary, magic ward, resists, vulns, and most importantly, mirror image
  4. Since pinch did such little damage to begin with, it would sometimes not wake you up if you had sanc/ward/etc and they reduced pinch's damage to 0*
  5. I changed pinch from using damage() to raw_damage(), a routine that does damage but ignores most other factors like sanc, ward, and mirrors.
  6. Because we still wanted pinch to hit mirrors like poison did, I had to add special code into the pinch spell to make it destroy images in the same way that it does from within the damage() routine
To achieve this, I had to figure out how the mirror images code in damage() actually works.  When I opened up the file, I found this:

if (IS_AFFECTED2(victim, AFF_MIRROR))
  {
    for (paf=victim->affected; paf != NULL; paf = paf->next)
      {
        if (paf->type == gsn_mirror)
          break;
      }
    if (paf)
      {
        imagehit = number_range(0,paf->modifier);
  
        /* mirrors exempt */
        if( dt == gsn_backstab ||
            dt == gsn_blister ||
            dt == gsn_decay ||
            dt == gsn_atrophy ||
            dt == gsn_wilt )
          {
            imagehit = 0;
          }
            
        if (imagehit != 0)
          {
            act("$n hits an image of $N",ch,0,victim,TO_NOTVICT);
            act("$n DESTROYS an image of you!",ch,0,victim,TO_VICT);
            act("You DESTROY an image of $n!",victim,0,ch,TO_VICT);
            paf->modifier--;
            if (paf->modifier == 0) 
              { 
                affect_strip(victim,gsn_mirror);
                REMOVE_BIT(victim->affected_by2, AFF_MIRROR);
              }
                
            return FALSE;                                                                        
          }
      }
    else
      {
        bug( "Damage: Mirror Image failure.", 0 );
      }
  }
In English, the code essentially does this:
  1. Check to see if the target of the damage() is affected by mirror images.  If he is, 
  2. Figure out how many images they have left.  If we can find this out,
  3. Determine the % chance of this damage hitting a mirror image:
    • if the damage is being done by backstab, blister, decay, atrophy, or fester, the chance of hitting an image is 0% (these skills/spells will never hit images)
    • otherwise, the chance of hitting an image is N/(N+1), where N is the number of images the victim has left
  4. If an image is hit,
    1. Send messages to the victim, the attacker, and the rest of the room,
    2. Reduce the number of images on the victim by one,
    3. and if the number of images is now 0, strip off the aff entirely so the victim can re-cast them
While the above code certainly works, it was written in a painfully verbose manner.  Every aspect of mirror images' behavior is checked separately and there are four levels of nesting in the logic.  Because the code is so drawn out and wordy, it's difficult to just glance at the code and see exactly what factors into the mirror image calculation.

Trying to extend and modify code like this to add new features is similarly difficult; not only do you need to figure out how it works, but you need to figure out at what level in the logic the new feature needs to be added.  And wouldn't you know it, huge swaths of the game's code are written in a similar fashion.  Either a lot of code is used to accomplish something very little (which makes the code hard to maintain, extend, or modify), or too little code is used to accomplish something very big (in which case the code causes the game to crash whenever someone does something out of the ordinary).

I suspect that most of the game's past coders weren't thinking about how easy their code would be to maintain in five or ten years, and that mentality resulted in a lot of garbage piling up without anyone cleaning up**.  Such an approach to code development is not sustainable though, and that approach usually leads up to a major breaking point.  In my mind, Dark Risings reached that breaking point right around the same time I took over as coder and the game was crashing on a daily basis.

Although I have no formal training in anything having to do with programming, computer science, IT, or anything like that, my programming mantra is to do things the "right" way rather than the "easy" way.  Given our game's code, this results in a significant amount of my time on the game being spent on code maintenance rather than development.  While I never go into the code with the intent of just rewriting things, it winds up happening when I need to modify or add a new spell or feature that is tied into a gnarly piece of code from years past.  What starts out as a five-minute change can turn into literally days of restructuring and testing.  And the real kicker here?  Provided I did a good job, nobody will ever notice that I did anything.

This is all part of the job involved in maintaining our Frankenstein code though, and I've put in my share of bad code too.  I just thought it might be insightful to get a peek at what coding at Dark Risings entails.

And for those who are interested, I replaced the old mirror image code (above) with this version which does the same thing in a much more concise fashion:
if ( IS_AFFECTED2(victim, AFF_MIRROR)
&&  (paf = affect_find(victim->affected, gsn_mirror))
&&  number_range(0,paf->modifier)
&&  dt != gsn_backstab
&&  dt != gsn_blister
&&  dt != gsn_decay
&&  dt != gsn_atrophy
&&  dt != gsn_wilt )
{
  act("$n hits an image of $N",ch,NULL,victim,TO_NOTVICT);
  act("$n DESTROYS an image of you!",ch,NULL,victim,TO_VICT);
  act("You DESTROY an image of $n!",victim,NULL,ch,TO_VICT);
  if ( --paf->modifier == 0 )
    affect_strip(victim,gsn_mirror);
  return FALSE;
}
Much simpler, right?


* This is not the whole story, but it's close enough
** Also not entirely true.  My predecessor, Kesavaram, did a lot of cleaning up.  As far as I can tell, nobody else did.

Wednesday, April 25, 2012

A Case Study

Recently we had a complaint come in regarding a possible multikill rule violation. It was a very interesting case with a lot of points to consider, so we thought it might be interesting to use it as sort of a “case study” to give players an idea of how these kind of investigations are conducted, and why we make the kind of rulings we do. All names have been changed to protect the innocent!

As with all such complaints, we talked with all parties involved, reviewed both the rule itself and timestamped logs from the shell, and also took into consideration other relevant factors, such as if anyone involved was a newbie and how likely each participant was to have been aware of his options at the time. As always, our goal is to make a ruling that is fair for all parties involved, and which overall is best for the game as a whole.

First, we reviewed the rule itself, as it currently stands:
Once a character is KO'd, they should not be KO'd again until they have reached sanctuary and are out of fight lag. They also cannot be KO'd after coming back for the items on their corpse if they have been murdered. This rule does not apply if they enter the combat again after being KO'd. See help etiquette for some finer points on this subject.
We then looked at objective facts about what happened, which all parties agree on:

  • Bob and Dan start to fight. Another one of Bob’s enemies, and Dan’s ally, Jim logs on during the fight but does not interfere.
  • Bob is knocked out by Dan and lightly looted of five items.
  • Dan then goes safe and does not participate further.
  • After awakening from stupor, Bob checks the who list and sees Jim is still around. 
  • Bob considers going safe but instead decides to go to Thalos.
  • Jim trails Bob to Thalos but does not attack.
  • Bob runs into a ghost in Thalos and gets back into fightlag. Jim may or may not be aware of this.
  • Bob leaves Thalos and goes through Arinock to the Void. He chooses not to stop in safe along the way.
  • Jim follows Bob to the Void. 
  • At this point almost three full minutes have passed since Bob awoke from Dan’s stupor, double the 90 seconds required by fightlag. Bob has re-equipped and is missing only a light, head slot, and weapon.
  • Jim attacks and knocks Bob out.

The first thing we do after gathering the facts is return to the rule. Bob was not murdered by Dan, so the part about coming back to his corpse for items does not apply. Since Bob wasn’t the one to start combat with Jim, the part about the victim entering combat again doesn’t apply. The only part to consider is: Once a character is KO'd, they should not be KO'd again until they have reached sanctuary and are out of fight lag.

Bob’s point of view is that since he did not reach sanctuary and was only out of fightlag for a few seconds before Jim attacked, he feels the rule was clearly violated.

On the other hand, Jim is positive Bob was out of fightlag since Jim was careful to count seconds, tacking on an additional 45 seconds past the 90 fightlag count just to be sure, and feels it is not his fault that Bob chose not to get safe when he had plenty of chances. Since Bob could easily have gotten safe and was, without question, out of fightlag, he feels the rule was clearly NOT violated.

Applying the rule (as it is currently written) is certainly problematic in this case because it assumes that the victim of a KO will attempt to get safe immediately after awakening from stupor. Bob could have done this, but he also states that he made a conscious decision not to. What if Bob never went to safe again after the initial knockout? Would Jim never be able to attack him again? Obviously that’s not fair; the line has to be drawn somewhere.

Further muddying the waters is this: Bob was not in fightlag in Thalos, and only got back into fightlag because he ran into an aggressive mob. If he had not run into that mob, then his own estimation of having been out of fightlag for “a few seconds” would have to include “a few seconds + the 90 seconds I was in fightlag from the mob”, which means by his own estimation he absolutely was out of fightlag from the first fight by the 90 seconds required by the rule, plus those few seconds in the Void, plus the time it took him to go from the area where he was KO’d initially by Dan to Thalos: a total of almost three full minutes by the game logs.

So to reiterate:

Dan knocked out Bob. Bob spent almost three minutes being shadowed by Jim (who is allied with Dan, but was not working with Dan). Jim counted the time required by the rule to give Bob time to get safe, and added extra time just to be sure. Bob chose to avoid safe. Jim attacked and KO’d Bob.

What makes this case messy is how tight the time is. 90 seconds is not that long. 3 minutes is not that long. In a different recent case, we had Roy the rogue fail a steal on new player Pete, who responded by trying to push Roy away repeatedly. Roy knew the rules extremely well and ducked into safe for 93 seconds, and then immediately went back to Pete who was still trying to push him away. Roy then used the psteal rule to KO Pete and loot heavily. In that case, we ruled against Roy for two reasons. First, the psteal rule states that the rogue must CLEARLY have gotten out of fightlag. 93 seconds is not clear to a new player who may or may not be aware of what fightlag even is. Although it’s not up to Roy to know who is and is not a new player, he should know better than to race back to a PK just seconds out of fightlag and try to get attacked so that he can KO that player. Roy should have waited long enough for Pete to realize that they were no longer fighting. 93 seconds is not long enough to be CLEARLY sure someone has gotten out of fightlag.

In the case with Jim and Bob, both players are pros, and Jim consciously waited out the fightlag time and added extra time to be absolutely sure Bob was out of fightlag. So where Roy did NOT try to make sure that the fightlag time was CLEARLY up, Jim did. 

Once a character is KO'd, they should not be KO'd again until they have reached sanctuary and are out of fight lag.

After deliberating the facts in this case, the admin team decided that although Bob did not reach sanctuary, he could have easily and it was HIS choice not to try, which therefore exempts him from the protection that part of the rule (reaching sanctuary) provides. Therefore the question is whether or not Jim violated the fightlag portion of the rule. Given that Jim waited not only the 90 seconds the rule demands, but well past that, it seems clear that all parties involved had a reasonable chance to be sure that Bob was not still in fightlag from the fight with Dan when Jim initiated the second attack on Bob that day. Therefore we ruled that in this case no rule was broken, and the situation was not multi-kill.

The rule as it stands is badly worded, since it assumes the KO’d player will automatically get safe after a KO when clearly that’s not always the case. It’s also a little vague about what fightlag is: Is it reasonable to expect Jim to factor in fightlag from mobs Bob may or may not be fighting? Should Bob be able to kill a mutt every 85 seconds, stay in fightlag forever, and expect all his enemies to be aware of his fightlag state? Obviously not. For these reasons, we will be revising this rule for clarity.

We thought players might find it interesting to see the thought process behind rules calls. We are also interested to hear from those who think there may be factors we overlooked, but we ask those who may recognize this case (or think they do) to keep identifying factors out of it for the sake of the players involved.