Difference between revisions of "Arcane University:Scripting Best Practices"

The Beyond Skyrim Wiki — Hosted by UESP
Jump to: navigation, search
(Redirect links to UESP ck wiki)
 
(7 intermediate revisions by 4 users not shown)
Line 1: Line 1:
{{Trail}}
+
{{Trail|Implementation}}
 
This is a guide of best practices to follow when writing Papyrus scripts.  
 
This is a guide of best practices to follow when writing Papyrus scripts.  
 +
 +
== Papyrus Scripting Best Practices ==
  
 
# '''COMMENT YOUR CODE!''' Use <code>;</code> for general comments, <code>;/ ... /;</code> for comment blocks, and <code>{ ... }</code> for docstrings.
 
# '''COMMENT YOUR CODE!''' Use <code>;</code> for general comments, <code>;/ ... /;</code> for comment blocks, and <code>{ ... }</code> for docstrings.
# To get started with Papyrus scripting, read through the concepts and tutorials here: https://www.creationkit.com/index.php?title=Category:Papyrus
+
# To get started with Papyrus scripting, read through the concepts and tutorials here: https://ck.uesp.net/wiki/Category:Papyrus
 
# If you expect to do extensive scripting, an external editor & compiler is highly recommended (see above link for instructions for different editors).
 
# If you expect to do extensive scripting, an external editor & compiler is highly recommended (see above link for instructions for different editors).
 
# Keep these pages bookmarked; you will use them often:
 
# Keep these pages bookmarked; you will use them often:
#*https://www.creationkit.com/index.php?title=List_of_Papyrus_Functions
+
#*https://ck.uesp.net/wiki/List_of_Papyrus_Functions
#*https://www.creationkit.com/index.php?title=Category:Events
+
#*https://ck.uesp.net/wiki/Category:Events
#*https://www.creationkit.com/index.php?title=Default_Scripts_List
+
#*https://ck.uesp.net/wiki/Default_Scripts_List
#*https://www.creationkit.com/index.php?title=Condition_Functions
+
#*https://ck.uesp.net/wiki/Condition_Functions
 
#Enable and check your Papyrus logs. Make liberal use if <code>debug.trace(...)</code> when first writing and testing your code.
 
#Enable and check your Papyrus logs. Make liberal use if <code>debug.trace(...)</code> when first writing and testing your code.
 
#Avoid using <code>Game.GetPlayer()</code> when possible. It is almost always better to use and autofill an Actor property PlayerREF instead.
 
#Avoid using <code>Game.GetPlayer()</code> when possible. It is almost always better to use and autofill an Actor property PlayerREF instead.
Line 15: Line 17:
 
#Before making a new script, see if there's an existing default script you can use, e.g. <code>defaultSetStageOnDeath</code> or <code>defaultPlayerEnableDisableLinkedRef</code>.
 
#Before making a new script, see if there's an existing default script you can use, e.g. <code>defaultSetStageOnDeath</code> or <code>defaultPlayerEnableDisableLinkedRef</code>.
 
#Don’t start properties, variables, functions, or any type of Papyrus identifier with a number --- the compiler will fail (i.e. <code>123function()</code> fails).
 
#Don’t start properties, variables, functions, or any type of Papyrus identifier with a number --- the compiler will fail (i.e. <code>123function()</code> fails).
#Avoid constant polling with short intervals whenever possible (see previous lesson). There are almost always more efficient ways of doing something, such as events that detect what you are trying to poll. If you use a while loop or <code>OnUpdate</code>, make sure there is some failsafe to exit the loop/stop the updates.
+
#Avoid constant polling with short intervals whenever possible. There are almost always more efficient ways of doing something, such as events that detect what you are trying to poll. If you use a while loop or <code>OnUpdate</code>, make sure there is some failsafe to exit the loop/stop the updates.
#Avoid long <code>Wait()</code> functions (this can cause issues with stack dumping: https://www.creationkit.com/index.php?title=User:DavidJCobb/Stack_dumping). Instead, use <code>RegisterForSingleUpdate</code>/<code>OnUpdate</code> or <code>RegisterForSingleUpdateGameTime</code>/<code>OnUpdateGameTime</code>.
+
#Avoid long <code>Wait()</code> functions (this can cause issues with stack dumping: https://ck.uesp.net/wiki/User:DavidJCobb/Stack_dumping). Instead, use <code>RegisterForSingleUpdate</code>/<code>OnUpdate</code> or <code>RegisterForSingleUpdateGameTime</code>/<code>OnUpdateGameTime</code>.
 
#If you make an ObjectReference or an Actor a property, that ref will become persistent (always loaded). Avoid this when possible and use linkedrefs or referencealiases.
 
#If you make an ObjectReference or an Actor a property, that ref will become persistent (always loaded). Avoid this when possible and use linkedrefs or referencealiases.
 
