Difference between revisions of "Scripting"
(→CHECK or IF) |
|||
(20 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
− | + | =Introduction= | |
− | + | The scripting system is currently used for the behaviour of characters and items. For characters this is only part of the behaviour, as many more systems influence this in the following priority: | |
− | <ul><li>Story: executing Osiris Tasks</li> | + | <ul> |
+ | <li>Story: executing Osiris Tasks</li> | ||
<li>Dialogs: dialog behaviour and animations</li> | <li>Dialogs: dialog behaviour and animations</li> | ||
<li>Statuses: dying, frozen, petrified, ...</li> | <li>Statuses: dying, frozen, petrified, ...</li> | ||
<li>Scripts: executing the current reaction or scriptframe</li></ul> | <li>Scripts: executing the current reaction or scriptframe</li></ul> | ||
− | + | Scripts are the lowest priority and will only execute if none of the other systems want to do something. For items it's a lot simpler, as the scripting system is the only thing | |
+ | that influences the items behaviour. | ||
− | + | To create and modify scripts, use the [[Script_editor|script editor]]. Make sure to also [[Activating_Scripts|activate them]] so they can be checked for errors and actually apply to objects. | |
− | |||
− | < | + | <br /> |
− | + | =Object Naming= | |
− | Inheritance is done with the USING keyword and copies everything from the other script into the current script. This includes all global variables, reactions, scriptframes, and events. You can use USING SHARED if you intend to overwrite reactions, events, or scriptframes.<br> | + | When you with to refer to objects/local instances from script or elsewhere, make sure to follow the [[Templates_explanation#Local_instances.2Ftemplates|correct naming convention]]. |
− | An example INIT section: | + | =Sections= |
− | < | + | Before any section in a script, we can INCLUDE files. When you INCLUDE a file, you're telling the system to parse that file first. |
− | + | <br /> | |
+ | ===INIT=== | ||
+ | In the INIT section you can declare global variables and inherit from other scripts. Global variables always start with % and can be accessed and modified everywhere (including other scripts, osiris, and in code). There are also EXTERN variables, which are exposed when you assign a script to an object so you can change the value for local instances of the script. The INIT section also contains the special variable '__Me', which contains the owner of the script. This is a character or item depending on the script type. Code will automatically fill this variable.<br /> | ||
+ | Inheritance is done with the USING keyword and copies everything from the other script into the current script. This includes all global variables, reactions, scriptframes, and events. You can use USING SHARED if you intend to overwrite reactions, events, or scriptframes.<br /> | ||
+ | An example INIT section: | ||
+ | <pre> | ||
+ | INIT | ||
USING SHARED Base | USING SHARED Base | ||
CHARACTER:__Me | CHARACTER:__Me | ||
Line 23: | Line 30: | ||
</pre> | </pre> | ||
− | + | If you want to override a specific reaction, event, or scriptframe you have to specify this with the OVERRIDE keyword: | |
<pre> | <pre> | ||
REACTION TestReaction, 5 OVERRIDE | REACTION TestReaction, 5 OVERRIDE | ||
Line 29: | Line 36: | ||
SCRIPTFRAME TestFrame OVERRIDE | SCRIPTFRAME TestFrame OVERRIDE | ||
</pre> | </pre> | ||
− | + | If needed, you can also only inherit specific reactions, events, or scriptframes: | |
− | |||
<pre> | <pre> | ||
USING ScriptName REACTION ReactionName,NewPriority | USING ScriptName REACTION ReactionName,NewPriority | ||
Line 36: | Line 42: | ||
USING ScriptName SCRIPTFRAME ScriptFrameName | USING ScriptName SCRIPTFRAME ScriptFrameName | ||
</pre> | </pre> | ||
− | + | <br /> | |
− | < | + | ===BEHAVIOUR=== |
− | + | This section contains the actual behaviour of the object. It solely consists of a whole bunch of reactions, of which only 1 reaction can be active at the same time. The current reaction gets selected based on its priority and its CHECK. The priority defines how much it wants to be executed and the CHECK decided whether its possible to be executed. The reaction with the highest priority whose CHECK succeeds will be selected for execution: | |
− | <ul><li><span style="background-color:transparent;font-size:1em">The whole list of reactions is always sorted from HIGH to LOW priorities.</span></li> | + | <ul> |
+ | <li><span style="background-color:transparent;font-size:1em">The whole list of reactions is always sorted from HIGH to LOW priorities.</span></li> | ||
<li><span style="background-color:transparent;font-size:1em">We then check in that order all the reactions with a higher priority of the current reaction and see if their CHECK succeeds.</span></li> | <li><span style="background-color:transparent;font-size:1em">We then check in that order all the reactions with a higher priority of the current reaction and see if their CHECK succeeds.</span></li> | ||
− | <li><span style="background-color:transparent;font-size:1em">As soon as a higher priority reaction's CHECK succeeds, it interrupts the current reaction and will start executing.</span></li></ul> | + | <li><span style="background-color:transparent;font-size:1em">As soon as a higher priority reaction's CHECK succeeds, it interrupts the current reaction and will start executing.</span></li> |
− | + | </ul> | |
− | + | <b>Important note</b>: only the CHECKs of higher priority reactions are evaluated every frame. This means that the current reaction's CHECK is <b>NOT</b> reevaluated while it's executing! It could become invalid during the execution. This is also true when calling Reset(): execution simply restarts at the beginning of the current reaction without evaluating the CHECK conditions again. | |
− | <ul><li>USAGE COMBAT: can only be executed during combat when its the turn of the object</li> | + | On top of the priority and CHECKs there's also the USAGE keyword. A reaction needs to specify a USAGE context. You can pick the following USAGE contexts: |
+ | <ul> | ||
+ | <li>USAGE COMBAT: can only be executed during combat when its the turn of the object</li> | ||
<li>USAGE WAITING: can only be executed during combat when waiting for its turn</li> | <li>USAGE WAITING: can only be executed during combat when waiting for its turn</li> | ||
<li>USAGE PEACE: can only be executed when not in combat</li> | <li>USAGE PEACE: can only be executed when not in combat</li> | ||
− | <li>USAGE ALL: can always be executed</li></ul | + | <li>USAGE ALL: can always be executed</li> |
− | + | </ul> | |
− | + | A reaction can have its own local variables. These variables have to start with an underscore and can only be accessed within the reaction itself.<br /> | |
− | <ul><li>An underscore ('_'): this variable is not relevant for the event, it will not prevent the event from firing</li> | + | As soon as a reaction is interrupted, the INTERRUPT event will be called immediately and you can execute some code to for example Reset the reaction. You can catch all INTERRUPTS or only specific interrupts (like the movement failed interrupt) and execute actions on the interrupt. If you catch a specific interrupt event you have the possibility to fill in variables for the event: |
+ | <ul> | ||
+ | <li>An underscore ('_'): this variable is not relevant for the event, it will not prevent the event from firing</li> | ||
<li>A constant (e.g. "TestString") or a global variable: the variable has to be equal to this constant, otherwise the event will not be executed</li> | <li>A constant (e.g. "TestString") or a global variable: the variable has to be equal to this constant, otherwise the event will not be executed</li> | ||
− | <li>A local variable: this variable will get filled with the variable from the event</li></ul> | + | <li>A local variable: this variable will get filled with the variable from the event</li> |
− | + | </ul> | |
+ | Some examples: | ||
<pre> | <pre> | ||
OnEnteredCombat(__Me, _) // Only catch the event when __Me entered combat, the second variable one is irrelevant | OnEnteredCombat(__Me, _) // Only catch the event when __Me entered combat, the second variable one is irrelevant | ||
Line 59: | Line 71: | ||
OnEnteredCombat(_LocalCharacterVariable, _) // Always catches the event, and _LocalCharacterVariable contains the character that entered combat. BEWARE: _LocalCharacterVariable can be null if an item entered combat | OnEnteredCombat(_LocalCharacterVariable, _) // Always catches the event, and _LocalCharacterVariable contains the character that entered combat. BEWARE: _LocalCharacterVariable can be null if an item entered combat | ||
</pre> | </pre> | ||
− | + | The actions during an interrupt are limited to immediate/simple actions, which can be executed immediately (e.g. CharacterMoveTo() is not allowed). Keep in mind though that a reaction can be interrupted for many reasons: | |
− | <ul><li>Higher priority reaction takes over.</li> | + | <ul> |
+ | <li>Higher priority reaction takes over.</li> | ||
<li>Other system takes over: statuses/dialog/story</li> | <li>Other system takes over: statuses/dialog/story</li> | ||
<li>An exception was thrown in the script: e.g. pathfinding failed, ...</li> | <li>An exception was thrown in the script: e.g. pathfinding failed, ...</li> | ||
− | <li>The object got culled (more details on culling can be found later on this page)</li></ul> | + | <li>The object got culled (more details on culling can be found later on this page)</li> |
− | + | </ul> | |
− | + | <b>Important note</b>: if a reaction gets interrupted and later gets resumed, it will continue executing where it got interrupted! (unless "Reset" was called in the INTERRUPT handler, in which case it will begin from the start again)<br /> | |
+ | An example BEHAVIOUR section: | ||
<pre> | <pre> | ||
BEHAVIOUR | BEHAVIOUR | ||
Line 72: | Line 86: | ||
VARS | VARS | ||
CHARACTER:_TestCharacter | CHARACTER:_TestCharacter | ||
− | CHECK "(c1| | + | CHECK "(c1|c2)&!c3" // Reaction can be executed when one of the first 2 conditions passes, and the third condition doesn't pass |
IsInSurface(__Me, SurfaceOil) | IsInSurface(__Me, SurfaceOil) | ||
IsInDangerousSurface(__Me) | IsInDangerousSurface(__Me) | ||
Line 84: | Line 98: | ||
Reset() | Reset() | ||
</pre> | </pre> | ||
− | + | <br /> | |
− | < | + | ===STORY=== |
− | + | There are specific story "reactions" called scriptframes. These are almost exactly the same as reactions, but they don't have a priority or CHECK block. They are managed by Story (Osiris) and can be SET or INJECTED. We keep a stack of all currently set scriptframes. If something is on the stack, the current REACTION always gets interrupted! The stack is the highest priority and comes before BEHAVIOUR reactions! Example of stack behaviour: | |
− | <ul><li>When you push something, it is put on the top of the stack.</li> | + | <ul> |
− | <li>When you pop, the top scriptframe is removed (happens when it finished executing)</li></ul> | + | <li>When you push something, it is put on the top of the stack.</li> |
− | + | <li>When you pop, the top scriptframe is removed (happens when it finished executing)</li> | |
− | If you SET a scriptframe the stack gets cleared (Abort!) and the new scriptframe gets pushed on the stack.<br> | + | </ul> |
− | If you INJECT a scriptframe, it will just push a copy of it on the stack. You can keep injecting the same scriptframe on the stack and it will be executed multiple times.<br> | + | In our case:<br /> |
− | As soon as the TOP scriptframe is finished, it pops from the stack and the one below it starts/resumes executing.</ | + | If you SET a scriptframe the stack gets cleared (Abort!) and the new scriptframe gets pushed on the stack.<br /> |
− | + | If you INJECT a scriptframe, it will just push a copy of it on the stack. You can keep injecting the same scriptframe on the stack and it will be executed multiple times.<br /> | |
− | + | As soon as the TOP scriptframe is finished, it pops from the stack and the one below it starts/resumes executing.<br /> | |
− | + | Just like with reactions, if you INJECT a new scriptframe and there was already a scriptframe on the stack active, it will get interrupted and later resume where it was before. The syntax for writing a SCRIPTFRAME is exactly the same as a REACTION (except for the lack of priority and CHECK, and the fact that they need to be in the STORY section). | |
− | + | <br /> | |
− | In a reaction or scriptframe, each frame only 1 action gets executed at most. Let's take a piece of script from a REACTION: | + | ===EVENTS=== |
+ | This section contains all the events that we want to catch. Events are always <b>immediately</b> and <b>completely</b> executed! It cannot take multiple frames: it's a blocking call and will execute and return immediately when it's done. Even when the object is culled or offstage will it execute its events. Events are always thrown to all objects in the current level. An event can be thrown from code, osiris, or from script itself.<br /> | ||
+ | In a reaction or scriptframe, each frame only 1 action gets executed at most. Let's take a piece of script from a REACTION: | ||
<pre> | <pre> | ||
Set(%TargetPos,%Enemy) | Set(%TargetPos,%Enemy) | ||
Line 103: | Line 119: | ||
CharacterMoveTo(%TargetPos) | CharacterMoveTo(%TargetPos) | ||
</pre> | </pre> | ||
− | + | In the example above, the first 2 lines will take 2 frames (1 frame per line). The third one will take as long as it takes for the character to move to the target position. However, in an event the actions get executed immediately. Even if it was 100 actions with IFs and WHILEs, it would be executed as it was 1 action. For that reason, you cannot use actions in events that need time (Sleep, CharacterMoveTo, ...). Additionally, the number of actions an event can execute is limited. If you exceed the limit, Osiris will log an error and exit the event. In fact, let me amaze you even more. If you throw a CharacterEvent in an event, that spawns a new event and is thrown on every character and item... even all those events are executed immediately before it gets to the next line.<br /> | |
− | + | Just like INTERRUPTs in REACTIONs, EVENTs have to react on something (INTERRUPTs can and EVENTs must). The syntax is equal as well (more information on the event variables can be found in the REACTION INTERRUPT section).<br /> | |
− | An example EVENTS section: | + | An example EVENTS section: |
<pre> | <pre> | ||
EVENT TestInitEvent | EVENT TestInitEvent | ||
Line 112: | Line 128: | ||
ACTIONS | ACTIONS | ||
IF "!c1" | IF "!c1" | ||
− | IsEqual(% | + | IsEqual(%StartTrigger, null) |
THEN | THEN | ||
− | + | TeleportTo(__Me, %StartTrigger) | |
− | |||
ENDIF | ENDIF | ||
</pre> | </pre> | ||
− | + | <br /> | |
− | < | + | =Variables= |
− | < | + | ===Types=== |
− | + | <ul> | |
− | + | <li>INTEGER and INTEGER64: 'whole' numbers. The difference between INTEGER and INTEGER64 is not relevant for the scripter (unless you care about really, really big numbers), as long as you make sure to pass the right type in actions. For example: -10, 0, 8, 102324, ...</li> | |
+ | <li>FLOAT: all other numbers. For example: 0.1, -0.323, 8.3, ...</li> | ||
+ | <li>STRING and FIXEDSTRING: the difference between STRING and FIXEDSTRING is not important when scripting, just make sure to pass the correct type in actions. For example: "", "testString", "testing123", ... . <b>Important note:</b> do not start a string with the characters '%' or '_', as they will get recognized as a variable instead (it's not intended, it's a bug).</li> | ||
+ | <li>CHARACTER, ITEM, TRIGGER, and SPLINE: objects that are currently loaded in the game. This includes globals and locals of the curently loaded level. The script will assert if the script uses an object that isn't currently loaded. It will also warn you if the character's name does not start with the prefix 'S_'. This prefix helps to prevent objects being deleted because they aren't used (they could be used in script). For example: S_GLO_Dallis, S_MyFirstItem, ...</li> | ||
+ | <li>RELATION_TYPE: only used in a handful actions. A list of possible relation types can be found [[scripting_relation_types|here]].</li> | ||
+ | <li>SKILL_ID: the name/id of a skill as it can be found in [[skill_creation|SkillData]]. For example: Target_Bless, Projectile_Fireball, ...</li> | ||
+ | <li>SURFACE_TYPE: surface types can be found [[scripting_surface_types|here]].</li> | ||
+ | <li>STATUS_TYPE: status types can be found [[Scripting_status_types|here]].</li> | ||
+ | <li>CHARACTERSTAT_TYPE: characterstat types can be found [[Scripting_characterstat_types|here]].</li> | ||
+ | <li>ITEMSTAT_TYPE: itemstat types can be found [[Scripting_itemstat_types|here]].</li> | ||
+ | <li>WEAPON_TYPE: weapon types can be found [[Scripting_weapon_types|here]].</li> | ||
+ | <li>DAMAGE_TYPE: damage types can be found [[Scripting_damage_types|here]].</li> | ||
+ | <li>TALENT_TYPE: talent types can be found [[Scripting_talent_types|here]].</li> | ||
+ | <li>COMPARE_TYPE: compare types can be found [[Scripting_compare_types|here]].</li> | ||
+ | <li>COMPARE_FUNCTION: compare functions can be found [[Scripting_compare_functions|here]].</li> | ||
+ | <li>FLOAT3: floats are written as {number;number;number}, and can contain integers as well</li> | ||
+ | <li>CHARACTERTEMPLATE, ITEMTEMPLATE, and PROJECTILETEMPLATE: the script will warn you if the template's name does not start with the prefix 'S_'. This prefix helps to prevent objects being deleted because they aren't used (they could be used in script). For example: S_Chair_A, S_Human_Male_B, ...</li> | ||
+ | <li>POTION_ID: all loaded potions from stats</li> | ||
+ | <li>ABILITY_TYPE: ability types can be found [[Scripting_ability_types|here]].</li> | ||
+ | <li>DEATH_TYPE: death types can be found [[Scripting_death_types|here]].</li> | ||
+ | <li>STATUS_ID: all loaded statuses from stats</li> | ||
+ | <li>ARCHETYPE: all available archetypes that are loaded. More information on archetypes can be found [[CombatAi_Archetypes_And_Modifiers|here]].</li> | ||
+ | <li>SURFACE_TRANSFORM_TYPE: surface transform types can be found [[Scripting_surface_transform_types|here]].</li> | ||
+ | </ul> | ||
+ | <br /> | ||
+ | ===Lists=== | ||
+ | Lists are a special parameter type and contain multiple parameters of a single type.<br /> | ||
+ | <b>Important note</b>: Lists are more convenient, but they are slower than normal parameters. They also have a limited size, so don't try to fill them with thousands of entries.<br /> | ||
+ | Declaring a list: | ||
<pre> | <pre> | ||
LIST<INT>:%TestListInt // Fine | LIST<INT>:%TestListInt // Fine | ||
Line 164: | Line 207: | ||
LIST<STRING>:%TestString // Using STRING as the type | LIST<STRING>:%TestString // Using STRING as the type | ||
</pre> | </pre> | ||
+ | Now, how would you use these lists? | ||
+ | <br /> | ||
− | + | =Important Events= | |
− | + | ===OnInit and OnShutDown=== | |
− | + | OnInit is called each time on all objects in the level when the level is loaded, and OnShutdown is called when the level is unloaded. This would be the place where you can create/destroy your looping effects that should always be on the object. | |
− | + | <br /> | |
− | + | ===OnLoaded=== | |
− | + | OnLoaded is called when the savegame is loaded or when the levelcache is loaded (levelswap). The version of the savegame is passed so you can do your patching "hacks" if necessary. | |
− | + | <br /> | |
+ | ===Interrupts=== | ||
+ | Interrupts are only called on reactions and can only be caught on that reaction: | ||
<ul> | <ul> | ||
<li>OnBetterReactionFound: a higher priority reaction succeeded its check</li> | <li>OnBetterReactionFound: a higher priority reaction succeeded its check</li> | ||
Line 179: | Line 226: | ||
<li>OnMovementFailed: pathfinding has failed to find a path to the target</li> | <li>OnMovementFailed: pathfinding has failed to find a path to the target</li> | ||
<li>OnException: a character task has failed</li></ul> | <li>OnException: a character task has failed</li></ul> | ||
− | < | + | <br /> |
− | + | ===OnFunction=== | |
− | + | You can create your own "functions' with <b>CallFunction("functionName")</b> and catching the event <b>OnFunction("functionName")</b>. This way you can share functionality between multiple events and reactions. You can not pass parameters directly, but you could set some function-specific global variables which the function uses and can even 'return' results in. | |
− | + | <br /> | |
− | + | =Culling= | |
− | The updating characters are: | + | For performance reasons we only update a small set of all the characters & items in the level.<br /> |
+ | The updating characters are: | ||
<ul> | <ul> | ||
<li>Force updating (can be set in script/story, but should only be used in an emergency!)</li> | <li>Force updating (can be set in script/story, but should only be used in an emergency!)</li> | ||
Line 191: | Line 239: | ||
<li>In range of the party (within 40m)</li> | <li>In range of the party (within 40m)</li> | ||
</ul> | </ul> | ||
− | + | The updating items are: | |
<ul> | <ul> | ||
<li>Force updating (can be set in script/story using ItemSetForceSynch(), but should only be used in an emergency!)</li> | <li>Force updating (can be set in script/story using ItemSetForceSynch(), but should only be used in an emergency!)</li> | ||
Line 198: | Line 246: | ||
<li>In range of the party (within 40m)</li> | <li>In range of the party (within 40m)</li> | ||
</ul> | </ul> | ||
− | + | This means that in most cases objects far away from the party get culled and stop updating. When an object doesn't update anymore this means the behaviour is interrupted and won't be executed anymore. However, events always get executed, so culling has no impact on this. Another consequence of culling is that timers of that script are not ticking anymore and are basically paused. | |
− | + | <br /> | |
− | + | =Debugging= | |
− | + | There are 4 ways to help you debug: | |
<ul> | <ul> | ||
<li>The script debugger in the script editor</li> | <li>The script debugger in the script editor</li> | ||
Line 208: | Line 256: | ||
<li>Start the script log for an object, which logs as much stuff as possible about the script (reactions, interrupts, events, ...)</li> | <li>Start the script log for an object, which logs as much stuff as possible about the script (reactions, interrupts, events, ...)</li> | ||
</ul> | </ul> | ||
− | + | <br /> | |
− | < | + | ===Script Debugger=== |
− | + | See the [[script_debugger|Script Debugger]] page for more information. | |
− | + | <br /> | |
− | + | ===Script Screen=== | |
− | + | On the left side of the Script Screen it shows the script variables, the script reactions (ordered from high priority to low), and the current Event it catches. It colors the reactions gray (not checked), green (check passed), and red (check failed). At the right you can see the current reaction's conditions and actions. You can see which action is currently executing and you can see the return results of the conditions from the check: gray (not evaluated), green (TRUE), and red (FALSE).<br /> | |
− | You can filter in the script variables through the [[console|console]]by using: "filtervar <filter>". | + | You can filter in the script variables through the [[console|console]] by using: "filtervar <filter>". |
− | <b>Tip:</b> type "filtervar asdf" or something to remove all variables so you can see all the reactions!<br> | + | <b>Tip:</b> type "filtervar asdf" or something to remove all variables so you can see all the reactions!<br /> |
− | + | You can also change which reaction you want to see at the right, by entering the number of that reaction in the console. This way you can easily check why certain reactions are never succeeding their CHECK by checking the return values of the conditions. To do this first you must activate this mode, by entering "show server" in the console. Then you can type the number of the wanted reaction or type "0" to show the current reaction again. | |
− | You can also change which reaction you want to see at the right, by entering the number of that reaction in the console. This way you can easily check why certain reactions are never succeeding their CHECK by checking the return values of the conditions. To do this first you must activate this mode, by entering "show server" in the console. Then you can type the number of the wanted reaction or type "0" to show the current reaction again.</ | + | <br /> |
− | + | ===DebugText=== | |
− | + | You can put DebugText in events/scriptframes/reactions to print text in the world on any object you want. This way you can easily see if you get in the code you wrote and you can also print any variable's value at that time. To print the variables we use the same syntax as translated strings: [1] [2] [3] ...<br /> | |
− | + | For example: | |
− | For example: | ||
<pre> | <pre> | ||
DebugText(__Me,"Target: [1], Speed is [2]",_Target,%Speed) | DebugText(__Me,"Target: [1], Speed is [2]",_Target,%Speed) | ||
</pre> | </pre> | ||
− | + | For characters, items, triggers, splines, and templates the name will be shown in the debug text. Numbers will be shown in full, and all other variables will be directly converted to strings. | |
− | < | + | <br /> |
− | + | ===ScriptLog=== | |
+ | You can also start/stop the scriptlog in console with: | ||
<ul> | <ul> | ||
<li>"ai log all": Start the scriptlog for all character/items.</li> | <li>"ai log all": Start the scriptlog for all character/items.</li> | ||
Line 233: | Line 281: | ||
<li>"ai log selected": Toggle the separate scriptlog for the selected (CTRL+SHIFT+CLICK) object</li> | <li>"ai log selected": Toggle the separate scriptlog for the selected (CTRL+SHIFT+CLICK) object</li> | ||
</ul> | </ul> | ||
− | + | It will generate scriptlog file(s) right next to the executable. | |
+ | <br /> | ||
+ | =Common Mistakes= | ||
+ | ===Reset()=== | ||
+ | <pre> | ||
+ | REACTION CastSkill, 1000 | ||
+ | USAGE COMBAT | ||
+ | VARS | ||
+ | FLOAT:_minRange | ||
+ | FLOAT:_maxRange | ||
+ | CHECK "!c1&c2&c3" | ||
+ | IsEqual(%SkillTarget,null) | ||
+ | CharacterCanCast(__Me,Projectile_Fireball,0) | ||
+ | CharacterGetSkillRange(_minRange,_maxRange,__MeProjectile_Fireball) | ||
+ | ACTIONS | ||
+ | CharacterMoveInRange(%SkillTarget,_minRange,_maxRange,1) | ||
+ | PlayEffectAt(__Me, "FX_GP_HugeFireball_A") | ||
+ | CharacterUseSkill(Projectile_Fireball,%SkillTarget) | ||
+ | </pre> | ||
+ | <p>In this case we have a reaction that will move in range of the target and as soon as we are in range we will play an effect and cast a fireball. However, if this reaction gets interrupted during the playeffect, next time this reaction becomes active again we will just cast a fireball on the target even when we would not be in range and didn't play the effect. This is why in these cases you should add an INTERRUPT to Reset() the reaction.</p> | ||
+ | |||
+ | ===CHECK or IF=== | ||
+ | <pre> | ||
+ | REACTION CastSkill,1000 | ||
+ | USAGE COMBAT | ||
+ | VARS | ||
+ | FLOAT:_minRange | ||
+ | FLOAT:_maxRange | ||
+ | CHECK "!c1&c2" | ||
+ | IsEqual(%SkillTarget,null) | ||
+ | CharacterGetSkillRange(_minRange,_maxRange,__MeProjectile_Fireball) | ||
+ | ACTIONS | ||
+ | IF "c1" | ||
+ | CharacterCanCast(__Me,Projectile_Fireball,0) | ||
+ | THEN | ||
+ | CharacterMoveInRange(%SkillTarget,_minRange,_maxRange,1) | ||
+ | PlayEffectAt(__Me, "FX_GP_HugeFireball_A") | ||
+ | CharacterUseSkill(Projectile_Fireball,%SkillTarget) | ||
+ | ENDIF | ||
+ | INTERRUPT | ||
+ | ACTIONS | ||
+ | Reset() | ||
+ | </pre> | ||
+ | In this case we're checking something in the reaction when it's executing, but we don't do anything if the IF fails. There is no ELSE behaviour. This means that in combat this reaction could keep failing and not do anything, which will cause a ForceEndTurn for that character. Here we should have checked CharacterCanCast in the CHECK and not write a separate IF check. In general when the IF check in a REACTION is blocking the whole REACTION from executing it's probably better to place it in the CHECK instead. | ||
+ | <br /> | ||
+ | |||
+ | ===Check order=== | ||
+ | <pre> | ||
+ | IF "c1&c2&!c3&c4" | ||
+ | CharacterGetStat(_Vitality, __Me, Vitality) // Fetch before checking | ||
+ | IsGreaterThen(_Vitality, 0.5) | ||
+ | IsEqual(_Character, null) // Null check before checking | ||
+ | CharacterIsPlayer(_Character) | ||
+ | THEN | ||
+ | // Do something | ||
+ | ENDIF | ||
+ | </pre> | ||
+ | Make sure to perform your checks in the right order. Before checking a stat make sure to fetch it. Before checking if the character is a player consider checking if it is a valid player. Could it be the player is off stage? Could it be dead? Is it even set? | ||
+ | <br /> | ||
+ | ===AD interrupts=== | ||
+ | |||
+ | [[Category:Behaviour_Scripts]] |
Latest revision as of 19:20, 11 December 2018
Contents
Introduction
The scripting system is currently used for the behaviour of characters and items. For characters this is only part of the behaviour, as many more systems influence this in the following priority:
- Story: executing Osiris Tasks
- Dialogs: dialog behaviour and animations
- Statuses: dying, frozen, petrified, ...
- Scripts: executing the current reaction or scriptframe
Scripts are the lowest priority and will only execute if none of the other systems want to do something. For items it's a lot simpler, as the scripting system is the only thing that influences the items behaviour.
To create and modify scripts, use the script editor. Make sure to also activate them so they can be checked for errors and actually apply to objects.
Object Naming
When you with to refer to objects/local instances from script or elsewhere, make sure to follow the correct naming convention.
Sections
Before any section in a script, we can INCLUDE files. When you INCLUDE a file, you're telling the system to parse that file first.
INIT
In the INIT section you can declare global variables and inherit from other scripts. Global variables always start with % and can be accessed and modified everywhere (including other scripts, osiris, and in code). There are also EXTERN variables, which are exposed when you assign a script to an object so you can change the value for local instances of the script. The INIT section also contains the special variable '__Me', which contains the owner of the script. This is a character or item depending on the script type. Code will automatically fill this variable.
Inheritance is done with the USING keyword and copies everything from the other script into the current script. This includes all global variables, reactions, scriptframes, and events. You can use USING SHARED if you intend to overwrite reactions, events, or scriptframes.
An example INIT section:
INIT USING SHARED Base CHARACTER:__Me CHARACTER:%AnotherCharacter=null FLOAT3:%CurrentPosition=null EXTERN INT:%CanRun=1
If you want to override a specific reaction, event, or scriptframe you have to specify this with the OVERRIDE keyword:
REACTION TestReaction, 5 OVERRIDE EVENT TestEvent OVERRIDE SCRIPTFRAME TestFrame OVERRIDE
If needed, you can also only inherit specific reactions, events, or scriptframes:
USING ScriptName REACTION ReactionName,NewPriority USING ScriptName EVENT EventName USING ScriptName SCRIPTFRAME ScriptFrameName
BEHAVIOUR
This section contains the actual behaviour of the object. It solely consists of a whole bunch of reactions, of which only 1 reaction can be active at the same time. The current reaction gets selected based on its priority and its CHECK. The priority defines how much it wants to be executed and the CHECK decided whether its possible to be executed. The reaction with the highest priority whose CHECK succeeds will be selected for execution:
- The whole list of reactions is always sorted from HIGH to LOW priorities.
- We then check in that order all the reactions with a higher priority of the current reaction and see if their CHECK succeeds.
- As soon as a higher priority reaction's CHECK succeeds, it interrupts the current reaction and will start executing.
Important note: only the CHECKs of higher priority reactions are evaluated every frame. This means that the current reaction's CHECK is NOT reevaluated while it's executing! It could become invalid during the execution. This is also true when calling Reset(): execution simply restarts at the beginning of the current reaction without evaluating the CHECK conditions again. On top of the priority and CHECKs there's also the USAGE keyword. A reaction needs to specify a USAGE context. You can pick the following USAGE contexts:
- USAGE COMBAT: can only be executed during combat when its the turn of the object
- USAGE WAITING: can only be executed during combat when waiting for its turn
- USAGE PEACE: can only be executed when not in combat
- USAGE ALL: can always be executed
A reaction can have its own local variables. These variables have to start with an underscore and can only be accessed within the reaction itself.
As soon as a reaction is interrupted, the INTERRUPT event will be called immediately and you can execute some code to for example Reset the reaction. You can catch all INTERRUPTS or only specific interrupts (like the movement failed interrupt) and execute actions on the interrupt. If you catch a specific interrupt event you have the possibility to fill in variables for the event:
- An underscore ('_'): this variable is not relevant for the event, it will not prevent the event from firing
- A constant (e.g. "TestString") or a global variable: the variable has to be equal to this constant, otherwise the event will not be executed
- A local variable: this variable will get filled with the variable from the event
Some examples:
OnEnteredCombat(__Me, _) // Only catch the event when __Me entered combat, the second variable one is irrelevant OnEnteredCombat(_, %GlobalItemVariable) // Only catch the event when %GlobalItemVariable entered combat, the first variable is irrelevant OnEnteredCombat(_LocalCharacterVariable, _) // Always catches the event, and _LocalCharacterVariable contains the character that entered combat. BEWARE: _LocalCharacterVariable can be null if an item entered combat
The actions during an interrupt are limited to immediate/simple actions, which can be executed immediately (e.g. CharacterMoveTo() is not allowed). Keep in mind though that a reaction can be interrupted for many reasons:
- Higher priority reaction takes over.
- Other system takes over: statuses/dialog/story
- An exception was thrown in the script: e.g. pathfinding failed, ...
- The object got culled (more details on culling can be found later on this page)
Important note: if a reaction gets interrupted and later gets resumed, it will continue executing where it got interrupted! (unless "Reset" was called in the INTERRUPT handler, in which case it will begin from the start again)
An example BEHAVIOUR section:
BEHAVIOUR REACTION TestReaction, 5 // Priority 5 USAGE PEACE VARS CHARACTER:_TestCharacter CHECK "(c1|c2)&!c3" // Reaction can be executed when one of the first 2 conditions passes, and the third condition doesn't pass IsInSurface(__Me, SurfaceOil) IsInDangerousSurface(__Me) CharacterIsFloating(__Me) ACTIONS CharacterFleeFromSurface() INTERRUPT ON OnMovementFailed(_) // Only when interrupted by a failed movement ACTIONS Reset()
STORY
There are specific story "reactions" called scriptframes. These are almost exactly the same as reactions, but they don't have a priority or CHECK block. They are managed by Story (Osiris) and can be SET or INJECTED. We keep a stack of all currently set scriptframes. If something is on the stack, the current REACTION always gets interrupted! The stack is the highest priority and comes before BEHAVIOUR reactions! Example of stack behaviour:
- When you push something, it is put on the top of the stack.
- When you pop, the top scriptframe is removed (happens when it finished executing)
In our case:
If you SET a scriptframe the stack gets cleared (Abort!) and the new scriptframe gets pushed on the stack.
If you INJECT a scriptframe, it will just push a copy of it on the stack. You can keep injecting the same scriptframe on the stack and it will be executed multiple times.
As soon as the TOP scriptframe is finished, it pops from the stack and the one below it starts/resumes executing.
Just like with reactions, if you INJECT a new scriptframe and there was already a scriptframe on the stack active, it will get interrupted and later resume where it was before. The syntax for writing a SCRIPTFRAME is exactly the same as a REACTION (except for the lack of priority and CHECK, and the fact that they need to be in the STORY section).
EVENTS
This section contains all the events that we want to catch. Events are always immediately and completely executed! It cannot take multiple frames: it's a blocking call and will execute and return immediately when it's done. Even when the object is culled or offstage will it execute its events. Events are always thrown to all objects in the current level. An event can be thrown from code, osiris, or from script itself.
In a reaction or scriptframe, each frame only 1 action gets executed at most. Let's take a piece of script from a REACTION:
Set(%TargetPos,%Enemy) CharacterEvent(%Enemy,"Attack!") CharacterMoveTo(%TargetPos)
In the example above, the first 2 lines will take 2 frames (1 frame per line). The third one will take as long as it takes for the character to move to the target position. However, in an event the actions get executed immediately. Even if it was 100 actions with IFs and WHILEs, it would be executed as it was 1 action. For that reason, you cannot use actions in events that need time (Sleep, CharacterMoveTo, ...). Additionally, the number of actions an event can execute is limited. If you exceed the limit, Osiris will log an error and exit the event. In fact, let me amaze you even more. If you throw a CharacterEvent in an event, that spawns a new event and is thrown on every character and item... even all those events are executed immediately before it gets to the next line.
Just like INTERRUPTs in REACTIONs, EVENTs have to react on something (INTERRUPTs can and EVENTs must). The syntax is equal as well (more information on the event variables can be found in the REACTION INTERRUPT section).
An example EVENTS section:
EVENT TestInitEvent ON OnInit() ACTIONS IF "!c1" IsEqual(%StartTrigger, null) THEN TeleportTo(__Me, %StartTrigger) ENDIF
Variables
Types
- INTEGER and INTEGER64: 'whole' numbers. The difference between INTEGER and INTEGER64 is not relevant for the scripter (unless you care about really, really big numbers), as long as you make sure to pass the right type in actions. For example: -10, 0, 8, 102324, ...
- FLOAT: all other numbers. For example: 0.1, -0.323, 8.3, ...
- STRING and FIXEDSTRING: the difference between STRING and FIXEDSTRING is not important when scripting, just make sure to pass the correct type in actions. For example: "", "testString", "testing123", ... . Important note: do not start a string with the characters '%' or '_', as they will get recognized as a variable instead (it's not intended, it's a bug).
- CHARACTER, ITEM, TRIGGER, and SPLINE: objects that are currently loaded in the game. This includes globals and locals of the curently loaded level. The script will assert if the script uses an object that isn't currently loaded. It will also warn you if the character's name does not start with the prefix 'S_'. This prefix helps to prevent objects being deleted because they aren't used (they could be used in script). For example: S_GLO_Dallis, S_MyFirstItem, ...
- RELATION_TYPE: only used in a handful actions. A list of possible relation types can be found here.
- SKILL_ID: the name/id of a skill as it can be found in SkillData. For example: Target_Bless, Projectile_Fireball, ...
- SURFACE_TYPE: surface types can be found here.
- STATUS_TYPE: status types can be found here.
- CHARACTERSTAT_TYPE: characterstat types can be found here.
- ITEMSTAT_TYPE: itemstat types can be found here.
- WEAPON_TYPE: weapon types can be found here.
- DAMAGE_TYPE: damage types can be found here.
- TALENT_TYPE: talent types can be found here.
- COMPARE_TYPE: compare types can be found here.
- COMPARE_FUNCTION: compare functions can be found here.
- FLOAT3: floats are written as {number;number;number}, and can contain integers as well
- CHARACTERTEMPLATE, ITEMTEMPLATE, and PROJECTILETEMPLATE: the script will warn you if the template's name does not start with the prefix 'S_'. This prefix helps to prevent objects being deleted because they aren't used (they could be used in script). For example: S_Chair_A, S_Human_Male_B, ...
- POTION_ID: all loaded potions from stats
- ABILITY_TYPE: ability types can be found here.
- DEATH_TYPE: death types can be found here.
- STATUS_ID: all loaded statuses from stats
- ARCHETYPE: all available archetypes that are loaded. More information on archetypes can be found here.
- SURFACE_TRANSFORM_TYPE: surface transform types can be found here.
Lists
Lists are a special parameter type and contain multiple parameters of a single type.
Important note: Lists are more convenient, but they are slower than normal parameters. They also have a limited size, so don't try to fill them with thousands of entries.
Declaring a list:
LIST<INT>:%TestListInt // Fine LIST<CHARACTER>:%TestListCharacter // Fine LIST<LIST<INT>>:%TestListNested // Error, can't nest lists
Adding entries:
ListAdd(%TestListInt, 0) // Fine ListAdd(%TestListInt, {0;1;2}) // Error, trying to add a FLOAT3 to an INT LIST
Removing entries: indices start at 1, and end at the list size. All entries after the removed entry will be shifted to the left:
ListRemove(%TestListInt, 1) // Fine, assuming that the list has 1 or more entries ListRemove(%TestListInt, 0) // Error, invalid index
Setting or getting entries:
ListSet(%TestListInt, 1, %AnInt) // Fine ListGet(%TestListInt, 1, %AnInt) ListSet(%TestListInt, 1, %ACharacter) // Error, trying to set a CHARACTER in an INT LIST ListGet(%TestListInt, 1, %ACharacter) ListSet(%TestListInt, 0, %AnInt) // Error, invalid index ListGet(%TestListInt, 0, %AnInt)
Getting the list size:
ListGetSize(%TestList, %ListSize)
Clearing a list:
ListClear(%TestList)
Important note: Lists currently do not support EXTERN, constants, and STRING , meaning that the following lines will not work in scripts and result in an error during script build:
LIST<INT>:%TestList = {1, 2, 3, 4, 5} // Using a constant to initialize ListGetSize({1, 2, 3, 4}, %ListSize) // Using a constant in an action call EXTERN LIST<INT>:%TestList = {1, 2} // Using EXTERN and using a constant to initialize LIST<STRING>:%TestString // Using STRING as the type
Now, how would you use these lists?
Important Events
OnInit and OnShutDown
OnInit is called each time on all objects in the level when the level is loaded, and OnShutdown is called when the level is unloaded. This would be the place where you can create/destroy your looping effects that should always be on the object.
OnLoaded
OnLoaded is called when the savegame is loaded or when the levelcache is loaded (levelswap). The version of the savegame is passed so you can do your patching "hacks" if necessary.
Interrupts
Interrupts are only called on reactions and can only be caught on that reaction:
- OnBetterReactionFound: a higher priority reaction succeeded its check
- OnNoReactionFound: no reaction has succeeded its check
- OnScriptDisabled: another system has taken control (dialogs, statuses, story)
- OnManualInterrupt: the Interrupt action itself has caused the interrupt
- OnMovementFailed: pathfinding has failed to find a path to the target
- OnException: a character task has failed
OnFunction
You can create your own "functions' with CallFunction("functionName") and catching the event OnFunction("functionName"). This way you can share functionality between multiple events and reactions. You can not pass parameters directly, but you could set some function-specific global variables which the function uses and can even 'return' results in.
Culling
For performance reasons we only update a small set of all the characters & items in the level.
The updating characters are:
- Force updating (can be set in script/story, but should only be used in an emergency!)
- In Combat
- Executing Osiris Tasks
- In range of the party (within 40m)
The updating items are:
- Force updating (can be set in script/story using ItemSetForceSynch(), but should only be used in an emergency!)
- In Combat
- Moving
- In range of the party (within 40m)
This means that in most cases objects far away from the party get culled and stop updating. When an object doesn't update anymore this means the behaviour is interrupted and won't be executed anymore. However, events always get executed, so culling has no impact on this. Another consequence of culling is that timers of that script are not ticking anymore and are basically paused.
Debugging
There are 4 ways to help you debug:
- The script debugger in the script editor
- CTRL+SHIFT+ClICK on the object you wish to debug to show the Script Screen, and CTRL+SHIFT+SCROLL to the Script and/or Variables screen to see what the current state is.
- Add a whole bunch of DebugText output in your code to see which code is called in what order.
- Start the script log for an object, which logs as much stuff as possible about the script (reactions, interrupts, events, ...)
Script Debugger
See the Script Debugger page for more information.
Script Screen
On the left side of the Script Screen it shows the script variables, the script reactions (ordered from high priority to low), and the current Event it catches. It colors the reactions gray (not checked), green (check passed), and red (check failed). At the right you can see the current reaction's conditions and actions. You can see which action is currently executing and you can see the return results of the conditions from the check: gray (not evaluated), green (TRUE), and red (FALSE).
You can filter in the script variables through the console by using: "filtervar <filter>".
Tip: type "filtervar asdf" or something to remove all variables so you can see all the reactions!
You can also change which reaction you want to see at the right, by entering the number of that reaction in the console. This way you can easily check why certain reactions are never succeeding their CHECK by checking the return values of the conditions. To do this first you must activate this mode, by entering "show server" in the console. Then you can type the number of the wanted reaction or type "0" to show the current reaction again.
DebugText
You can put DebugText in events/scriptframes/reactions to print text in the world on any object you want. This way you can easily see if you get in the code you wrote and you can also print any variable's value at that time. To print the variables we use the same syntax as translated strings: [1] [2] [3] ...
For example:
DebugText(__Me,"Target: [1], Speed is [2]",_Target,%Speed)
For characters, items, triggers, splines, and templates the name will be shown in the debug text. Numbers will be shown in full, and all other variables will be directly converted to strings.
ScriptLog
You can also start/stop the scriptlog in console with:
- "ai log all": Start the scriptlog for all character/items.
- "ai log none": Stop the scriptlog for all character/items.
- "ai log selected": Toggle the separate scriptlog for the selected (CTRL+SHIFT+CLICK) object
It will generate scriptlog file(s) right next to the executable.
Common Mistakes
Reset()
REACTION CastSkill, 1000 USAGE COMBAT VARS FLOAT:_minRange FLOAT:_maxRange CHECK "!c1&c2&c3" IsEqual(%SkillTarget,null) CharacterCanCast(__Me,Projectile_Fireball,0) CharacterGetSkillRange(_minRange,_maxRange,__MeProjectile_Fireball) ACTIONS CharacterMoveInRange(%SkillTarget,_minRange,_maxRange,1) PlayEffectAt(__Me, "FX_GP_HugeFireball_A") CharacterUseSkill(Projectile_Fireball,%SkillTarget)
In this case we have a reaction that will move in range of the target and as soon as we are in range we will play an effect and cast a fireball. However, if this reaction gets interrupted during the playeffect, next time this reaction becomes active again we will just cast a fireball on the target even when we would not be in range and didn't play the effect. This is why in these cases you should add an INTERRUPT to Reset() the reaction.
CHECK or IF
REACTION CastSkill,1000 USAGE COMBAT VARS FLOAT:_minRange FLOAT:_maxRange CHECK "!c1&c2" IsEqual(%SkillTarget,null) CharacterGetSkillRange(_minRange,_maxRange,__MeProjectile_Fireball) ACTIONS IF "c1" CharacterCanCast(__Me,Projectile_Fireball,0) THEN CharacterMoveInRange(%SkillTarget,_minRange,_maxRange,1) PlayEffectAt(__Me, "FX_GP_HugeFireball_A") CharacterUseSkill(Projectile_Fireball,%SkillTarget) ENDIF INTERRUPT ACTIONS Reset()
In this case we're checking something in the reaction when it's executing, but we don't do anything if the IF fails. There is no ELSE behaviour. This means that in combat this reaction could keep failing and not do anything, which will cause a ForceEndTurn for that character. Here we should have checked CharacterCanCast in the CHECK and not write a separate IF check. In general when the IF check in a REACTION is blocking the whole REACTION from executing it's probably better to place it in the CHECK instead.
Check order
IF "c1&c2&!c3&c4" CharacterGetStat(_Vitality, __Me, Vitality) // Fetch before checking IsGreaterThen(_Vitality, 0.5) IsEqual(_Character, null) // Null check before checking CharacterIsPlayer(_Character) THEN // Do something ENDIF
Make sure to perform your checks in the right order. Before checking a stat make sure to fetch it. Before checking if the character is a player consider checking if it is a valid player. Could it be the player is off stage? Could it be dead? Is it even set?