Functions, custom placeholders and string conditions - Membercat-Studios/Streamlabs-Integration GitHub Wiki

After a while of using the plugin, your config will get quite complex and hard to maintain. To help you organize it better, the plugin provides a few small features that can save you a lot of time when used correctly.

String conditions

When using conditions, you might want to create some quite complex logic using nested condition groups. This can rather quickly result in longer config sections, which is why you can use string conditions to write complex expressions in one line. Let's take this example:

conditions:
  - "{amount}>100"
  - mode: OR
    conditions:
      - "{message}.>creeper"
      - "{message}.>aw man"

Using string conditions, we can "compress" it into only two lines:

conditions:
  - "[{amount}>100|({message}.>creeper|{message}.>aw man)]"

The brackets seen above are called condition groups. You can imagine them like the conditions: property in the first example. A condition group can have two different modes, either AND or OR, determining how conditions will be checked. You can create an AND group by using the [] brackets, and an OR group by using the () brackets.

Conditions inside groups can be separated using |, and condition groups can contain other condition groups, which allows our example from above to be recreated in a string condition.

Custom placeholders

Conditions are essential to limit execution on an action, but there are certain scenarios where you might want to have the same or similar logic on multiple actions. Let's say that we have a few actions, spawning mobs that viewers can specify in their donations:

spawn_zombie:
  enabled: true
  action: twitch_bits
  conditions:
    - '{message}.>zombie'
  steps:
    - message: '[title]<red>Spawning zombie...'
    - command: 'execute at {player} run summon zombie ~ ~1 ~'

spawn_skeleton:
  enabled: true
  action: twitch_bits
  conditions:
    - '{message}.>skeleton'
  steps:
    - message: '[title]<red>Spawning skeleton...'
    - command: 'execute at {player} run summon skeleton ~ ~1 ~'

spawn_wither:
  enabled: true
  action: twitch_bits
  conditions:
    - '{message}.>wither'
  steps:
    - message: '[title]<red>Spawning wither...'
    - command: 'execute at {player} run summon wither ~ ~1 ~'

You can see that we have to repeat a lot, which is going to make it harder to change things like the message displayed, or the position the mob will spawn at. The idea behind custom placeholders is that you can create your own placeholders that change based on conditions. Using a custom placeholder, we can simplify all of this to one action:

actions:
  spawn_mob:
    enabled: true
    action: twitch_bits
    steps:
      - message: '[title]<red>Spawning {!mob_from_message}...'
      - command: 'execute at {player} run summon {!mob_from_message} ~ ~1 ~'

custom_placeholders:
  mob_from_message:
    default_value: 'zombie'
    skeleton:
      conditions:
        - '{message}.>skeleton'
    wither:
      value: 'wither'
      conditions:
        - '{message}.>wither'

You can see that our custom placeholder mob_from_message is defined in the custom_placeholders property. Custom placeholders can have different state-based values, which are all of the values your placeholder can have. A value works just like a condition group, meaning it can have conditions, donation conditions and different modes determining if it should be used. If all conditions of a value match, the placeholder will return the name of that value.

If you want to return a string containing characters that you can't use in a yaml property name (like :), you can use the value property of the to override the return value (example in the wither value above). A custom placeholder can also have a default value, which is used if none of the state-based values have matching conditions. A good thing to keep in mind as well is that the values are processed from top to bottom, meaning if the skeleton and the wither conditions in our example are both true, skeleton will be used. The default value will always be used last, regardless of where it is placed.

Custom placeholders act just like any other placeholder, but look slightly different: {!my_placeholder} (the exclamation mark differentiates them from normal placeholders). You can also use custom placeholders in the result or conditions of another custom placeholder, allowing for more complex structures.

Functions

Functions are a common concept in programming, since they allow for the creation of dynamically reusable blocks of code, meaning the same block of code can be used from different places in the program, with different options that alter the behavior of the code block. Those "options" are called parameters, which are given to the function when it's being called (executed), and can then be used anywhere in the function.

