Ok. I'll review what my best knowledge was at the time that most of these changes took place on UOSA, and the things that were discovered since then regarding the behavior of disrupts on the demo. So far as I know, these discoveries were never introduced into the UOSA mechanics in any form, and the code that we utilize is an approximation of what we thought was the
intended effect of the mechanics at the time.
First, to review what was known at the time, there are 4 scripts for handling the casting of any spell: the specific script for casting the spell, the base script for executing the effect of the spell, the spelskil.c script (where many checks for casting a spell such as checking for whether you're casting another spell, whether your hands are empty, and whether you have the proper mana/reagents for a spell are done as well as calculating the actual spell damage and chance to resist a spell), and the casting.c script which handles the freezing/unfreezing of the caster, and most importantly - disruptions. The relevant code for the casting script is as follows, which had been recently successfully decompiled by Batlin earlier in the year back in 2009:
Code: Select all
object Q5UY;
#on creation()
{
Q5UY = getObjVar(this, "spellObj");
removeObjVar(this, "spellObj");
setMobFlag(this, 0x02, 0x01);
return(0x01);
}
#on washit(object attacker, integer damamt)
{
integer Q4IB = (Q42S(Q5UY) - 0x01) * 0x03E8 / 0x07;
Q4IB = Q4IB + damamt * 0x14;
integer Q5NC = getSkillSuccessChance(this, 0x19, Q4IB, 0x28) - random(0x00, 0x03E7);
if(Q5NC <= 0x00)
{
systemMessage(this, "Your concentration is disturbed, thus ruining thy spell.");
Q4AR(this);
return(0x01);
}
return(0x01);
}
From the above code for the #on event() functions (formally called triggers in the scripts), Batlin discovered that whenever a relevant event was detected in the server core it would send out the proper trigger to the scripts attached to the relevant objects to handle the occurrence of that event. It was later discovered that in the event of a spell causing damage, which would trigger the "was hit" event, a bug existed in the code for sending out that trigger which caused the trigger to be sent out twice for spell damage, once before and once after the damage had been properly halved. From this broad understanding, we were interested in how the function from above is actually calculated in order to understand the chance to be disrupted in any given scenario.
From the above event, we can see that the general code seems pretty clear. Q4IB is first calculated via this formula:
Code: Select all
Q4IB = (Spell circle - 1) * 1000 / 7
Q4IB then has 20 times the amount of damage done added to itself, and then Q5NC is calculated via a getSkillSuccessChance check against magery with the Q4IB being used in the check. If the resulting value for getSkillSuccessChance minus a random value from 0 to 999 turned out to be less than 1, then the spell was disturbed.
However, at the time, a problem existed:
We didn't know what the getSkillSuccessChance calculation actually was. Back then, we really only had access to the scripts in that we could read through them, and we did not have any ability to inject our own code into the demo, nor had Batlin published any of his incredibly useful work on decompiling any of the core functions of the demo. As a result, we had to set out to build our own equation that approximated what we thought was the best representation of the intent of the January 1998 patch note and the available information. We knew that reports from that time and the era indicated that it was
very easy to disrupt a spell, but we recognized that the patch note indicated that there was a
chance to be interrupted based on the spell circle cast, and the magery of the caster. Thus, Derrick set out to create his own equation that allowed players to be disrupted to a very high degree, while allowing them to continue to cast in some cases
even when hit by a spell with a double check for disrupts. While I am not privy to the equation that he came up with, it is obvious that any equation which is crafted to occasionally allow a cast to continue despite a double check with one check twice as hard as the other would not reliably disrupt on a single check (for half damage IIRC). This is likely why the same double check was put into place for melee damage, as there was no indication that a difference existed between the two.
That brings us up as far as I can track regarding the changes to spell disruption, and likely covers what is in our code. However, this particular implementation isn't actually correct, and so far as I know, hasn't been revisited since that time.
Looking back at the the demo casting script, we see that the washit event utilizes a simple equation for calculating the input value to getSkillSuccessChance, but we didn't know what was done with that value. In early 2011, Batlin gave me the relevant code the getSkillSuccessChance formula which was part of a skill simulator system that he was working on. At roughly the same time, we got the rudimentary ability to inject new code into the demo scripts, allowing us to test certain things. Out of an interest in exploring this issue, I wrote in some output lines to see what the actual input variables to the getSkillSuccessChance formula actually were on the demo. Thus, the washit event was modified to be as follows:
Code: Select all
on washit(object attacker, integer damamt)
{
systemMessage(this, "Damage taken: " + damamt);
integer Q4IB = (Q42S(Q5UY) - 0x01) * 0x03E8 / 0x07;
systemMessage(this, "Circle difficulty: " + Q4IB);
Q4IB = Q4IB + damamt * 0x14;
systemMessage(this, "Total difficulty: " + Q4IB);
integer Q5NC = getSkillSuccessChance(this, 0x19, Q4IB, 0x28) - random(0x00, 0x03E7);
systemMessage(this, "Success chance: " + getSkillSuccessChance(this, 0x19, Q4IB, 0x28));
if(Q5NC <= 0x00)
{
systemMessage(this, "Your concentration is disturbed, thus ruining thy spell.");
Q4AR(this);
return(0x01);
}
return(0x01);
}
The code for the getSkillSuccessChance equation is as follows:
Code: Select all
(this->GetSkillLevelReal(SkillNumber, 0) - Diff) * 100 / Focus) + 500;
With Diff being equal to Q4IB, Focus being equal to the the last value passed in when getSkillSuccessChance is called, and the skill level being on a scale of 0 to 1000.
When tested on the demo itself, the following values appeared when hit by a fire field while attempting to cast blade spirits with 100.0 magery:
- casting bug.jpg (103.89 KiB) Viewed 5338 times
As you can see from the screenshot, the total difficulty for casting the spell is astronomically high at 11420, while the damage taken is actually 0 due to standing in town. We also see that the difficulty for the circle is 571, which should output a value of 571 for the overall difficulty, so what's the problem? As it turns out, the part of the equation here...
reveals a bug with the parsing engine for equations in the scripts. Specifically - order of operations is not properly followed.
While the demo does properly parse things like parenthesis, and it properly calculates the values within parenthesis first before evaluating terms outside of parenthesis, if a set of terms are on the same level, then the equation is simply parsed from left to right.
The result is that for all disrupt checks, the spell circle difficulty is added to the damage, and then multiplied by 20. This results in virtually every disrupt check producing incredibly huge numbers that will
always result in a disturbed spell. The one exception to this is first circle, which has a very low chance be disrupted except in the most extreme cases.
To run some numbers, let's take an attempt to cast second circle spell while taking 0 damage, due to being in town, by a caster with 100 magery. The equation, as parsed by the demo will be as follows:
Code: Select all
Q4IB = 1000 / 7 = 142
Q4IB = 142 + 0 * 20 = 2840
Inputting this value into getSkillSuccessChance (where order of operations is followed properly), we get the following output:
Code: Select all
(1000 - 2840) * 100 / 40 + 500 = -4100
Which is filtered to 0 via the rest of the core function for being a very negative number.
In essence, we can see that when a player is hit, even for 0 damage, that if they are casting even a second circle spell, they will always be disturbed regardless of their magery skill. On the other hand, if a player is casting a first circle spell, we will get very different results. Running some number again, let's assume that a player is casting a first circle spell and is hit for 50 damage. The input difficulty will be calculated as follows:
Code: Select all
Q4IB = 0 / 7 = 0
Q4IB = 0 + 50 * 20 = 1000
Inputting into getSkillSuccessChance, we get the following:
Code: Select all
(1000 - 1000) * 100 / 40 + 500 = 500
Thus, even at rather high numbers, first circle spells are extremely difficult to disrupt. Combined with the fast cast time, and the fact that there is only 1 tick where the spell is even vulnerable to being disrupted makes first circle spells virtually uninterruptable.
With an understanding of the bug itself, the obvious fix is to add in parenthesis to cause the equation to conform to the proper order of operations. However, by doing so, the results of the equation are radically altered. As an example, suppose a player with 100 magery is casting a 6th circle spell and is hit for 20 damage in an attempt to disturb the spell. The difficulty would be calculated as follows:
Code: Select all
Q4IB = 5000 / 7 = 714
Q4IB = 714 + (20 * 20) = 1114
Inputting this into getSkillSuccessChance, we get the following:
Code: Select all
(1000 - 1114) * 100 / 40 + 500 = 215
As you can see, despite a pretty reliable amount of damage being done, there is still a reasonable chance the the spell will be cast (21.5%). The chance to disturb gets far worse for lower circle spells, with spells below 5th circle being impossible to disturb at low damage values (>18 damage for 4th circle at GM Magery, >25 for 3rd, and >32 for 2nd). It is likely that this error was seen, corrected, and later reverted out in a later patch which documents the reversion to "correct" spell disruption.
So that covers the relevant part for disturbing a spell. That is: on the demo, any spell beyond a first circle spell will always be disrupted at all skill levels no matter the damage done. However, there is more to the problem than that. With 100% effective disruption, we have to consider our current design for disrupting a spell where a caster must wait out the rest of the cast time before being allowed to attempt to cast another spell. Referring to the above screenshot of the demo, you'll notice that the last line in that screenshot is the message "You are already casting a spell.". This was triggered by attempting to cast a spell immediately after being disturbed, and actually represents a second bug related to spell casting and disruption that was
likely fixed with the September 1998 patch which attempted to fix this class of bug. The bug behaves as follows and will connect code from a few places:
If a spell is disturbed in the demo, the following piece of code executes:
Code: Select all
if(Q5NC <= 0x00)
{
systemMessage(this, "Your concentration is disturbed, thus ruining thy spell.");
Q4AR(this);
return(0x01);
}
From here, we can see that a function, Q4AR, is called. This function, which is in the casting script, is as follows:
Code: Select all
void Q4AR(object it)
{
setMobFlag(this, 0x02, 0x00);
detachScript(it, "casting");
return();
}
This function is simply meant to end the casting process, by detaching the casting script and unfreezing the caster. However, this function is used in a generic fashion both for the casting process being disturbed, and for the casting process ending normally.
This is relevant because a function in the spelskil script, Q4M9, acts as the set up function for beginning the casting process of a spell. The code of this function is as follows:
Code: Select all
void Q4M9(object spell, object caster)
{
if(Q4YT(caster) || Q507(caster))
{
systemMessage(caster, "You are already casting a spell.");
return();
}
if(getMobFlag(caster, 0x02))
{
systemMessage(caster, "You can not cast a spell while frozen.");
return();
}
integer Q5UX = Q4T2(spell);
integer Q5US = Q4SY(Q5UX);
integer Q55B = Q4SX(Q5US);
if(!Q49Q(caster, Q55B))
{
return();
}
setObjVar(caster, "spellObj", spell);
attachScript(caster, "casting");
integer Q4H9 = Q4SV(Q5US, Q5UX);
shortcallback(caster, Q4H9, 0x80);
shortcallback(caster, 0x00, 0x82);
bark(caster, Q4T5(Q4T2(spell)));
return();
}
This function, which is the first function called when attempting to cast a spell, first performs several checks to see if a player can begin casting a spell, including seeing whether a player is already casting a spell, whether they're frozen, whether their hands are empty, and whether they do or don't have the required reagents and mana to cast the spell. If these checks are successful, the casting process is started, and the casting script is attached to the player for the duration of the casting time, which then freezes them until they are disrupted or complete the casting process. The relevant lines of code to achieve that are as follows:
Code: Select all
attachScript(caster, "casting");
integer Q4H9 = Q4SV(Q5US, Q5UX);
shortcallback(caster, Q4H9, 0x80);
The final line, shortcallback, refers to the relevant piece of code for this bug found in the casting script, with the value Q4H9 referring to the number of ticks before this particular piece of code is triggered (based on the spell circle). The code for the actual callback found in the casting script is as follows:
Code: Select all
#on callback<0x80>()
{
list Q5A8;
appendToList(Q5A8, this);
message(Q5UY, "castspell", Q5A8);
Q4AR(this);
return(0x00);
}
This piece of code ends the casting process, and prepares the spell to be cast by the caster, but notably, this code also calls the Q4AR function found above as the final piece of ending the casting script. This is notable, because of
where this particular call to the function occurs, specifically within the callback trigger itself.
Looking back at function Q4M9, we see two functions are called at the beginning of the function, Q4YT and Q507. Function Q4YT is the important function here, as it checks to see if the person who is attempting to begin casting a spell is already in the middle of casting another spell. The code for Q4YT is as follows:
Code: Select all
integer Q4YT(object Q68S)
{
if(hasCallback(Q68S, 0x80))
{
return(0x01);
}
return(0x00);
}
Here, we see a core function call, which simply looks to see if a callback with a specific ID number is already queued up for the caster. In this case, callback ID 0x80, which happens to be the callback ID for ending the casting process. This makes sense as a general barometer for whether someone is casting a spell, because the only time that they should have that callback is if they attempted to start casting a spell earlier. However, a problem does exist with this approach as it relates to a spell being disturbed.
Looking back up a bit to the piece of code that triggers when a spell is disturbed, we can see where a problem might occur in the overall casting process. To visualize this, we can set up some timelines of events for when a spell is cast and compare the sequence of events. The following is a timeline for a cast that starts and ends normally:
- Player attempts to cast a spell.
- Function Q4M9 checks to see if they can cast the spell (not casting, not targeting, reagents, mana, hands)
- Function Q4M9 sets up the spell (attaches casting script, sets cast time, sets callbacks, words of power)
- Casting script freezes character, and starts casting animation.
- Cast time is completed, and callback 0x80 is triggered.
- Callback 0x80 prepares spell to be cast by player.
- Callback 0x80 calls function Q4AR.
- Function Q4AR unfreezes caster and detaches casting script.
We can also set up a timeline to visualize what would happen if a player is disturbed when attempting to cast a spell:
- Player attempts to cast a spell.
- Function Q4M9 checks to see if they can cast the spell (not casting, not targeting, reagents, mana hands)
- Function Q4M9 sets up the spell (attaches casting script, sets cast time, sets callbacks, words of power)
- Casting script freeses character and starts casting animation.
- Spell is disturbed after some number of ticks.
- Casting script calls function Q4AR.
- Function Q4AR unfreezes caster and detaches casting script.
- Callback 0x80 attempts to fire and gracefully exits as no script with that callback ID is attached to the caster.
From the above procedure, we can see that callback 0x80 is left hanging to gracefully exit and do nothing if the spell is disturbed. However, if a player attempts to cast a spell
immediately after being disturbed, then depending on how long left they had in the casting process, the following bug might occur:
- Player attempts to cast a spell.
- Function Q4M9 checks to see if they can cast the spell (not casting, not targeting, reagents, mana hands)
- Function Q4M9 sets up the spell (attaches casting script, sets cast time, sets callbacks, words of power)
- Casting script freeses character and starts casting animation.
- Spell is disturbed after some number of ticks.
- Casting script calls function Q4AR.
- Function Q4AR unfreezes caster and detaches casting script.
- Player immediately attempts to cast another spell.
- Funtion Q4M9 checks to see if they cast the spell, and sees that they already have a callback for 0x80, cancelling the attempt with the system message "You are already casting a spell.".
- Callback 0x80 attempts to fire and gracefully exits as no script with that callback ID is attached to the caster.
From the bolded text, we can see that when a player is disturbed, they cannot attempt to cast another spell, because function Q4M9 will see the leftover callback to 0x80 queued for the caster from the previous spell, and prevent them from casting the spell. This functionally makes a player have to wait until the rest of the casting time is done for their spell in order to start a new one, but causing them to do so is strictly a bug. This bug was likely fixed with the September 1998 patch note which states the following:
The "already casting" spell bug should not recur.
At the time, a large suite of different bugs would potentially cause a player to become glitched and stuck with a spell they couldn't cast, including targeting certain floor tiles (mostly carpet), targeting an innocent and cancelling the criminal action queue, targeting someone just as they moved off the tile, and thus "targeting" the ground where they stood, as well as this bug (and probably more). The assumption is that the generic wording of the bug fix was intended to cover these broad causes for the same bug, but there is no guarantee that this specific bug was indeed fixed. The only other relevant information is that no other documentation exists regarding later bug fixes to an "already casting" bug, and that on live OSI servers, circa 2011, it was possible to immediately attempt to begin casting a spell when disturbed.
That's pretty much it. I know that this post is extremely long and contains some relatively technical information, but it is important that this information is understood, both so you can present the information as best as possible given the current information, and that you can understand the implications of the changes.
Relevant to that decision making process is the effect that changing the disturb rates will have on combat. Specifically, since we do know that until UOR, each time a debuff was cast (even if it produced the "fizzle" animation), the spell effectively "hit" the target for 0 damage. With an accurate view of disturb rates, we know that this was a reliable strategy for disturbing players at the time of the demo, and that the few examples of in-era tactics where spells like weaken were reliably used to disturb opponents have a basis in fact. It is important to consider that if a change is made to disturb rates, then a change to whether a player does or doesn't need to wait out the cast time of a spell is equally important, as the disruption tactic will need an equalizer in terms of the player being disturbed. Without that change, which is backed up by the available evidence, disruptions via debuff tactics would have no reliable answer to them (e.g. zero chance to recall from a PK, etc.).
I hope this explanation has been informative.