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.

Thursday, April 19, 2012

New Vorpal Functionality

Verghazrin was kind enough to host a battle royale PK quest last night which was a great testing grounds for the new functionality we gave vorpal.  It generated a fair bit of discussion both last night and this morning, but some of the suggestions or criticisms seem to be rooted in misconceptions on how vorpal weapons now work.  So as to make it unambiguous, here's how everything is laid out:

Class Recharge Spell Relative
Damage
Damage
Potential
Mage 2 acid blast 10 5.0
Cleric 2 smite 8.3 4.2
Monk 3 lightning bolt 1.3 0.44
Warrior 4 fireball 2.7 0.66
Barbarian 5 N/A N/A N/A
Psionicist 2 mind blast 8.3 4.2
Druid 2 natures wrath 7.1 3.6
Ranger 5 lightning bolt 1.3 0.26
Rogue 3 N/A N/A N/A
Bard 3 deathsong 7.1 2.4
Wildmage 2 wildfire 4.9 2.5
Warlock 3 ravage 7.1 2.4
Necromancer 2 decay 6.3* N/A
Templar 3 flamestrike 4.6 1.5

Recharge is the time (rounds of combat) it takes for the vorpal to "recharge."  Once a vorpal weapon is recharged, it will discharge (cast its spell) on the next hit, then recharge again.
Relative Damage is a figure of merit expressing how much damage each class's vorpal spell will do.  The higher this number is, the more damage a single vorpal discharge will do.
Damage Potential is the Relative Damage divided by the recharge time.  It gives you an idea of how much damage a class's vorpal spell will do over time.  Necromancers' Damage Potential is a bit trickier to calculate since their spell does damage over time.

Whenever a vorpal weapon discharges (mind you, "discharge" is a term I just made up now), it casts your class's vorpal spell at the level of the weapon and won't discharge again until the recharge time is up.  However, this recharge time is attached to the character, not the weapon, so a dual-wielding ranger will not get double discharges, and swapping out vorpal weapons very quickly in combat will not let you get a bunch of free spell casts.  Incidentally, a side-effect of these new vorpal spells is that other special weapon properties (chill, flame, shock, poison, and others) are now also limited by each class's recharge time.  This side-effect will be removed soon so that chill/flame/shock/poison/etc work the way they used to.

We are fairly confident in how this new vorpal is working for the time being and we do not consider the feature to be still in the testing stage.  We aren't looking for unsolicited feedback on what classes should get what spells or recharge times or anything like that; however, we will be keeping an eye on how vorpal weapons get used and, if necessary, can tweak things.

With that being said, we are considering tweaking necromancers after last night's quest.  One of the leading suggestions was to make necromancers' vorpal be like judgment but with necro spells instead of maledictions.

Monday, April 16, 2012

Wednesday Night Quest!

Verghazrin is holding a Battle Royale PK quest on Wednesday night (April 18) at 8pm Standard Time.

This quest will test out the shiny new Vorpal flag for quest weapons, which is designed to be the equivalent for magic classes what Sharp is for fighter classes.

Stop in, grab a weapon, and join the fun!

Love,
the staff

Monday, April 9, 2012

A few new changes

We've got a stack of new changes going in, but we have opted to delay rolling out the new changes (and any possible bugs included with them) until after the big event that's happening tonight happens.  So as to not let anyone who follows our Dark Risings twitter feed feel like we've been leading anyone on, I figured it would be appropriate to reiterate that big new changes are going in.  Here's a taste of what's going to happen:

  1. Poison will no longer break sleep, but pinch/haunt will now always work.  There are a bunch of auxiliary changes accompanying this too.  For example, we are working to replace all current poison-giving items (moldy bread, vial of the undead) with pinch/haunt equivalents, sleep's duration is now much shorter, and things of that nature.
  2. People were complaining about protection of peaches being undispellable, so we've made it possible to dispel, but not quite as easily as sanctuary.
  3. Monks will now get divine focus (the templar ability) at level 15.  This is in addition to their frenzy spell, so it should give monks a nice boost.
  4. Lots of changes have been made to the prompt.  You now have to manually color %h, %H, %m, %M, %v, and %V, %l will now show the exact number of seconds left in latelog, %f will now show the exact seconds left in fightlag (this is pretty huge!), and some other stuff.
  5. Ticks will have randomized lengths to curb unfair practices that certain script-happy pkers have been abusing.
There are a lot more intermediate changes going in too; the brawler board will be sorted now, various code has been added to support the new tradeskill we'll be putting in, and a lot of (mostly) transparent bug fixes are going in.  For example, sand spirits and desert scorpions should no longer be holding hundreds of fossilized shells, the 'info' command will now work properly in creation, and harm touch will now depend on your skill% in it.

The complete details will be posted as a change in the game once the new code is actually in play.  Hopefully they are received well!

Saturday, April 7, 2012

Monday Night Quest!

Hello everyone,

This is just a quick note to get the word out: we are planning a big event for the evening of Monday, April 9. We'd love to see a good turnout and are more than happy to offer bribes in the form of quest eq and prizes :)

You won't want to miss it!

Love,
the staff