Timers and Timings

I have a few various situations arise where I would like to understand or know of a way to use timers or some such with my scripting.
I’m not really sure how to go about this. I wondered if someone had a simple example script for me to analyse and breakdown, or any functions or Include scripts to look over?

Currently I’m working with lots of If (Random …) calls and variables, but its extremely convoluted.

A few examples:

  • A player clicks a placeable (bush) and gets an item (herb) - the bush will no longer give any herbs for a Random amount of time (between x mins and y mins?)

  • A player walks over a Trigger and Floating Text appears with descriptive text, the text wont appear again for x minutes.

  • A group of players kill a “boss” enemy, and that enemy will not spawn again for a random amount of time, between x mins and y mins.

Are the above things possible? They are just some simple examples I thought of that could help discuss the process.

Many thanks,
Roarthing

Yep. In examples 1 and 2, it’s as simple as flagging the bush/trigger, such as by setting an integer on them, and then randomly delaying the removal of the flag.

SetLocalInt(OBJECT_SELF, "RECENTLY_USED", 1);
DelayCommand(60.0, DeleteLocalInt(OBJECT_SELF, "RECENTLY_USED"));

^- Fixed time of 60.0 seconds, here.

Then, you can add an abort check based on the flag at the beginning of the script, aborting before anything happens:

if (GetLocalInt(OBJECT_SELF, "RECENTLY_USED"))
    return;

Random-float function, for getting a random time:

// Returns a random float between fMin and fMax.
float CBS_RandomFloat(float fMax=3.5, float fMin=0.0);
float CBS_RandomFloat(float fMax=3.5, float fMin=0.0)
{
    if (fMax <= fMin)
        return fMin;

    float fRandom = (fMax - fMin) * 10.0;
    int nRandom = FloatToInt(fRandom);

    nRandom  = Random(nRandom+1);
    fRandom  = IntToFloat(nRandom) / 10.0;

    return fRandom + fMin;
}

Updated.

In the case of the dead boss, I’ve personally done it the same way, just triggering a time-delayed respawn function rather than removing an integer.

It’d be a good idea to assign the delayed command to remove the flag to the flagged object, in cases where the caller of the script is not (edit: corrected nonsense) the flagged object. If the delayed command to remove a flag on a placeable is assigned to the player, and the player logs out before the timer is up, I think the flag might get stuck - whereas, if the flagged object no longer exists, it doesn’t matter that the flag on it doesn’t get removed.

In the case of usable objects like harvestable bushes, you could also temporarily set them unusable via SetUseableFlag. That’d spare players the effort of clicking bushes that can’t currently be harvested.

A “Can this code be optimized for cleanness and readability?”-topic where we post somewhat convoluted code and help one another minimize it actually sounds like a really great idea. :thinking: I’d love to get some how-to-write-optimally-clean-code tips, too.

2 Likes

If it’s meant to be for a PW, this might be helpful regarding spawnable placeables: https://neverwintervault.org/project/nwn1/script/acceraks-simple-spawn-n-clean-pws

From the viewpoint of a player I would prefer a spawning system that destroys the placeable after it has been harvested/mined etc. and respawnes it after some time so to spare me clicking for nothing (as TheBarbarian already mentioned)

2 Likes

I echo the Barbarian’s suggestions above - it took me longer to write my missive which then was confusing compared to the above.

2 Likes

Fantastic.

Thanks once again everyone for responses, especially @TheBarbarian.

Fantastic link @TheStoryteller01 , thanks; Will explore that!

@TheBarbarian 's method seems very clear and simple, it’s basically what I was trying to do, but maybe over thinking it.

The Random-float function is still losing me, which is annoying - I really want to understand it!

Would I create a new script (save as conditional) and then use #Include and then … ? or Would the new function be placed within the same script?

Thanks,
Frustrated of England

1 Like

Either is fine. If you’re going to be reusing the function across other scripts, it’d make sense to put it in a library, but if you’re only going to be using it within a single script, then keep it in that one.

The advantage of a script library is that you’ve only got a single instance of the function. Like this RandomFloat function here; I’m mostly using that for delays and ranges across spell scripts. Now, if I wanted to affect all the random floats in all my spell scripts at once, I could alter the function in the library, which all of those scripts are using, whereas, if each spell script had their own instance of the function, I’d have to open each spell script and put the edits to the function in one at a time.