#If you ''must'' use an Actor or ObjectReference property, try to use a reference alias instead, as these clear automatically when the quest is done. Example: <code>ReferenceAlias Property Hentus_alias Auto</code>, then later <code>(Hentus_alias.GetReference() as actor).AddItem()</code>. (See Skyrim quests for examples.)
 
#If you ''must'' use an Actor or ObjectReference property, try to use a reference alias instead, as these clear automatically when the quest is done. Example: <code>ReferenceAlias Property Hentus_alias Auto</code>, then later <code>(Hentus_alias.GetReference() as actor).AddItem()</code>. (See Skyrim quests for examples.)
#More information on levels of persistence can be found here: https://www.afkmods.com/index.php?/topic/4250-skyrim-levels-of-persistence/ and more information on how to avoid persistence can be found here: https://www.creationkit.com/index.php?title=Persistence_(Papyrus)  
+
#More information on levels of persistence can be found here: https://www.afkmods.com/index.php?/topic/4250-skyrim-levels-of-persistence/ and more information on how to avoid persistence can be found here: https://ck.uesp.net/wiki/Persistence_(Papyrus)  
 
#When testing a script, if you choose to COC into the cell, COC into a cell NEAR the one where your script will be triggered, not in it, or it might bug out – better yet, just travel to the trigger area in the same way a real player would.
 
#When testing a script, if you choose to COC into the cell, COC into a cell NEAR the one where your script will be triggered, not in it, or it might bug out – better yet, just travel to the trigger area in the same way a real player would.
 
#If you’re making a do-once script, use states and make sure to send the script to an empty state when it is done running.  
 
#If you’re making a do-once script, use states and make sure to send the script to an empty state when it is done running.  
 
#If you’ve got an empty state, make sure it remains empty; otherwise the event in it will still be processed, which takes away a big benefit to empty states (not having events even be processed).
 
#If you’ve got an empty state, make sure it remains empty; otherwise the event in it will still be processed, which takes away a big benefit to empty states (not having events even be processed).
 
#Avoid piling too much onto your script at once, especially if it has to be run often – too much activity can be hard on Papyrus. States can be useful for limiting what is running at a given point in time.
 
#Avoid piling too much onto your script at once, especially if it has to be run often – too much activity can be hard on Papyrus. States can be useful for limiting what is running at a given point in time.
#Avoid convenience functions – use <code>GetReference()</code> as Actor instead of <code>GetActorRef()</code>, <code>GetActorValue()</code> instead of <code>GetAV()</code>, etc...
+
#Avoid convenience functions – use <code>GetReference() as Actor</code> instead of <code>GetActorRef()</code>, <code>GetActorValue()</code> instead of <code>GetAV()</code>, etc...
 
#Sometimes there are Condition Functions that do not have Papyrus equivalents (e.g. <code>IsPlayerInRegion</code>). If you need to access these conditions in a script, two possibilities are:
 
#Sometimes there are Condition Functions that do not have Papyrus equivalents (e.g. <code>IsPlayerInRegion</code>). If you need to access these conditions in a script, two possibilities are:
 
##'''Use a spell:''' Make a constant effect magic effect that sets a global to 1 on effect start and 0 on effect end. Then, make an ability that has this effect, and condition the effect using the condition function you want to use in your Papyrus script. Add the ability to the player (or to a player quest alias). Then, add this global variable as a script property. You can check if it is equal to 1 or 0 to see if your condition is met.
 
##'''Use a spell:''' Make a constant effect magic effect that sets a global to 1 on effect start and 0 on effect end. Then, make an ability that has this effect, and condition the effect using the condition function you want to use in your Papyrus script. Add the ability to the player (or to a player quest alias). Then, add this global variable as a script property. You can check if it is equal to 1 or 0 to see if your condition is met.
 
##'''Use a quest:''' Make a quest with reusable stages, then make a stage with multiple log entries. Condition the different entries using the condition function you need to check, and use quest fragments for the different entries for how you want your script to respond to the different conditions. Then, when you need to check the condition, use the SetStage command on your new quest. The quest will evaluate the conditions, and response with the appropriate fragment.
 
##'''Use a quest:''' Make a quest with reusable stages, then make a stage with multiple log entries. Condition the different entries using the condition function you need to check, and use quest fragments for the different entries for how you want your script to respond to the different conditions. Then, when you need to check the condition, use the SetStage command on your new quest. The quest will evaluate the conditions, and response with the appropriate fragment.
#Condition functions generally process faster than their Papyrus equivalents. For a large script, using the steps in (19) can also be a good way to lighten the load of your script.
+
#Condition functions generally process faster than their Papyrus equivalents. For a large script, using the steps in (20) can also be a good way to lighten the load of your script.
 +
#More generally: Don't use Papyrus for something that can be done without papyrus.
 
#'''COMMENT 👏 YOUR 👏 CODE 👏'''
 
#'''COMMENT 👏 YOUR 👏 CODE 👏'''
 
