Skip to content
ponzipyramid edited this page Mar 4, 2024 · 31 revisions

This is a terse guide on creating your own console commands using Papyrus and Custom Console (CC). This guide assumes experience with the Papyrus programming language and Unix-style shell commands i.e. ls, cat, git.

Flow

On game start, CC will generate a list of custom console commands using config files within Data/SKSE/CustomConsole. Each YAML configuration file in this folder contains one command. Each command can contain as many subcommands as the author wants which prevents potential name conflicts between different authors.

When the user types something into the console, CC will determine whether or not it is a valid custom command. If it is, it will try to parse the arguments and if everything is valid, invoke the function provided in the configuration file for the given subcommand. If it is invalid, it will print an error in the console and not invoke the function. If your function returns a value (float, string, form), CC will attempt to format it and print the value to the console.

Flag Example

Let's say we want to create a command we can use to determine whether or not a particular actor is being detected by anyone. Luckily, a function for this can be found in PO3's Papyrus Extender: Bool Function IsDetectedByAnyone(Actor akActor) global native so all we need to do is create a config file:

name: det-utils
alias: det
script: PO3_SKSEFunctions
help: utilities for testing detection
subs:
  - name: is-detected
    alias: idet
    func: IsDetectedByAnyone
    help: is the selected actor being detected by anyone
    args:
      - name: --target
        alias: -t
        type: actor
        selected: true
        required: true
        help: the actor to check detection for

This creates a new command called det-utils with the alias (shortened name) det. It has one subcommand is-detected that takes one argument --target. selected: true on this argument means it can be filled in using the currently selected object reference (by opening the console and clicking on something). The prefix -- in the name means its a flag argument rather than a positional argument. required: true means that an error will be printed to the console if the user has not clicked on a valid actor and has not manually passed in an actor using the flag. This field is necessary because flag arguments are usually optional. If not required and not provided by the user, CC will invoke the function with the actor argument being none. Finally, -t is a shortened way to write --target. The help fields will automatically be printed out if the user asks for help i.e. det-utis --help and allows command config files to be self-documenting.

A user can then use the console command as follows:

After clicking on an actor with the console open: det-utils is-detected

Manually passing in a specific actor instead of using the currently selected one: det-utils is-detected --target HuldaREF

Shortened form: det idet det idet --target HuldaREF

These commands will print true if the target is detected and false if not.

Positional Example

name: form-utils
alias: futil
script: PO3_SKSEFunctions
help: utilities for reading and manipulating forms
subs:
  - name: add-keyword
    alias: ak
    func: AddKeywordToForm
    help: add the provided keyword to selected form
    args:
      - name: form
        type: form
        help: the form the keyword will be added to 
      - name: keyword
        type: keyword
        help: the keyword to add to the provided form
  - name: remove-keyword
    alias: rk
    func: RemoveKeywordOnForm
    help: remove the provided keyword to selected form
    args:
      - name: form
        type: form
        help: the form the keyword will be removed from 
      - name: keyword
        type: keyword
        help: the keyword to remove from the provided form

In this example, we use positional arguments (there are no leading dashes). Positional arguments are usually best for arguments you expect to be required, although CC will still supply default values if you don't add required: true.

Usage:

Add the valuable keyword to gold: form-utils add-keyword Gold001 ValuableKwd

Remove the heavy armour keyword from Dragonscale Armor: futil rk 0x13940|Skyrim.esm ArmorTypeHeavy

Known Limitations

Custom types may or may not be supported. If a function you're using takes a custom type, you may need to alias the function with another that takes a regular type like Form, Armor, etc.

CC only supports static functions i.e. those that have the global specifier. Workarounds are discussed in the Best Practices section at the end.

Dealing with Forms

Form arguments can be passed to functions as either editor or form IDs. CC will attempt to convert the string into an actual object and cast it to the correct type prior to calling the command's function. For example, the gold form can be provided to a command as either 0xF|Skyrim.esm or Gold001.

Known Limitations

The functions being invoked must be global. Workaround for executing functions on attached scripts i.e. quests can be found below. Sometimes turning a form into a specific type (Keyword, Quest, Armor) can fail. In these cases, it might be better to change the argument type to a Form and cast it yourself within the function.

Array arguments and return values are unsupported. I intend to eventually add support for both.

Best Practices

While CC will attempt to make return values from functions user-readable, its best to simply return a string that CC can print out or to print to the console yourself using ConsoleUtilSSE to be more user-friendly. Even if you don't want to change a function signature, you can simply create an alias that creates a string using the original function's return value. For example, I can create an alias for Bool Function IsDetectedByAnyone(Actor akActor) global native in my own separate script as follows:

Scriptname PO3_ExtenderCommands Hidden

String Function IsDetectedByAnyone(Actor akActor) global
   string name = akActor.GetActorBase().GetName()

   if PO3_SKSEFunctions.IsDetectedByAnyone(akActor)
      return name + " is currently detected by someone"
   else
      return name + " is not being detected by anyone"
   endIf
EndFunction

My config file would then become:

name: det-utils
alias: det
script: PO3_ExtenderCommands # changed from PO3_SKSEFunctions
help: utilities for testing detection
subs:
  - name: is-detected
    alias: idet
    func: IsDetectedByAnyone
    help: is the selected actor being detected by anyone
    args:
      - name: --target
        alias: -t
        type: actor
        selected: true
        required: true
        help: the actor to check detection for

Similarly, if you want to invoke a function in a script attached to a specific object (which CC does not currently support), you can simply alias the original. For instance, if I want to call a function named KnockOutNazeem in one of my quest scripts named KnockOutNazeemScript I can do this:

Scriptname Py_Commands Hidden

String Function KnockoutNazeem() Global
   (Quest.GetQuest("KnockOutNazeemQuest") as KnockOutNazeemScript).KnockoutNazeem()
   return "You've done Whiterun a great service."
EndFunction 
name: py-commands
alias: pyc
script: Py_Commands
help: have you been to the cloud district?
subs:
  - name: knockout
    alias: kout
    func: KnockoutNazeem
    help: knock that sucker out

Usage: pyc kout.