If you plan ahead for that, you can save yourself a whole lot of refactoring (rewriting/restructuring/simplifying code without changing it’s external/visible capabilities) later on.

But to continue with the spell scripts example; while setting up new spell templates, I’m starting to repeatedly script similar functions for different spells - which I then later combine into merged, reusable functions that get added to the library. Without adding the individual spells and seeing what they need to work, I wouldn’t have the “individual instances” of code that I could later merge into functions. So I think there isn’t really one ultimate catch-all “Do it this way! This way is The Correct Way!”-method there, and if you want nice, clean, easy-to-use code, you’ll need to refactor it later one way or another.

For data and variable stuff like this, a library would make a lot of sense, though, since this is stuff that’s very likely to be reused in other scripts in the future.

RandomFloat explained:

Summary

This is a float type function, which means that it returns a float, and can be used anywhere a float is called for as a parameter, such as for DelayCommand()'s first parameter (a float).

DelayCommand(CBS_RandomFloat(300.0, 60.0), InsertDelayedFunctionCallHere);

For understanding functions, acquire a rubber duck, and then read the code to it one line at a time while it serenely smiles at you like the little duckie angel it is:

If max value is less than or equal to min value (user input error), always return min value.

Assuming a max value of 10.5, min value of 0.5.

10.5 - 0.5 = 10.0

Multiply remainder by ten:

100.0

Turn 100.0 into an integer; 100.

Get a random number from 0 to (99+1). (Random(100) is 0-99 because the zero counts as the first number… Random(2) returns either 0 or 1)

Let’s assume we got a 100 (max result).

Turn random number into float again and divide by ten:

100.0 / 10.0 = 10.0

Add the previously removed min value back in:

10.5

Return result.

::headscratch:: Don’t look at me, though. ::cough:: I don’t know what I’m doing either; I kinda just looked up other random float generating functions online and got annoyed that most of the ones I found never returned either the exact minimum or exact maximum value, so I tried to write one that does. This one seems to work as intended. Fingers crossed; it hasn’t made anything explode for me just yet. … except for all the actual explosions it’s being used for. :thinking:

God I hope I’m not publicly talking mathematical/logical nonsense. >_<

Edited because the glorious power of Socratic Teaching has revealed an unnecessary step. Hooray! One less line of code.

2 Likes

There is anothe way how to do this and thats calculating time difference. This is preffered choice for serious multiplayer PW development as DelayCommand is expensive function and might lag out module if overused.

Personally I use nwnx plugin for returning unix time, so I don’t have source code to show you as example but basically.

You calculate current time value. To do that, get current year, month, day, hour, substract it from starting values in module properties, then convert it to seconds, then add hours+minutes(as seconds)+seconds.

That gives you current game time since module was started. Store that into variable. Next time player wan’t to use the placeable check if new time value is greater than old time value by X seconds you needl.

I suppose the DelayCommand will be enough for you, just wanted to mention it isn’t the only way.

(And btw I agree with the placeable destroying method it is much transparent to the player)

2 Likes

i use these in Nwn2 – but can be adapted to 1 (eg. store the int on the module-object)

// Sets the start time for sVar-event.
void kL_SetIntervalStart(string sVar)
{
    SetGlobalInt(sVar, CalcCurrent());
}

// Gets how many seconds have passed since sVar-event.
// returns -1 if start event hasn't been set
int kL_GetIntervalElapsed(string sVar)
{
    int i = GetGlobalInt(sVar);
    if (i != 0)
        return CalcCurrent() - i;

    return -1;
}

// Resets the counter.
void kL_ClearInterval(string sVar)
{
    SetGlobalInt(sVar, 0);
}

// Returns current game-time in seconds.
int CalcCurrent()
{
    int iYear   = GetCalendarYear();
    int iMonth  = GetCalendarMonth();
    int iDay    = GetCalendarDay();
    int iHour   = GetTimeHour();
    int iMinute = GetTimeMinute();
    int iSecond = GetTimeSecond();

    return     iYear  * 12 * 28 * 24 * 60 * 60
            + (iMonth - 1) * 28 * 24 * 60 * 60
            + (iDay - 1)        * 24 * 60 * 60
            +  iHour                 * 60 * 60
            +  iMinute                    * 60
            +  iSecond;
}

