Difference between revisions of "Chapter 7"

From SphereWiki
Jump to: navigation, search
Line 214: Line 214:
 
REMOVE
 
REMOVE
 
ENDFOR</spherescript>
 
ENDFOR</spherescript>
 +
 +
 +
===FORINSTANCES===
 +
You may have seen scripts that do something like the following:
 +
 +
 +
<spherescript>[FUNCTION removespawns]
 +
FORITEMS 9999
 +
IF (<BASEID> == i_worldgem_bit)
 +
REMOVE
 +
ENDIF
 +
ENDFOR</spherescript>
 +
 +
 +
At a glance this script looks like a fairly useful script that can be used to remove all of your i_worldgem_bit (spawn) items from the server. If you attempt to run it you will most certainly notice that your server pauses for a signicantly long time (on a well-populated server you may even end up waiting a whole minute for the script to run), even if you only have several instances of the item on the server! The reason that this happens is because ''FORITEMS 9999'' will loop through ''every'' item within the 9999 tile radius, so the code will be looping hundreds or thousands of times when you only wanted to affect a handful of items!
 +
 +
 +
To resolve this issue we have FORINSTANCES. This is a special loop which will loop through all instances of a given character or item BASEID that exist on the server. This offers the following advantages over using ''FORITEMS/FORCHARS 9999'':
 +
* Sphere will only run your script for objects with the BASEID you are interested in.
 +
* Sphere knows how many instances of the object exist, so can abort the loop at an appropriate time.
 +
* Items inside containers (e.g. character backpacks or player banks) will not be missed out.
 +
 +
 +
So with this in mind, the above script can be rewritten to the following:
 +
 +
 +
<spherescript>[FUNCTION removespawns]
 +
FORINSTANCES i_worldgem_bit
 +
REMOVe
 +
ENDFOR</spherescript>
 +
 +
 +
Now when you run this script you should notice a significant drop in the execution time.
 +
 +
 +
'''Note:''' Since Sphere still has to internally search for references to the objects you're after you may find that on well-populate servers there is still a noticable pause. If you need to use this kind of loop then you should do so sparingly, and if you need to regularly use this then you may wish to consider finding a more optimal way of implementing your script(s).
  
  

Revision as of 23:59, 1 June 2009

(WIP)

Recursive Functions

I discovered this very seldom explored extension of SPHERE scripting while reading messages on the boards. Someone was trying to create a function that counted the number of items in a container using this sort of thing, and it worked for the most part. I was very amazed, because before that, no one had even thought of using functions that looped back upon themselves.


Which is what a recursive function is. I'll say it one more time.


A recursive function is one that calls itself, or recurs. Surprise!


So how do we do this? It's as simple as calling a function:


[FUNCTION recursive_test]
recursive_test


This very small piece of code in fact IS a recursive function. As you can see, the function will call itself and start over from the beginning, which will proceed to call the function again, and again, and again, and on and on. In this case, we don't have any way to stop it. This is called an infinite loop, one that will continue forever without stopping. Your server will die a flaming death.


Lesson 1: How to NOT create an infinite loop

Let me tell you right now. You will write a script that implements an infinite loop. You will test it. Your server will die. It's guaranteed. No programmer can say that they have never accidentally written an infinite loop. (RANDOM NOTE: All windows programs are in fact infinite loops. Your SPHERE server is an infinite loop.) In a SPHERE script, however, here's what happens:

  1. The function is called.
  2. Some stuff takes place
  3. The function is called from within itself. Being a good scripting language, it records where it left off so it can go back later. This is called the stack.
  4. Go back to number 1.

This "stack" builds up very very quickly, and soon the server cannot allocate any more memory for it, and will crash when it tries. Fun stuff, I tell you.


Anyways, here is one way to avoid creating an infinite loop. Let's say we want to make a function that executes SRC.SYSMESSAGE Hello World 35 times. Here would be an example of how one could do this:


[FUNCTION recurse_hello]
IF (<ARGN1> < 1)
    RETURN 1
ENDIF
SYSMESSAGE Hello World
RECURSE_HELLO <EVAL <ARGN1> - 1>
RETURN 1


Then, in another script, we would execute this command: SRC.RECURSE_HELLO 35


Remember what ARGN is from the previous chapter? It's the argument to the function stored as a number. Initially, as you can see, it's 25 because we made it be that way. However, every time the function calls itself, or "recurses", it sets ARGN1 to be one less than itself. Here's the step-by-step analysis of this:

  1. The function is called. ARGN1 is 35 because we said so.
  2. It checks to see if ARGN1 is less than one. If it is, we immediately RETURN 1 and set off the chain reaction that stops the recursive function.
  3. The next part should be fairly obvious. We're sending a SYSMESSAGE to the default object. Because we used SRC when we initially called the function, the default object is SRC.
  4. This is where the recursion takes place. The function calls itself with an argument ONE LESS than the current one. This starts the whole thing over at step 1. This is a NEW FUNCTION CALL, remember. The original function call STILL EXISTS and the program will "rewind" back down the stack to that location later. That is why I have a RETURN 1 after the function call.