+
# Performance: Profile your scripts and functions to understand the time delay by your scripts and functions. You can use [https://speedscope.app speedscope] to visualize your profiles. https://www.reddit.com/r/skyrimmods/comments/i57siz/profiling_your_papyrus_scripts/
 
 
 
 
[[Category:Arcane University-Implementation]]
 

Latest revision as of 11:55, 11 August 2024

< Arcane University:Implementation

This is a guide of best practices to follow when writing Papyrus scripts.

Papyrus Scripting Best Practices[edit]

  1. COMMENT YOUR CODE! Use ; for general comments, ;/ ... /; for comment blocks, and { ... } for docstrings.
  2. To get started with Papyrus scripting, read through the concepts and tutorials here: https://ck.uesp.net/wiki/Category:Papyrus
  3. If you expect to do extensive scripting, an external editor & compiler is highly recommended (see above link for instructions for different editors).
  4. Keep these pages bookmarked; you will use them often:
  5. Enable and check your Papyrus logs. Make liberal use if debug.trace(...) when first writing and testing your code.
  6. Avoid using Game.GetPlayer() when possible. It is almost always better to use and autofill an Actor property PlayerREF instead.
  7. If you want to remove a property from a script, be sure to clear that property from anywhere that script is used in the Creation Kit first. Otherwise, the property will still exist on the CK object (but you won't be able to see it) and the game will still try to load the property. If you forget to do this, you can remove the zombie property in xEdit.
  8. Before making a new script, see if there's an existing default script you can use, e.g. defaultSetStageOnDeath or defaultPlayerEnableDisableLinkedRef.
  9. Don’t start properties, variables, functions, or any type of Papyrus identifier with a number --- the compiler will fail (i.e. 123function() fails).
  10. Avoid constant polling with short intervals whenever possible. There are almost always more efficient ways of doing something, such as events that detect what you are trying to poll. If you use a while loop or OnUpdate, make sure there is some failsafe to exit the loop/stop the updates.
  11. Avoid long Wait() functions (this can cause issues with stack dumping: https://ck.uesp.net/wiki/User:DavidJCobb/Stack_dumping). Instead, use RegisterForSingleUpdate/OnUpdate or RegisterForSingleUpdateGameTime/OnUpdateGameTime.
  12. If you make an ObjectReference or an Actor a property, that ref will become persistent (always loaded). Avoid this when possible and use linkedrefs or referencealiases.
  13. If you must use an Actor or ObjectReference property, try to use a reference alias instead, as these clear automatically when the quest is done. Example: ReferenceAlias Property Hentus_alias Auto, then later (Hentus_alias.GetReference() as actor).AddItem(). (See Skyrim quests for examples.)
  14. More information on levels of persistence can be found here: https://www.afkmods.com/index.php?/topic/4250-skyrim-levels-of-persistence/ and more information on how to avoid persistence can be found here: https://ck.uesp.net/wiki/Persistence_(Papyrus)
  15. When testing a script, if you choose to COC into the cell, COC into a cell NEAR the one where your script will be triggered, not in it, or it might bug out – better yet, just travel to the trigger area in the same way a real player would.
  16. If you’re making a do-once script, use states and make sure to send the script to an empty state when it is done running.
  17. If you’ve got an empty state, make sure it remains empty; otherwise the event in it will still be processed, which takes away a big benefit to empty states (not having events even be processed).
  18. Avoid piling too much onto your script at once, especially if it has to be run often – too much activity can be hard on Papyrus. States can be useful for limiting what is running at a given point in time.
  19. Avoid convenience functions – use GetReference() as Actor instead of GetActorRef(), GetActorValue() instead of GetAV(), etc...
  20. Sometimes there are Condition Functions that do not have Papyrus equivalents (e.g. IsPlayerInRegion). If you need to access these conditions in a script, two possibilities are:
    1. Use a spell: Make a constant effect magic effect that sets a global to 1 on effect start and 0 on effect end. Then, make an ability that has this effect, and condition the effect using the condition function you want to use in your Papyrus script. Add the ability to the player (or to a player quest alias). Then, add this global variable as a script property. You can check if it is equal to 1 or 0 to see if your condition is met.
    2. Use a quest: Make a quest with reusable stages, then make a stage with multiple log entries. Condition the different entries using the condition function you need to check, and use quest fragments for the different entries for how you want your script to respond to the different conditions. Then, when you need to check the condition, use the SetStage command on your new quest. The quest will evaluate the conditions, and response with the appropriate fragment.
  21. Condition functions generally process faster than their Papyrus equivalents. For a large script, using the steps in (20) can also be a good way to lighten the load of your script.
  22. More generally: Don't use Papyrus for something that can be done without papyrus.
  23. COMMENT 👏 YOUR 👏 CODE 👏
  24. Performance: Profile your scripts and functions to understand the time delay by your scripts and functions. You can use speedscope to visualize your profiles. https://www.reddit.com/r/skyrimmods/comments/i57siz/profiling_your_papyrus_scripts/