Beware that the int will overflow if year is beyond … ~2000 (i haven’t calculated it exactly)

/as-is

2 Likes

Love this! Tried combining:

// Returns elapsed game-time in seconds.
int CalcElapsedTime()
{
    int iYear   = GetCalendarYear()  - 1372;
    int iMonth  = GetCalendarMonth() - 6;
    int iDay    = GetCalendarDay()   - 1;
    int iHour   = GetTimeHour()      - 13;
    int iMinute = GetTimeMinute();
    int iSecond = GetTimeSecond();

    return  iYear  * 12 * 28 * 24 * 60 * 60
         +  iMonth      * 28 * 24 * 60 * 60
         +  iDay             * 24 * 60 * 60
         +  iHour                 * 60 * 60
         +  iMinute                    * 60
         +  iSecond;
}

(edit: Cut extra ints back out; were unnecessary.)

Pro (?): No overflow risk from high year numbers.
Con (?): Need to manually alter int values if changing module starting time.

1 Like

yeah this is why i meant, and maybe you dont even need to alter the values, I suppose this will work correctly even when the function returns negative value?

alternatively another way I can see is to store GetCalendarYear() (and maybe also month+day but not needed) into local variable on module if the variable is 0 (ie. when such function is used for first time in module) and then use this number instead of 1372. This will then make it compatible with modules with year set to 9999 or something absurd like that without need to alter the function manually…

2 Likes

Tested, and confirming: Negative values work just fine. :smiley:

This is super cool; one could use this to script a “movie-style” module, with plot events happening according to time elapsed since start. ::head whirring with possibilities::

In SP I use a similar function:

// Return the current time in hours

int bhHour() {return ((GetCalendarYear() - 1) * 12 * 30 * 24) + ((GetCalendarMonth() - 1) * 30 * 24)
                   + ((GetCalendarDay() - 1) * 24) + GetTimeHour();}

The start time is captured in the module pre-enter script:

SetLocalInt(oPC, "StartHour", bhHour());

Then elapsed times are easily calculated.

DelayCommand is fine for actions that will be executed almost immediately, but I use the above for events that occur days, weeks or months later.

Another technique is to use heartbeat (or pseudo-heartbeat) to increment a counter (six seconds per heartbeat). This is useful when we might need to cancel the future event if circumstances have changed (unlike DelayCommand, which is fire-and-forget). The game clock function bhHour could be used instead, of course, but the counter is quicker and arguably clearer.

1 Like

I know it’s a very old thread, but I guess this neat function from @kevL_s

// Returns current game-time in seconds.
int CalcCurrent()
{
    int iYear   = GetCalendarYear();
    int iMonth  = GetCalendarMonth();
    int iDay    = GetCalendarDay();
    int iHour   = GetTimeHour();
    int iMinute = GetTimeMinute();
    int iSecond = GetTimeSecond();

    return     iYear  * 12 * 28 * 24 * 60 * 60
            + (iMonth - 1) * 28 * 24 * 60 * 60
            + (iDay - 1)        * 24 * 60 * 60
            +  iHour                 * 60 * 60
            +  iMinute                    * 60
            +  iSecond;
}

should be corrected a bit:

// Returns current game-time in seconds.
int CalcCurrent()
{
    int iYear   = GetCalendarYear();
    int iMonth  = GetCalendarMonth();
    int iDay    = GetCalendarDay();
    int iHour   = GetTimeHour();
    int iMinute = GetTimeMinute();
    int iSecond = GetTimeSecond();
    int iSecondsPerHour = FloatToInt(HoursToSeconds(1));

    return     iYear  * 12 * 28 * 24 * iSecondsPerHour
            + (iMonth - 1) * 28 * 24 * iSecondsPerHour
            + (iDay - 1)        * 24 * iSecondsPerHour
            +  iHour                 * iSecondsPerHour
            +  iMinute                    * 60
            +  iSecond;
}

There isn’t 60 minutes in one in-game hour. This value can vary.

P.S. Thanks to Terrahkitsune for the tip.

4 Likes