That's your example of a recursive function. It isn't very practical. Let's look at a more practical example. See if you can figure it out for yourself. This is courtesy of Belgar, for the most part:


[FUNCTION pack_to_bank]
IF (<FINDLAYER.21.FINDCONT.0.UID> == 0)
    RETURN 1
ENDIF
FINDLAYER.21.FINDCONT.0.CONT = <FINDLAYER.layer_bankbox.UID>
PACK_TO_BANK
RETURN 1


(As you can see, we don't always need an ARGS to make a function loop. In this case, we use a backpack with an unknown number of items inside and only stop when the pack no longer contains items.)


Recursive functions are very useful. Be sure you don't overuse them, though! Remember, while a script is running, YOUR SERVER IS FROZEN. If a recursive function takes too long to complete, your server will lag. A good method is to make sure that no function should be looping more than about 500 times. (Actually other server emulators such as POL have a mechanism to catch "runaway scripts" like this and halt them in their tracks.)


FOR

FOR

FOR is a powerful way to create a recursive function, and it allows a simpler level of control over your recursions.

Usage:

[FUNCTION for_display]
FOR X 1 20
    SYSMESSAGE <LOCAL.X> //Will sysmessage the current for count.
ENDFOR


The loop will loop through 20 times, starting at 1 and ending at 20. X is the variable containing the current FOR count. If no variable is declared, the count can be accessed using <LOCAL._FOR>


Changing LOCAL._FOR or whatever you declared as count, will not change the loop's behaviour. But be aware that if you "stack" FOR loops without giving them different loop variables, the innermost loop will overwrite the loop variables of its successors, usually leading towards a completely messup of the whole loop stuff.


FORCHARLAYER

FORCHARLAYER is another type of FOR loop. Basically it allows you to loop through each item that is stored on the specified layer of a character. This can be useful for when you want to manipulate all of the spell runes or memory items on a character as an alternative to using FINDLAYER.x in a loop.


Something to be aware of here is that whilst inside the loop, the default object will be temporarily changed to the item in the loop. As you can see in the following example, we must store a reference to the original default object (the character) so that we can still reference it from within the loop:


[FUNCTION get_mitems_names]
REF1 = <UID> // store the default object in REF1
FORCHARLAYER 30
    REF1.SYSMESSAGE <NAME> is a memory item in layer <LAYER>
ENDFOR


FORCHARMEMORYTYPE

FORCHARMEMORYTYPE is a very useful type of FOR loop. You may want to use it for experience systems, and some player and NPC killing systems. It loops through every memory item on a character that has a specified flag.


For example, a character has 4 memory items with the following names and flags:

Ellessar 02000
Sorea 022bc
Introvert 0740d
Enrath 0c40d

Script:

[FUNCTION get_war_targ_mems]
REF1 = <UID> // As with FORCHARLAYER, the default object changes within the loop
FORCHARMEMORYTYPE memory_war_targ // Loop through memory items with flag 02000=memory_war_targ
    REF1.SYSMESSAGE There is a memory item with name <NAME>, uid <UID>, flags <COLOR> and one of its flag is also 02000.
ENDFOR


Result:
On your screen you would see:

There is a memory item with name Ellessar, uid 04f000001, flags 02000 and one of its flag is also 02000.
There is a memory item with name Sorea, uid 04f000002, flags 022bc and one of its flag is also 02000.
There is a memory item with name Introvert, uid 04f000003, flags 0740d and one of its flag is also 02000.
</tt>


Note that there are only three messages, because the memory item "Enrath" does not have the flag 02000.


FORCHARS

FORCHARS is a FOR loop that you can use to check all mobiles (player and NPC) within a set radius of an object.


The correct syntax being FORCHARS x where x is the radius in tiles the loop will cover.

  • FORCHARS 2 would check any mobile within a 2 tile radius
  • FORCHARS 18 would check the area inside your screen
  • FORCHARS 6144 would check the entire world map


One example of a function using FORCHARS

[FUNCTION kill_vendors]
FORCHARS 6144 // checks entire map
    IF (<BRAIN> == brain_vendor) //argument for what will be acted upon within this function
        KILL // action
    ENDIF
ENDFOR


As with all FOR loops you have to stipulate inside the loop what it is to act upon, if you are restricting it to certian players/npcs (or in the case of FORITEMS, items) otherwise it will perform the action upon all players/npcs within the radius of the loop.


In this case the loop checks for any Vendor npc's and kills them.


FORCLIENTS, FORPLAYERS

FORCLIENTS and FORPLAYERS are FOR loops, both are used to affect a clients/players in certain radius. If you do not set the radius, radius 18 is used as default. While FORCLIENTS only acts on player characters who are logged in, FORPLAYERS acts on each and every player character, even if logged off.


Usage:

[FUNCTION radius_players]
FORCLIENTS 25
    IF (<ACCOUNT.PLEVEL> <= 1) // Affects only logged in players, not staff
        SAY I am here!
    ENDIF
ENDFOR


FORCONT

FORCONT is a type of FOR loop. It loops through every item in a container. The default object inside the loop will be the item currently being looped over.


Usage:

[FUNCTION rem_spellbooks]
FORCONT <FINDLAYER.21.UID> 10 // <FINDLAYER.21.UID> - UID of a container, 10 - how many subcontainers the function goes through, if set 0, it affects only items in container with UID
    IF (<BASEID> == i_spellbook)
        REMOVE
    ENDIF
ENDFOR


FORCONTID

FORCONTID is a FOR loop that works in a similar way to FORCONT, except that it will only cycle through items that have a specific BASEID. You can set the amount of subcontainers to loop through, like the FORCONT example.


Usage:

[FUNCTION rem_spellbooks2]
FORCONTID i_spellbook 10
    REMOVE
ENDFOR


FORCONTTYPE

This is another FOR loop that is almost identical to FORCONTYPE. The only difference is that instead of looping through items with a specific BASEID, it will loop through items with a specific TYPE. Following the spellbooks remover example:


Usage:

[FUNCTION rem_spellbook3]
FORCONTTYPE t_spellbook
    REMOVE
ENDFOR


FORINSTANCES

You may have seen scripts that do something like the following:


[FUNCTION removespawns]
FORITEMS 9999
    IF (<BASEID> == i_worldgem_bit)
        REMOVE
    ENDIF
ENDFOR


At a glance this script looks like a fairly useful script that can be used to remove all of your i_worldgem_bit (spawn) items from the server. If you attempt to run it you will most certainly notice that your server pauses for a signicantly long time (on a well-populated server you may even end up waiting a whole minute for the script to run), even if you only have several instances of the item on the server! The reason that this happens is because FORITEMS 9999 will loop through every item within the 9999 tile radius, so the code will be looping hundreds or thousands of times when you only wanted to affect a handful of items!


To resolve this issue we have FORINSTANCES. This is a special loop which will loop through all instances of a given character or item BASEID that exist on the server. This offers the following advantages over using FORITEMS/FORCHARS 9999:

  • Sphere will only run your script for objects with the BASEID you are interested in.
  • Sphere knows how many instances of the object exist, so can abort the loop at an appropriate time.
  • Items inside containers (e.g. character backpacks or player banks) will not be missed out.


So with this in mind, the above script can be rewritten to the following:


[FUNCTION removespawns]
FORINSTANCES i_worldgem_bit
    REMOVe
ENDFOR


Now when you run this script you should notice a significant drop in the execution time.


Note: Since Sphere still has to internally search for references to the objects you're after you may find that on well-populate servers there is still a noticable pause. If you need to use this kind of loop then you should do so sparingly, and if you need to regularly use this then you may wish to consider finding a more optimal way of implementing your script(s).


FORITEMS

FORITEMS works in much the same way that FORCHARS does, except it checks for ITEMS within the set radius as opposed to characters. Default obejct is set to the item which can be affected.


A basic example of a function using FORITEMS:


[FUNCTION Spawn_remover]
FORITEMS 6144 //once again it checks the entire map
    IF (<TYPE> == t_spawn_char) //if this arguement is met
        REMOVE //remove it
    ENDIF //end the IF arguement
ENDFOR //end the FOR loop


FOROBJS

FOROBJS works in the same way that FORITEMS and FORCHARS does with the exception that this loop will find both characters AND items within the specified radius.


WHILE

WHILE is a conditional loop, a block of code that will repeat itself whilst a given condition is true. Basically you can think of this as being an IF..ENDIF block that will run indefinately until the IF statement returns false.


ALWAYS AND ALWAYS:

  1. End WHILE blocks with ENDWHILE.
  2. Perform some action within the WHILE block that will change the outcome of the conditional statement.


For example:

[FUNCTION remove_all_mems]
WHILE (<FINDID.i_memory.UID>)   // continue to loop whilst an i_memory item is found inside the object
FINDID.i_memory.REMOVE  // remove a memory item
ENDWHILE


Whilst inside the loop, LOCAL._WHILE can be used to access the number of times that the script has looped so far. In some situations you may want to use this to impose a 'limit' on how many times your WHILE block can loop before it is forced to exit. An example of this could be:


[FUNCTION random_health]
WHILE (<HITS> > 10) && (<LOCAL._WHILE> < 20)  // loop whilst the character has more than 10 health, but no more than 20 times
HITS = <R1,100>    // set the character's health to a random value between 1 and 100
ENDWHILE


Since random numbers are.. random.. it is in theory possible that the health will never be set to a value less than or equal to 10. By checking LOCAL._WHILE inside the WHILE condition we add protection against the script looping indefinately and freezing the server.