Author Topic: DelayCommand Potential Issues  (Read 678 times)

Legacy_zunath

  • Full Member
  • ***
  • Posts: 152
  • Karma: +0/-0
DelayCommand Potential Issues
« on: October 06, 2010, 12:42:46 am »


               I once read that if too many DelayCommand calls are made, NWN  will drop some of them. Can anyone confirm or deny that?

Just concerned about some recursive calls I'm trying to make. '<img'> 

Thanks!
               
               

               
            

Legacy_Shadooow

  • Hero Member
  • *****
  • Posts: 7698
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #1 on: October 06, 2010, 01:44:21 am »


               I can deny it.



How? There is some spawning system at vault using waypoints and each waypoint calls delaycommand. I know one server using that system and there is running like hundret delay commands every time when someone playing and killing monsters, the more players the more delays, afaik no problems with some of those monsters not respawning...
               
               

               
            

Legacy_zunath

  • Full Member
  • ***
  • Posts: 152
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #2 on: October 06, 2010, 01:59:21 am »


               Good enough for me. Thanks for the quick response.
               
               

               
            

Legacy_kalbaern

  • Hero Member
  • *****
  • Posts: 1531
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #3 on: October 06, 2010, 04:15:34 pm »


               There are tons of delay commands in most modules. What are too many will depend on your server's CPU, the number of players online at any given time and how efficient overall scriptwise your module is. Bioware Encounter Triggers are a type of delay. Psuedo-Heartbeats another. Any "buffing" spells with a duration are delayed commands. Most loot systems and scripted creature spawning systems use delayed commands. Several PWs have overused delays and suffered too. My own building/scripting attempts to limit or avoid their usage unless no other means is available.
               
               

               
            

Legacy_FunkySwerve

  • Hero Member
  • *****
  • Posts: 2325
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #4 on: October 06, 2010, 04:22:48 pm »


               I can confirm it. '<img'>



It takes a LOT more than 100 though, and it may be more related to overall server load than to simply the number of delay commands. I've only seen it occur in lag powerful enough to stutter module events (lag not from connection speed, but from processor overload). Generally speaking, you don't need to worry about this happening in normal operation, and if it's happening, you'll know it.



Funky
               
               

               
            

Legacy_FunkySwerve

  • Hero Member
  • *****
  • Posts: 2325
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #5 on: October 06, 2010, 04:28:02 pm »


               

kalbaern wrote...
 Any "buffing" spells with a duration are delayed commands.

This is incorrect, if I remember right. Delaycommands have a separate stack, or so I seem to recall. I'd have to ask my co-admin to be absolutely sure (he's the one who does all the sexy NWNeXalt hacks, and has spent prodigious amounts of time digging around in the engine), but I will say that he's advised me, and it's our policy, to avoid using 2 delaycommands where one will do, not out of fear of them failing, but in an attempt to keep overhead down.

Funky
               
               

               
            

Legacy_Terrorble

  • Sr. Member
  • ****
  • Posts: 370
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #6 on: October 11, 2010, 02:34:59 am »


               Our original module setup relies on newer computer hardware to overcome such worrisome things such as this.  DelayCommand away lol.  



Seriously now, for years we did fairly well with many NPCs wandering our cities all day, bosses in distant locations standing about with henchmen for hours, no doubt board out of their minds, and all of those prespawned NPCs set to respawn based on a delay command and some variables written to the module itself.  With 25 players on, I'd have to imagine there were hundreds of delay commands running and I'm certain our ancient little rock of a server could have been more responsive.  Still though, the NPCs always respawned and I never noticed it failing to spawn one.



Since then I've revamped many of these NPCs to spawn from encounters or other means.  Had I done this before we upgraded our server, I think it would have had a noticable benefit.  However, we got a new host and server which I'm sure uses its brute force computing power to trivialize the building sins of our past '<img'>