Our plugin implements functions as a separate list of steps that can use special parameter placeholders. This is how a function could look inside of a configuration file:

functions:
  an_example_function:
    steps:
      - message: "It works!"
      - message: "<red>Killing {$$player_to_kill}"
      - command: "kill {$$player_to_kill}"

You'll notice that the steps of our example function use a new type of placeholder, looking like this: {$$placeholder}. This is our special parameter placeholder, which is only available within the function. Now, how do we actually call this function and give it parameters?

actions:
  test_action:
    ...
    steps:
      - function: an_example_function
        parameters:
          player_to_kill: "YourMinecraftUsername"

In this example, we're calling our example function from within an action using the function step, and setting the player_to_kill parameter to "YourMinecraftUsername". This means that the {$$player_to_kill} placeholder will be replaced with "YourMinecraftUsername" within the function.

When to use functions

You might be wondering how and when functions can be used to save time and create "cleaner" configurations. We'll be going through a small example use case that'll help you understand exactly that:

actions:
  gifted_memberships_message:
    enabled: true
    action: youtube_gift_memberships
    steps:
      - message:
          - "<green>-----------------------------------------------------"
          - ""
          - ""
          - "<gray><aqua>{user}</aqua>just gifted <red>{amount}</red> memberships, thank you :D!"
          - ""
          - ""
          - "<green>-----------------------------------------------------"
      - command: "execute as @a at @s run playsound minecraft:entity.player.levelup master @s ~ ~ ~ 100 1"

  bits_message:
    enabled: true
    action: twitch_bits
    steps:
      - message:
          - "<green>-----------------------------------------------------"
          - ""
          - ""
          - "<gray><aqua>{user}</aqua>just donated <red>{amount}</red> bits, spawning zombies!"
          - ""
          - ""
          - "<green>-----------------------------------------------------"
      - command: "execute as @a at @s run playsound minecraft:entity.player.levelup master @s ~ ~ ~ 100 1"
      - repeat:
          - command: "execute at {player} run summon zombie ~ ~ ~"
        amount: "{amount}"
        server_thread: true

  raid_message:
    enabled: true
    action: twitch_raid
    steps:
      - message:
          - "<green>-----------------------------------------------------"
          - ""
          - ""
          - "<gray><aqua>{user}</aqua>just raided the stream with <red>{amount}</red> viewers!"
          - ""
          - ""
          - "<green>-----------------------------------------------------"
      - command: "execute as @a at @s run playsound minecraft:entity.player.levelup master @s ~ ~ ~ 100 1"

In the example, we're responding to 3 different events: Bits on Twitch, gifted memberships on YouTube and raids on Twitch. For each of them, we're sending a message and playing a sound. For the bits, we're also spawning zombies for the amount of bits gifted. As you can see, the biggest part of the entire configuration is sending the message and playing the sound on every action.

Using functions, we can put all of the message & sound functionality in a separate function, and control the message displayed using parameters. Here's how our example would look with functions:

functions:
  donation_effect:
    steps:
      - message:
          - "<green>-----------------------------------------------------"
          - ""
          - ""
          - "{$$message}"
          - ""
          - ""
          - "<green>-----------------------------------------------------"
      - command: "execute as @a at @s run playsound minecraft:entity.player.levelup master @s ~ ~ ~ 100 1"

actions:
  gifted_memberships_message:
    enabled: true
    action: youtube_gift_memberships
    steps:
      - function: donation_effect
        parameters:
          message: "<gray><aqua>{user}</aqua>just gifted <red>{amount}</red> memberships, thank you :D!"

  bits_message:
    enabled: true
    action: twitch_bits
    steps:
      - function: donation_effect
        parameters:
          message: "<gray><aqua>{user}</aqua>just donated <red>{amount}</red> bits, spawning zombies!"
      - repeat:
          - command: "execute at {player} run summon zombie ~ ~ ~"
        amount: "{amount}"
        server_thread: true

  raid_message:
    enabled: true
    action: twitch_raid
    steps:
      - function: donation_effect
        parameters:
          message: "<gray><aqua>{user}</aqua>just raided the stream with <red>{amount}</red> viewers!"

