Papyrus Introduction
What is Papyrus?
Papyrus is the scripting language used to make Skyrim. It works by receiving Events from the game, and sending function calls to it. It is the glue that holds quests together, by setting variables based on player actions, and waiting and responding for specific events from the game. It also drives much of the functionality for animated objects that require player or NPC interaction, and much of the complex behavior of magic effects.
At its heart, Papyrus is object oriented in nature. For an overview of what this means and the language at large, read on. Users with experience in an object oriented environment, simply perusing the Script Objects pages should give a pretty good sense of how things work.
What is a script?
A papyrus script is essentially a plain text source document (.psc file) that you can use any text editor to write and compile it into a form the game can understand (.pex file). To make changes after it is compiled, the raw .psc must be updated or replaced, re-compiled, replacing the outdated .pex.
What’s in a Papyrus script?
The language of papyrus can be broken down into a handful of concepts: Objects, Functions, Events, Variables and Properties.
Each script is defined to be a type of Object, such as a “Quest”, a “Reference,” an “Actor,” or a “Book.” For the most part, these script objects correlate exactly to the objects found in the editor's masterfile.
These objects have Functions that can be used to get at the data they have in them from the masterfile or that is saved in them at run time. For example, you can use the function GetActorValue(“Health”) to get the current health value of an actor, and ModActorValue(“Health”, 50) to add 50 points to an actor’s health value. You could also use the function Kill() to kill an actor. Each of these functions is part of the Actor Script object. If you tried to call Kill() on a Book Script, because the Book script has no Kill() function defined for it (since the game code has no notion of killing books), the compiler will complain and refuse to compile the script, giving you an error message that Kill() is not a function on the Book script.
Scripts also have Events, which are like function calls that the game itself calls on the object. For example, there is an OnDeath() event, that the game sends to Actor Scripts attached to an actor when the actor dies. This allows you to respond to game events, for example, completing a quest after the player kills a particular enemy. Therefore, Functions as described in the previous paragraph are typically called within an event.
A script can also have Variables. A variable is a value that can be modified and reference by the game and scripts. For example, you might want to store the number you get from a GetActorValue(). With variables you can store this information, and use it later.
A script can also have Properties, which function almost identically to variables, except that other scripts can ask about and set them. Properties can also be modified in the editor. (Note: In reality properties are more a bit complex than simply a “variable that you can get/set externally,” but for practical purposes, that’s a fine way to think of them).
Variables and Properties can be defined to be simple structures like a Boolean, an Integer, a Float, etc. But their real power lies in that they can be defined as and hold any Object Type. A Quest script, for instance, can have a property that holds a pointer to an Actor. Once you have a pointer to an object in a script property, that script can run functions on that property and thus on the object in it. For example, a quest script holding a property pointing to an Actor, can call Kill() on that property and it will kill that actor. Properties can be set by the script at run time, or can be set by pointing to objects in the editor.
It's also important to remember that papyrus scripts only run in response to the game and other scripts. Therefor all of your code needs to live inside an Event block, or inside a Script Fragments such as a quest stage fragment, or topic info fragment.
How do you write a papyrus script?
You will use a text editor to write the script (favorites include Notepad++ and Sublime Text). Once you have written the script, you will need to compile it before it will work in game.
Before you sit down to write a script, you need to think about what type of script it’s going to be. Or in other words, what type of object it’s going to be running on. Once you decide that, you create a new script by declaring it at the top of a .psc file, and EXTEND the Object Script that it’s based on. For example, if you are going to be creating a triggerbox that sets a variable, you will create a script that extends ObjectReference.
Extending scripts
For every object in the game that can be scripted, or pointed at by a property, there is a corresponding "base" papyrus script already premade: see Script Objects for a list. Essentially, the game can give any object of the proper type this script at runtime.
For example, there is an ObjectReference Script. So any ObjectReference in the game, essentially has the ObjectReference script attached to it. This is so that you can define an ObjectReference property on a script, and point it at any ObjectReference, and call any of the ObjectReference functions on it, such as Disable(), without having to manually attach the ObjectReference script in the editor to every ObjectReference in the game that you might want to call disable() on.
However, you are writing a script, presumably because you want to do something special. Respond to a specific event like waiting for the player to activate the object, or respond to the death of an actor, etc. None of this special functionality exists in the premade object scripts. However, so that the compiler knows what type of object you are dealing with, and to gain access to all the events and functions already defined for that type of object, you extend the existing “base” script, and add your special code to it.
Example Script (extending ObjectReference)
For example, let's say you want to set a quest stage when the player hits a particular triggerbox. You would create a script called “MyTriggerBoxScript” that extends “ObjectReference” and attach that to the triggerbox’s reference in the Render Window. You then create a Property called MyQuest, and point it to the your quest in the editor. Then you write your version of the OnTriggerEnter event, that calls set stage on the quest in the myQuest property.
Scriptname MyTriggerBoxScript extends ObjectReference
Quest Property MyQuest Auto
Int Property StageToSet Auto
Event OnTriggerEnter(ObjectReference akActionRef)
If akActionRef == Game.GetPlayer()
MyQuest.SetStage(StageToSet)
EndIf
EndEvent
There’s a lot going on in the above example, let’s break it down and further illustrate some points, and raise new ones.
The first line of any script starts (the "header line") with “Scriptname” that is us telling the compiler that this script is going to have following name (in this case the name is “MyTriggerBoxScript). This name MUST match the name of the text file you are writing the script in (in this case “MyTriggerBoxScript.psc”). The word “extends” means this script is going to be based on another script (in this case the “ObjectReference” script). That means your script will have access to all the functions and events defined in “ObjectReference.psc” and you can change them, or add new ones to your script.
The next line is declaring an "auto" property. “Property” is saying that we are defining a type of object to use as a property. Quest is declaring the Type of the property (in other words, only Quest objects can be put into this property). MyQuest is the name of the property. So all together this line is telling the compiler “I want you to know about a property called MyQuest, and it will only be able to have a Quest object in it, nothing else.” (For now we will ignore the “auto” keyword. Just know that you almost always write “auto” at the end of every property you declare. For the full discussion see: Declaring Properties.)
Similarly the next line is defining an integer property.
Next we see some lines that start with Event and EndEvent. You can think of these like book ends for this particular event. Everything in between is how this script will respond to the “OnTriggerEnter” event. You might have a script that responds to multiple events. Using Event/EndEvent you isolate everything you want to happen for this event from another one elsewhere in the script.
The OnTriggerEnter is the name of the event your script is implementing. The ( ) define what parameters the game is going to send to your script's OnTriggerEnter event. "akActionRef" is the name of the parameter, and "ObjectReference" defines what type that parameter is – and, if you have been paying attention, you also know that "ObjectReference" is itself a papyrus script (ObjectReference Script).
The next line starts with “if” and a line below it “EndIf.” This is a conditional statement. You will be writing a lot of “if-then-else statements” in your script. Here we are testing if the ObjectReference “akActionRef” that the game passed into our OnTriggerEnter when an actor walked into it is the player or not.
Game.GetPlayer() is a function GetPlayer() on the Game Script that returns the Actor that represents the player. The == is a test (a "Comparison Operator") which means “is this thing exactly this other thing?” So we are asking is the reference that the game sent to our script when an actor entered our trigger box, is that the same as the actor reference returned by Game.GetPlayer()? In other worlds “did the player enter the trigger?” If so, then call the SetStage() function on the quest that is in the MyQuest property, and tell it the stage we want to set is whatever we have stored in the StageToSet property.
If you have been following along carefully, you may wonder at how we can compare an ObjectReference "akActionRef" to an Actor (the return value of Game.GetPlayer()). The reason we can do this is because the compiler knows that all Actors are also ObjectReferences because the Actor Script extends the ObjectReference Script. So here you see that extension isn't used only for your own scripts, but the base scripts also extend each other as well! The wiki writeup for each Script Object contains at the top of its page, which script it is extending from.
So now we know how to set up a script that checks and does things with properties, but we may be wondering, where do we tell the script what objects and what values to assign into your properties? (In the above example, which quest, and which quest stage are we talking about here?)
Setting Variables and Properties
There is one basic way to set a variable, and two basic ways to set properties. Assigning values in a script, and assigning data to a property in the editor.
Setting variables and properties in the script
The way to set a variable and one of the ways to set a property is to assign it a value in the script itself.
You can define it one place, and set it some place else:
;At the top of the script we defined a variable:
Int MyStage
;...
;Later, inside an event block:
MyStage = 100
Or you can assign it at the time you declare it:
Int MyStage = 100
Note: you can always set variables and properties when you declare them as long as the type is a "Literal." All other types of values (an actor for example) can only be set when declared, if the variable/property is defined inside a function or event.
For example, declaring at top, and setting elsewhere:
;Delcared at top
ObjectReference ThingActivatingMe
;set inside an event
Event OnActivate(ObjectReference akActionRef)
ThingActivatingMe = akActionRef
EndEvent
You might do the above if you wanted to hang onto the thing that activated the object for longer than the one instance of the activation.
Or you could declare it and set at the same time (inside a function or event) like this:
;set inside an event
Event OnActivate(ObjectReference akActionRef)
ObjectReference ThingActivatingMe = akActionRef
EndEvent
For a more detailed discussion of how to declare and set variables and properties, see: Variables and Properties
Setting Property Data in the Editor
The other way is to assign a property by pointing to an object in the editor, or typing in the value in the editor. This is how we might set the MyQuest and StageToSet property in the example script we were looking at earlier.
Many forms in the editor have a "Scripts Tab" that allows you to a) add scripts to the object, and b) set the values of properties on those scripts.
For now, let's look at an Activator form in the editor, and look at its script area:
If you select the script by clicking it, then click the properties button, you will see a list of all the properties. If you click the "Edit Value" button and edit field will appear either a text field to enter a numeric data, checkbox if it's a bool, or one or more drop down if it's an object.
In the above image you can see that myQuest has been set to MQ101, and StageToSet has been set to 100.
References Inheriting scripts and property values from parent base objects
References inherit any scripts and property settings from their Base Objects (as listed in the Object Window).
For example, Actors and Activators. If you attach a script to an Actor or an Activator all of the individual references of those objects in the game will also have those same scripts. Their references will also inherit the data on any properties you have set on their scripts.
You can override these properties by opening the "Scripts Tab" on the reference in the Render Window, and set property data that you want to override on the reference itself. You can also remove the inherited script altogether from the reference, by removing the script from the reference's scripts tab in the render window. Note: you can also add scripts directly to the reference in the Render Window as well.
Papyrus Fragments
In addition to adding scripts to forms in the editor, certain forms have what is called "Papyrus fragments" that you can write script in, that run at particular times. They also have special variables that can be used, for example, Topic Info and Package fragments have an akSpeaker and akActor variables that you can use to gain access to the actor saying the line, or the actor running the package.
Example topic info fragment:
akSpeaker.StartCombat(Game.GetPlayer())
A common technique is to set the stage of the owning quest. Some types of forms can be owned by quests (For example scenes, and topic infos in the quest are said to be owned by it, as well as any package whose "owning quest" drop down is set). Example of setting a quest stage from a fragment that is on a form that is owned by a quest:
GetOwningQuest().SetStage(100)
Follow the links below for details on when each fragment runs, as well as any special variables it has.
States
Not only can a papyrus script respond to various events from the game, it can also be put into various "states" each with its own version of the event, so that it can respond differently to the same event, depending on what state it is in at the time the event comes through.
For example, many activators in the game, especially ones that need to play animations associated with their activation, have an "at rest" state and a "I'm busy animating state."
To give a sense of why this is important, consider a simple pull chain activator. When the player activates it, it needs to animate being pulled down. If the script simple played the animation every time it was activated, you could interrupt the pulling animation, and it would immediately start from the first frame of the animation, "jumping" unnaturally back to the top and animating down again. We'd rather make sure it was finished before allowing the player to activate it again.
Let's take a look at what this might look like inside ascript.
Event OnActivate(ObjectReference akActionRef)
;DO STUFF HERE
EndEvent
The above would happen every time the player activated the object, potentially causing things to happen to fast if he spams on the activate button.
Below shows the use of states to control things:
Auto State AtRest
Event OnActivate(ObjectReference akActionRef)
GoToState("Busy")
;DO STUFF HERE
;play animations, etc. - left vague for illustrative purposes
;Done doing stuff
GoToState("AtRest")
EndEvent
EndState
State Busy
Event OnActivate(ObjectReference akActionRef)
debug.trace("I'm busy, so not doing anything.")
EndEvent
EndState
In the above you'll see two State/EndState pairs, the first of which has the "auto" keyword. The auto keyword here means, "this is the default state" so before any calls to "GoToState()" happen, this is the state the script is in. Like the events inside them, these "state/endState" lines enclose the part of the script that belongs to each of the states. All the events and functions inside each state are the versions that the script uses when it is in each of those states.
You put a script into a state by calling the function GoToState("StateName") where "StateName" is a string that is the same as the name of a state you declared.
Any event outside a State/EndState block will run if the script isn't in ANY state. (For example, if you declare states and none of them have the "auto" keyword, then the events and functions declared outside all of the states will be used until a "GoToState()" call has put the script in a different state. Calling "GoToState()" with an empty string parameter will put the script in into no state (aka, the "empty state").
If an event only exists in a state that the script is not currently in, nothing will happen if that event is sent to the script if the script is not currently in that state.
For more information about states, see: States (Papyrus)
States are also used to control different versions of functions as well. The discussion above applies equally with functions, but we haven't discussed writing your own functions yet. Which brings us to:
Custom Functions
You can write your own functions in Papyrus. You can then call these functions from within the script where you create them, but also from other scripts as well. This is a more advanced, but powerful and helpful feature of the scripting language.
This will be the most complex discussion in the Introduction page. And in the discussion of writing functions, we will encounter other concepts as well. But stick with it. You will be able to achieve great things with less headaches. You don't often need to write your own functions, but when you do, it can save you time and energy, because you can write snippets of script that you resuse in multiple places, that will cut down on the time it takes you to implement things, and make debugging much easier.
Calling functions
Before we discuss writing your own functions, lets discuss a little more about what functions are.
A function is a section of script that you use (or "call") to do something or get something. A script can call a function that lives in its own file, or can "call" a function on other script.
Some example functions that exist on base scripts:
- GetDisabled() - used to find out if an ObjectReference is disabled or not
- Disable() - used to cause an ObjectReference to become disabled
- Enable() - used to cause an ObjectReference to become enabled
- GetDead() - used to find out if an Actor is dead or not
- Kill() - used to cause an Actor to die
- GetActorValue() - used to get the numeric actor value of an Actor (for example get an Actor's "Health," or "TwoHanded" skill level)
- SetActorValue() - used to set the numeric actor value of an Actor (for example to set an Actor's "Health," or "TwoHanded" skill level)
In the examples above, you'll note a few things. You can use functions to get information about an object, as well as set values and cause them to do things. Functions are used by particular types of objects (you call Enable() on ObjectReferences, and Kill() on Actors). What might not be clear at first is that the object that the function runs on, also has a corresponding script attached to it. The game essentially adds the appropriate base script to objects at run time (Actors get the Actor Script, ObjectReferences get the ObjectReference Script).
If you want to call GetDisabled(), you must have a "pointer" to an ObjectReference (to which the game attaches the ObjectReference Script. If you want to call GetDead(), you must have a "pointer" to an Actor (to which the game attaches the Actor Script).
So, how do you get a "pointer" to an object?
A common way to do this is by setting up a property in your script that points to that object in the Masterfile. For example, you might set up a property called "myReference" and point it to a large bolder blocking a dungeon entrance in a particular cell in the Render Window, and then you can write this in your script "myReference.Disable()" to cause it to disable, revealing an entrance to a dungeon the player is now allowed to explore.
Once you have a property set up:
ObjectReference Property myReference Auto ;defines the property, you would hook it up in the editor to point at a particular reference
You can then call functions on it:
myReference.Disable()
See the section above Setting Property Data in the Editor for information on how to set property data to things in the masterfile.
Another way to get a pointer to an object, is by getting one FROM another function.
For example: Game.GetPlayer() RETURNS an object of type actor, that is pointing to the player.
For example:
ObjectReference Property myActor Auto ;defines the property
Later in the script, you could then call a function that returns an actor, store it, and then call the function from the property
myActor = Game.GetPlayer()
myActor.Kill() ;this will kill the player. Not very nice. But sometimes it is well deserved. ;)
You could also just skip the assignment to the property altogether, and string things together like this instead:
Game.GetPlayer().Kill()
The above works because the compiler knows that Game.GetPlayer() always results in an object of the type Actor, and so that object will be able to use the script function Kill().
And a reminder about what we learned about Extending scripts. Some of the base scripts extend other scripts. For instance the Actor Script extends the ObjectReference Script. This means that the Actor Script has all the same functions that the ObjectReference Script has, and for all intents and purposes, all Actors can be treated as ObjectReferences as well. So that if you have a pointer to an Actor you can Disable() it, even though that function lives in the ObjectReference script.
This can all be summed up thusly: Objects in the game have default scripts attached to them, based on their type. You can call functions on particular instances of these objects (calling function in their script, or in a script their script extends), by getting a pointer to them and calling the function on that pointer. Pointers can be objects stored in properties, or objects returned by functions.
Calling custom functions
Your custom functions will be called just like existing functions. You get a pointer to an object with your script attached to it, and then you call the function on it. The pointer to the object that has your script attached to it that you will call the function on, must be declared as a type that is your script.
A common practice is to write functions in a script attached to a quest. For example, lets say you made a script called "myQuestScript" and wrote a function called "DoSomeStuff()"):
Scriptname myQuestScript extends Quest
Function DoSomeStuff()
;cool stuff happens here
EndFunction
And then you attach this script a quest in the game.
The script knows about all the functions declared inside it. So if you were going to call the "DoSomeStuff()" function in the script above that you attached to the quest, you could simply call the function.
DoSomeStuff()
But, if you were going to call this function somewhere else you will need a pointer to your quest (just like we discussed above for base scripts). However, you will need to type the pointer to be the type of your script. Your script's type is the same as the name you declared it in the first line of your script. In this case "myQuestScript." Here we are defining a new property of this new type:
myQuestScript Property myQuest Auto
Then you can open the scripts property button and assign it to be that particular Quest. For example, on the "Quest Giver" alias in your quest, you added a script that on death of the quest giver, you want to call "DoSomeStuff()" on the quest.
Let's look at what this might look like. Here is an example of a script that might be attached to a "Quest Giver" alias in your quest.
Scriptname myQuestGiverScript extends Quest
myQuestScript Property myQuest Auto ;you would point this to your quest in the "Quest Giver" [[alias]]'s script properties tab.
Event OnDeath(Actor akKiller)
myQuest.DoSomeStuff()
EndEvent
There's a shortcut to the above as well. Because Aliases are owned by quests, the Alias Script has the function "GetOwningQuest()" on it, that returns an object of type Quest, that is a pointer to the quest the alias is in. So you might be tempted to do something like this:
Event OnDeath(Actor akKiller) GetOwningQuest().DoSomeStuff() ;this results in a compile error EndEvent
But that won't work, because the object that is returned by GetOwningQuest() is the type Quest. Which only knows about the functions declared in it. The compiler will give an error that there is no function called "DoSomeStuff" on Quest. Your "DoSomeStuff()" function lives in your script called "myQuestScript."
Now, because myQuestScript extends Quest, you can make a promise to the compiler that the thing returned by GetOwningQuest will have your myQuestScript attached to it. You do this by "CASTING" the Quest object into a myQuestScript object.
Scriptname myQuestGiverScript extends Quest
Event OnDeath(Actor akKiller)
(GetOwningQuest() As myQuestScript).DoSomeStuff() ;this works
EndEvent
GetOwningQuest() will return a Quest, the word "As" means think of this AS a myQuestScript. The enclosing () allow you to use the "." to access on the cast object, so you can call DoSomeStuff() on it.
Writing Custom Functions
We'll get into the nitty gritty of writing Custom functions by looking at an example of something we might want to do while writing a quest: handle the death of the quest giver.
A function has a start/end line (Function/EndFunction) may contain parameter definitions. In this case lets create a function called "HandleQuestGiverDeath" that takes a parameter of the actor who did the killing, so we can do different things if the player killed him, or something else. We'll write this function on a script "myQuestScript" that we'll attach to our quest in the editor.
Scriptname myQuestScript extends Quest Conditional
ReferenceAlias Property QuestGiver Auto
ReferenceAlias Property QuestGiverBackup Auto
int Property QuestGiverKiller auto conditional ;0 = unset, 1 = player, -1 not the player
function HandleQuestGiverDeath(Actor Killer, ReferenceAlias DeadActorAlias)
if DeadActorAlias == QuestGiver
QuestGiver.ForceRefTo(QuestGiverBackup.GetReference())
SetStage(110)
if Killer == Game.GetPlayer()
QuestGiverKiller = 1
else
QuestGiverKiller = -1
endIf
endif
endFunction
A new keyword was introduced in the above example. You'll notice the keyword "Conditional" on both the Scriptname line, as well as the line declaring the QuestGiverKiller property. This allows the QuestGiverKiller property to be found by the GetVMQuestVariable condition, so that you can test the value of this property at run time to filter dialogue stacks to get appropriate dialogue, conditionalize package stacks on actors, etc. Having variables like this that track the specifics about what happened is also helpful for debugging as well. (To see the values of scripts at run time see: ShowQuestVars and ShowVars)
So let's look at the function line. Here you see "function/EndFunction" pair that delineates the section of the script that is in our function. "HandleQuestGiverDeath" is the name we are giving our function. And everything in between the parentheses () are the parameters we expect any call to "HandleQuestGiverDeath" to pass in to the function. In this case we are requiring a "Killer" parameter of the type Actor, and a ReferenceAlias that we are calling "DeadActorAlias."
Let's break down this line:
QuestGiver.ForceRefTo(QuestGiverBackup.GetReference())
Here we are calling the functionForceRefTo which will force a reference into the the alias, so it can take on the dialogue and packages of that alias. The ForceRefTo function takes a Reference as a parameter. It just so happens that GetReference returns a reference, the reference inside that alias. So here were are asking the QuestGiverBackup alias "hey, what's the reference that is currently in you?" and then taking that result and passing it into the ForceRefTo function we are calling on QuestGiver alias, so that the QuestGiver alias now has in it the same reference that is in the QuestGiverBackupAlias.
Now what's missing is the script we attach to the QuestGiverAlias:
Scriptname myQuestGiverAliasScript extends ReferenceAlias
Event OnDeath(Actor akKiller)
(GetOwningQuest() as myQuestScript).HandleQuestGiverDeath(akKiller, Self)
EndEvent
Here we see that during the OnDeath event, we will be calling the "HandleQuestGiverDeath" function on the myQuestScript script we attached to the quest. Here again is the common shortcut "(GetOwningQuest() as TheNameOfYourQuestScript)" syntax so we can append the .FunctionName() to it. This works because the script we are doing this in is "owned" by the quest (all Aliases are said to be owned by the quest they are created in)
Notice that we are simply "passing in" the akKiller that the event gives us, as the "Killer" actor parameter on our function, and we are passing in "self" as the parameter for the DeadActorAlias ReferenceAlias parameter. We can use "Self" to mean, "the object this script is attached to" which in this case, is a ReferenceAlias, the type our function is looking for it's DeadActorAlias parameter. (Astute readers may ask "Why do are we bothering to pass in the DeadActorAlias, can't we assume it's the quest giver?" We could. And probably would. But I wanted to give you an example that show cased some of these other concepts such as passing in "Self" as a parameter value)
And there's one more thing to complete your basic understanding of the scripting language, and that is your custom functions can return a value just like native functions do.
For example, we could change the above script to this:
Scriptname myQuestScript extends Quest Conditional
ReferenceAlias Property QuestGiver Auto
ReferenceAlias Property QuestGiverBackup Auto
int Property QuestGiverKiller auto conditional ;0 = unset, 1 = player, -1 not the player
function HandleQuestGiverDeath(Actor Killer, ReferenceAlias DeadActorAlias)
if IsQuestGiver(DeadActorAlias)
QuestGiver.ForceRefTo(QuestGiverBackup.GetReference())
SetStage(110)
if Killer == Game.GetPlayer()
QuestGiverKiller = 1
else
QuestGiverKiller = -1
endIf
endif
endFunction
bool function IsQuestGiver(ReferenceAlias RefAliasToCheck)
if RefAliasToCheck == QuestGiver
Return True
else
Return False
endif
endFunction
Here we put the check testing if the alias in question is the quest giver into it's own function "IsQuestGiver" that returns a value (in this case a bool) that we can then use in our comparison check. (Again, this is probably more complex than you would need for a simple function like this, but it illustrates the point that you can return your own values. You could also write a function that returns an actor, a quest, or any other Script Object. This can be a big boon in more complex scripting situations.)
So what should I read next?
Once you grasp the basic concepts on this page, next places to go from here:
- Read this page again, and follow the links to get more information about the particulars that might have been glossed over.
- Look over the documentation for the language here: Papyrus Language Reference category
- Examine the pages in the Script Objects category and see what kinds of functions are available to you for different objects.
- Look over some of the other informational pages and tutorials at the top of the Papyrus category page
- Poke around in the editor, and deconstruct things.