But I will say this, we have a message from the module heartbeat script that tells DMs how many heartbeats have passed since the last load.  I have reset the server and checked it against the computer clock and I've noticed it can run for a number of hours longer before getting off significantly than it did before...  granted this may have nothing to do with delaycommands and just the difference in the number of players, but I like to think it helped.
               
               

               
            

Legacy_Shadooow

  • Hero Member
  • *****
  • Posts: 7698
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #7 on: October 15, 2010, 12:57:37 pm »


               Yea, its wise to spare delay commands at minimum if possible, but I don't think its needed to replace them all via some time comparsion check which some of scripters describe as a must...



Just get rid of any TMIs and you should be fine.
               
               

               
            

Legacy_the.gray.fox

  • Full Member
  • ***
  • Posts: 214
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #8 on: October 15, 2010, 09:12:56 pm »


               Delayed commands are not inherently good or bad.
They are tools. To be used appropriately.

But there is an important detail for delayed commands, that should be remembered.
FunkySwerve already pointed out that delayed commands use a separate stack.
In that same stack also end up any and all parameters you pass to the delayed command.
The more the params, the greater and quicker the stack explosion.
Which leads to quicker and badder memory fragmentation, which is bad for performance.

As a general rule of thumb, for delayed commands it is best to limit the param count to max 2.
Use more if you need, but do not make it a habit.
The occasional use of a 15-params delayed command will not kill your module... but you can find better ways to move that much data around without pushing everything on the stack.


-fox
               
               

               
            