As you can see, we'll have to repeat much less configuration, which allows us to save time, and can also help avoid "copy-paste-mistakes" like forgetting to change something in a block of steps that was copied to 5 different locations, since you'll only have to modify the configuration in one place. You might be able to see how functions work similar to custom placeholders, both can help avoid repetitive configuration.

The impact of adding a function in the example above might not seem that big, since it doesn't change the configuration size drastically, but this will become much more clear in a bigger configuration. On top of that, the added benefit of not having to repeat configuration can save you a lot of time!

Functions with outputs

In the same way that parameter placeholders can only be used within a function, any other placeholder created within the function will only be accessible from that function. This does mean placeholders from outside the function can still be used, but it's recommended to use parameters instead, since they provide a more flexible approach of providing values to the function.

But outside of that, this does create one issue when working with functions: How can a function return a value to the action it was called from? Say we have a function that extracts some data from a minecraft command:

functions:
  get_time:
    steps:
      - command: "time query {$$time_mode}"
        output: "raw_time"
      # We need to extract the time using an expression query, since the command output looks like this:
      # The time is 6000
      #             ^^^^ (<- Our expression is matching this part of the output)
      - expression: "\\d+" # Matches any number
        input: "{$raw_time}"
        output: "parsed_time"

We might use the function like this in some action:

- function: get_time
  parameters:
    time_mode: "daytime"

This would make the function execute /time query daytime and parse the actual time number out of the result. Now you might be asking what the issue with this is. Say we now want to check for the time being higher than 13000, meaning it is currently night:

- function: get_time
  parameters:
    time_mode: "daytime"
- check:
    - message: "It is night!"
  else:
    - message: "It is day!"
  conditions:
    - "{$parsed_time}>13000" # https://minecraft.wiki/w/Daylight_cycle

Here, we're using the {$parsed_time} placeholder from the expression query in our function to check for the time. But remember how we can only use placeholders created inside the function in that function? Well, we're using the expression query in that function, meaning we won't be able to access the placeholder it creates. What we need is some way of "transferring" the placeholder from the function to the place it's called from. This is where the output of a function comes into place:

functions:
  get_time:
    steps:
      ...
      - expression: "\\d+" # Matches any number
        input: "{$raw_time}"
        output: "parsed_time"
    output: "{$parsed_time}"

This special property of a function will accept any placeholder from within the function, and "transfer" it to the step it was called from. That's great, but how do we now access the data from that step?

- function: get_time
  parameters:
    time_mode: "daytime"
  output: "the_time"

As you can see, the function step can optionally be used as a query, just like the command step. We can now grab the output of our get_time function (which we previously set to the {$parsed_time} placeholder within the function) as another placeholder, in this example {$the_time}! Now let's insert that into our previous check, and our complete configuration now looks like this:

functions:
  get_time:
    steps:
      - command: "time query {$$time_mode}"
        output: "raw_time"
      # We need to extract the time using an expression query, since the command output looks like this:
      # The time is 6000
      #             ^^^^ (<- Our expression is matching this part of the output)
      - expression: "\\d+" # Matches any number
        input: "{$raw_time}"
        output: "parsed_time"
    output: "{$parsed_time}"

actions:
  test_action:
    ...
    steps:
      - function: get_time
        parameters:
          time_mode: "daytime"
        output: "the_time"
      - check:
          - message: "It is night!"
        else:
          - message: "It is day!"
        conditions:
          - "{$the_time}>13000" # https://minecraft.wiki/w/Daylight_cycle
⚠️ **GitHub.com Fallback** ⚠️