Legacy_FunkySwerve

  • Hero Member
  • *****
  • Posts: 2325
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #9 on: October 15, 2010, 10:11:46 pm »


               Well said fox, I've been told that as well. To flesh out the 'better ways' a bit: we tend to pass data through vars set on objects, if the need arises, other than maybe a param to pass a second object for convenience's sake (typically getting the first object from OBJECT_SELF, as in most cases we're running the delay via an AssignCommand on the object of interest).



Even in this, though, as you say, using even commands with multiple parameters isn't a problem, so long as you don't do it everywhere. Take, for example, our fix for persistent AoE spells (heartbeats of spawned-in placeables and AoEs are VERY low priority in the engine, failing to fire under even relatively light loads, requiring pseuedoheartbeat treatment):




void DoAoEHeartbeat (struct SpellInfo si, object oAoE, effect eEff, effect eDur, effect eVis) {
    int bDestroy = FALSE, bAllPCs = FALSE;

    if (!GetIsObjectValid(oAoE))
        return;


    /* Storm of Vengeance doesn't destroy properly sometimes */
    if (GetPersistentAoEEffect(si) == AOE_PER_STORM) {
        if (si.id == SPELL_STORM_OF_VENGEANCE && GetLocalInt(GetArea(oAoE), "Area_Underwater"))
            bAllPCs = TRUE;

        if (GetLocalInt(GetModule(), "uptime") > GetLocalInt(oAoE, "AoEExpires"))
            bDestroy = TRUE;
    }

    if (bDestroy                     ||
        !GetIsObjectValid(si.caster) ||
        GetIsDead(si.caster)         ||
        GetArea(si.caster) != si.area) {

        SetPlotFlag(oAoE, FALSE);
        DestroyObject(oAoE);
        return;
    }

    DelayCommand(6.0, DoAoEHeartbeat(si, oAoE, eEff, eDur, eVis));
    AddLocalInt(oAoE, "AoERounds", 1);


    si.target = oAoE;
    int nMask = GetPersistentAoETargetMask(si);
    int nType = GetPersistentAoETargetType(si);

    for (si.target = GetFirstInPersistentObject(oAoE, nMask);
         GetIsObjectValid(si.target);
         si.target = GetNextInPersistentObject(oAoE, nMask)) {

        if (GetIsSpellTarget(si, si.target, nType) || (bAllPCs && GetIsPC(si.target))) {
            SignalEvent(si.target, EventSpellCastAt(si.caster, si.id));

            AssignCommand(si.caster, DelayCommand(GetRandomDelay(0.5, 4.5), ApplyAoEEffect(si, oAoE, si.target, eEff, eDur, eVis)));
        }
    }
}

Note that the primary param is a fairly sizeable struct, and effects are essentially smaller structs, though they can't be accessed as such in vanilla nwn. That's a LOT of data, but we still don't have problems with them. Of course, there aren't very many PAoEs running at any given time.



Funky
               
               

               
            

Legacy_Dark Defiance

  • Full Member
  • ***
  • Posts: 125
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #10 on: October 17, 2010, 03:15:49 am »


               If I read some where correctly (ages ago), assigning actions (in this case a DelayCommand()) to areas or the module, would give it a "higher" priority. Not sure if that it would mean they can still be "dropped". If I'm wrong someone let me know.

But my general rule for Delays is to execute them as fast as possible. If you need to execute multiple commands, put it in its own void() and delay that.
               
               

               


                     Modifié par Dark Defiance, 17 octobre 2010 - 02:16 .
                     
                  


            

Legacy_the.gray.fox

  • Full Member
  • ***
  • Posts: 214
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #11 on: October 18, 2010, 05:59:19 pm »


               Hello.

I did find in some spellscript a comment hinting at the average size of each variable in nwscript.
Each should cost you about 30 bytes because, as is always the case with scipts, there is more data (bytes) attached to each variable than just the one implied by the adopted datatype.

To make it short, a 32-bit integer would cost about 30 + 4 = 34 bytes.
Each primitive type is subject to this added cost.

While modern computers have RAM to burn and CPUs chewing data like they never did before, memory consumption and fragmentation are still a factor.
Add to the picture that NWN is the first to break the golden rule of "what you borrow you must return" (--> memory leaks) and the need of be gentle on memory holds true for nwscript today like yesterday.

In a scenario where much data has to be passed around, on a regular basis, through a delay, and you -for any reason- deem it better to push it on the stack, a clever trick might be adopted to save lot of bytes.

Imagine you have a structure of 10 members, each an integer.
Its average memory size would be (30 + 4) x 10 = 340 bytes (assuming there is not even more data with it because it is a "structure").

If those integers can not be squeezed together like in a bitfield, they can still be packed into a single string datatype.
The cost for the string would be the usual 30 bytes, plus the string size (byte count).

In the worst scenario where each of the 10 integers holds a value in the billions (so, 10 digits each), a string containing them all would count about 10 bytes x 10 integers = 100 bytes.
Plus 1 separator character (like a dot), which makes another 9 bytes.
(if all integers have constant size, the separator is not needed)

This string would have a size of about 109 bytes.
Add to it the usual 30 bytes for the nwscript extra data, and we hit ~139 bytes.
Which is far less than the 340 of keeping those integers separate.

If speed is not a concern (frankly... is it? We got uber CPUs today), we may even operate a conversion from integer to Hex, thus dropping from 10 to 8 digits per integer, culling 20 more bytes.

More tricks can be adopted to further cut the byte count, but you get the picture.

-fox
               
               

               


                     Modifié par the.gray.fox, 18 octobre 2010 - 05:02 .
                     
                  


            

Legacy_FunkySwerve

  • Hero Member
  • *****
  • Posts: 2325
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #12 on: October 19, 2010, 09:11:57 am »


               Yup, we use that trick all the time - in the case of the spell script example above, convenience simply won out, since the spellinfo struct is in all our spells, not just the dozen or so persistent aoes. Here's a handy bunch of string manipulation functions for anyone who wants them, courtesy of acaos. Holler at me if I missed any included functions - these were culled from a 4k line include.


struct IntList {
    int size;

    int i0,  i1,  i2,  i3,  i4;
    int i5,  i6,  i7,  i8,  i9;
};

struct StringList {
    int size;

    string s0,  s1,  s2,  s3;
    string s4,  s5,  s6,  s7;
    string s8,  s9,  s10, s11;
    string s12, s13, s14, s15;
};

struct SubString {
    string first, rest;
};


/* Replace the first (or all) occurrence of sFind in sStr with sRepl. */
string ReplaceString(string sStr, string sFind, string sRepl, int bAll=FALSE, int nLen=-1);

/* Return the specified digit nDigit of nInt. nDigit is the number of
 * powers of 10 to use (hence, 0 = 1s, 1 = 10s, 2 = 100s, etc). */
int GetIntegerDigit(int nInt, int nDigit);

/* Set the specified digit nDigit of nInt. */
int SetIntegerDigit(int nInt, int nDigit, int nValue);

/* Return an IntList struct containing all the digits of nInt. */
struct IntList GetDigitList(int nInt);

/* Get a random floating-point number in the range [-1.0, 1.0]. If fRange
 * is specified, a random number in the range [0.0, fRange] will be returned. */
float RandomFloat(float fRange=0.0);

/* Return the substring at the given index nIndex of sString, separating
 * sString with sSep. */
string GetStringSubString(string sString, int nIndex, string sSep=" ");

/* Return the number of substrings sString would have if it were separated
 * by sSep. */
int GetStringSubStrings(string sString, string sSep=" ");

/* Return a random string of sString separated by sSep. */
string GetRandomSubString(string sString, string sSep=" ");

/* Return a SubString struct containing the first substring of sString
 * separated by sSep, as well as the remainder of sString. The idiom
 * would be:
 *
 *   struct SubString ss;
 *
 *   ss.rest = sString;
 *
 *   while (ss.rest != "") {
 *       ss = GetFirstSubString(ss.rest);
 *
 *       (do things here)
 *   }
 */
struct SubString GetFirstSubString(string sString, string sSep = " ");

/* Return a StringList struct containing strings extracted from sString
 * separated by sSep. */
struct StringList GetStringList(string sString, string sSep=" ", int nLimit=16);

/* Return an IntList struct containing integers extracted from sString
 * separated by sSep. */
struct IntList GetIntList(string sString, string sSep=" ", int nLimit=10);




/* Return vPos in string form X, Y, Z. */
string GetPositionStringFromVector(vector vPos);

/* Return the position of lLoc in string form [X, Y, Z | F] */
string GetPositionStringFromLocation(location lLoc);

/* Return the position of oObject in string form [X, Y, Z | F] */
string GetPositionString(object oObject);




string ReplaceString(string sStr, string sFind, string sRepl, int bAll=FALSE, int nLen=-1) {
    int nPos = FindSubString(sStr, sFind);

    if (nPos
        return sStr;

    if (nLen
        nLen = GetStringLength(sFind);

    do {
        sStr = GetStringLeft(sStr, nPos) + sRepl +
               GetStringRight(sStr, GetStringLength(sStr) - (nPos + nLen));

        if (!bAll)
            break;
        nPos = FindSubString(sStr, sFind);
    } while (nPos >= 0);

    return sStr;
}

int GetIntegerDigit(int nInt, int nDigit) {
    switch (nDigit) {
        case 0: nDigit = 1; break;
        case 1: nDigit = 10; break;
        case 2: nDigit = 100; break;
        case 3: nDigit = 1000; break;
        case 4: nDigit = 10000; break;
        case 5: nDigit = 100000; break;
        case 6: nDigit = 1000000; break;
        case 7: nDigit = 10000000; break;
        case 8: nDigit = 100000000; break;
        case 9: nDigit = 1000000000; break;
        default:
            return 0;
    }

    return ((nInt / nDigit) % 10);
}

int SetIntegerDigit(int nInt, int nDigit, int nValue) {
    if (nValue
        nValue = 0;
    else if (nValue > 9)
        nValue = 9;

    switch (nDigit) {
        case 0: nDigit = 1; break;
        case 1: nDigit = 10; break;
        case 2: nDigit = 100; break;
        case 3: nDigit = 1000; break;
        case 4: nDigit = 10000; break;
        case 5: nDigit = 100000; break;
        case 6: nDigit = 1000000; break;
        case 7: nDigit = 10000000; break;
        case 8: nDigit = 100000000; break;
        case 9: nDigit = 1000000000; break;
        default:
            return 0;
    }

    nInt -= ((nInt / nDigit) % 10) * nDigit;
    nInt += nValue * nDigit;

    return nInt;
}

struct IntList GetDigitList(int nInt) {
    struct IntList ret;

    ret.i0 = (nInt % 10);
    ret.i1 = (nInt /= 10) % 10;
    ret.i2 = (nInt /= 10) % 10;
    ret.i3 = (nInt /= 10) % 10;
    ret.i4 = (nInt /= 10) % 10;
    ret.i5 = (nInt /= 10) % 10;
    ret.i6 = (nInt /= 10) % 10;
    ret.i7 = (nInt /= 10) % 10;
    ret.i8 = (nInt /= 10) % 10;
    ret.i9 = (nInt /= 10) % 10;

    return ret;
}

float RandomFloat(float fRange=0.0) {
    if (fRange > 0.0)
        return (Random(FloatToInt(fRange * 1000)) / 1000.0);

    return ((Random(10000001) - 5000000) / 5000000.0);
}

float RandomNormal(float fStdDev=1.0, float fMean=0.0) {
    float u1, u2, v1, v2, s;

    do {
        u1 = RandomFloat();
        u2 = RandomFloat();
        v1 = (2.0 * u1) - 1.0;
        v2 = (2.0 * u2) - 1.0;
        s  = (v1 * v1) + (v2 * v2);
    } while (s > 1.0);

    return (sqrt((-2.0 * log(s)) / s) * v1 * fStdDev) + fMean;
}

string GetStringSubString(string sString, int nIndex, string sSep=" ") {
    int nSep, nStart = 0, nSkip = GetStringLength(sSep);

    while (nIndex-- > 0) {
        nSep = FindSubString(sString, sSep, nStart);

        if (nSep
            return "";

        nStart = nSep + nSkip;
    }

    if ((nSep = FindSubString(sString, sSep, nStart))
        nSep = GetStringLength(sString);

    return GetSubString(sString, nStart, (nSep - nStart));
}

int GetStringSubStrings(string sString, string sSep=" ") {
    int nCount = 1, nStart = 0;
    int nSep = FindSubString(sString, sSep);
    int nSkip = GetStringLength(sSep);

    if (nSkip
        return 1;

    while (nSep >= 0) {
        nCount++;
        nSep = FindSubString(sString, sSep, nSep + nSkip);
    }

    return nCount;
}

string GetRandomSubString(string sString, string sSep=" ") {
    int nCount = GetStringSubStrings(sString, sSep);

    return GetStringSubString(sString, Random(nCount), sSep);
}

struct SubString GetFirstSubString(string sString, string sSep = " ") {
    struct SubString ret;
    int nSep = FindSubString(sString, sSep);

    if (nSep
        ret.first = sString;
        ret.rest  = "";

        return ret;
    }

    ret.first = GetStringLeft(sString, nSep);
    ret.rest  = GetStringRight(sString, GetStringLength(sString) - (nSep + GetStringLength(sSep)));

    return ret;
}

struct StringList GetStringList(string sString, string sSep=" ", int nLimit=16) {
    int nCount = 0, nSep = 0, nStart = 0;
    int nSkip = GetStringLength(sSep);
    int nLen = GetStringLength(sString);
    struct StringList sl;

    if (nSkip
        sl.size = 0;
        return sl;
    }

    while (nSep
        nSep = FindSubString(sString, sSep, nStart);
        if (nSep
            nSep = nLen;

        switch (nCount & 1) {
            case 0: switch (nCount++) {
                case  0: sl.s0  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case  2: sl.s2  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case  4: sl.s4  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case  6: sl.s6  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case  8: sl.s8  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case 10: sl.s10 = GetSubString(sString, nStart, (nSep - nStart)); break;
                case 12: sl.s12 = GetSubString(sString, nStart, (nSep - nStart)); break;
                case 14: sl.s14 = GetSubString(sString, nStart, (nSep - nStart)); break;
            }
            break;

            case 1: switch (nCount++) {
                case  1: sl.s1  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case  3: sl.s3  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case  5: sl.s5  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case  7: sl.s7  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case  9: sl.s9  = GetSubString(sString, nStart, (nSep - nStart)); break;
                case 11: sl.s11 = GetSubString(sString, nStart, (nSep - nStart)); break;
                case 13: sl.s13 = GetSubString(sString, nStart, (nSep - nStart)); break;
                case 15: sl.s15 = GetSubString(sString, nStart, (nSep - nStart)); break;
            }
            break;
        }

        nStart = nSep + nSkip;
    }

    sl.size = nCount;

    return sl;
}

struct IntList GetIntList(string sString, string sSep=" ", int nLimit=10) {
    int nCount = 0, nSep = 0, nStart = 0;
    int nLen = GetStringLength(sString);
    int nSkip = GetStringLength(sSep);
    struct IntList il;

    if (nSkip
        il.size = 0;
        return il;
    }

    while (nSep
        nSep = FindSubString(sString, sSep, nStart);
        if (nSep
            nSep = nLen;

        switch (nCount++) {
            case  0: il.i0  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  1: il.i1  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  2: il.i2  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  3: il.i3  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  4: il.i4  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  5: il.i5  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  6: il.i6  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  7: il.i7  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  8: il.i8  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
            case  9: il.i9  = StringToInt(GetSubString(sString, nStart, (nSep - nStart))); break;
        }

        nStart = nSep + nSkip;
    }

    il.size = nCount;

    return il;
}

string GetPositionStringFromVector(vector vPos) {
    return FloatToString(vPos.x, 1, 2) + ", " +
           FloatToString(vPos.y, 1, 2) + ", " +
           FloatToString(vPos.z, 1, 2);
}

string GetPositionStringFromLocation(location lLoc) {
    vector vPos = GetPositionFromLocation(lLoc);

    return "[" + GetPositionStringFromVector(GetPositionFromLocation(lLoc)) +
        " | " + FloatToString(GetFacingFromLocation(lLoc), 1, 0) + "]";
}

string GetPositionString(object oObject) {
    return GetPositionStringFromLocation(GetLocation(oObject));
}

location GetLocationFromString(object oArea, string sLoc) {
    float fFacing;
    vector vVec;

    vVec = Vector(StringToFloat(GetStringSubString(sLoc, 0, ",")),
                  StringToFloat(GetStringSubString(sLoc, 1, ",")),
                  StringToFloat(GetStringSubString(sLoc, 2, ",")));
    fFacing = StringToFloat(GetStringSubString(sLoc, 3, ","));

    return Location(oArea, vVec, fFacing);
}

If speed is not a concern (frankly... is it? We got uber CPUs today), we may even operate a conversion from integer to Hex, thus dropping from 10 to 8 digits per integer, culling 20 more bytes.

Nice. Speed is ALWAYS an issue. Given the size of strings that NWNX throws left and right, however (it uses large string groups as well, with a ~ delimiter), I wouldn't bother with the hex conversion in this case. '<img'>

Funky
               
               

               


                     Modifié par FunkySwerve, 19 octobre 2010 - 08:13 .
                     
                  


            

Legacy_Khuzadrepa

  • Sr. Member
  • ****
  • Posts: 347
  • Karma: +0/-0
DelayCommand Potential Issues
« Reply #13 on: October 19, 2010, 03:39:24 pm »


               

the.gray.fox wrote...
In that same stack also end up any and all parameters you pass to the delayed command.
The more the params, the greater and quicker the stack explosion.
Which leads to quicker and badder memory fragmentation, which is bad for performance.

As a general rule of thumb, for delayed commands it is best to limit the param count to max 2.

This is an awesome tip, thanks!