Sims 4 Tuning 101: A Deep Dive into how Tuning is Generated from Python — Part 4

Dominic M
14 min readJun 3, 2021

This is a continuation of my last article about how tunables get loaded into the game and converted into Python objects. In this article, I will show some tangible (if somewhat contrived) examples of how one can use scripts to inject into each type of tunable.

This article will assume you have some understanding of XML tuning and Python scripting as it pertains to the Sims 4 codebase, but I will try my best to explain things in a way that doesn’t force you to know Python in depth.

Section 4: Script Injection

The previous three articles focused on more abstract principles, but they’ve all been leading up to this. We are finally ready to explore some tangible examples of how to use what we’ve learned to inject into actual in-game tuning!

This article will be separated into the sections by tunable type and will detail some common strategies that can be used, some pitfalls you might face with them, and, of course, an actual example that shows you how to use script injection on the tunable.

In this section, we will leave out:

  • TunableFactorys, since they are a little more complicated;
  • Strings and Resources, since they can’t really be altered directly, only swapped out with each other, which amounts to what will be shown in the Tunables section*;
  • TunableVariants, since they basically resolve to the tunable that was described in the XML. Because of this, the only novel “strategy” here needed for them is knowing which tunable was chosen at runtime. While this is not trivial, it will be explored when we get around to making our own tuning, and so for the time being I am going to ignore it, as once you know what tunable you have, you can apply the strategies here to modify them.

Tunables

Probably the simplest tuning type to modify, modifying tunables is about as straightforward as you may imagine: get the tunable from the class and simply re-assign its value. That’s it! Depending on what it’s from, more work may be involved (when we get to TunableTuples, this will become more evident), but in essence, there’s not much to it.

For an example, I am going to change the category of some interactions in game, namely some of the Vampire socials, so that they show up under the Vampire category instead of the Friendly one.

The Interactions

Before we can begin, we must find the interactions we are moving around. In this case, I will use 3 vampire interactions:

  • Enthuse About Vampires (mixer_social_EnthuseAboutVampires_Targeted_Friendly_AlwaysOn / 149272)
  • Confess Fear of Vampires (mixer_social_ConfessFearOfVampires_Targeted_Friendly_AlwaysOn_STC / 149273)
  • Debate Existence of Vampires (mixer_social_DebateExistenceOfVampires_Targeted_Friendly_AlwaysOn / 149274)

As you can see, all these interactions are Mixers (SocialMixerInteraction to be more precise). Using this information, let’s browse the TDESCs of SocialMixerInteractions and see how/where the Interaction Category is defined.

category tunable from the Interaction class

The Pie Menu

As we can see here, it’s a TunableReference to a PieMenuCategory tunable. Since we want to replace the category the interactions show up in, we must find the Pie Menu Category for the base Vampire menu.

After some browsing in Sims 4 Studio, we see it’s pieMenuCategory_Vampire with instance ID 154678.

Extracting tuning to find the Pie Menu Category for Vampires

So, now that we have the interaction IDs we need and the Pie Menu Category ID we have, let’s put it all together.

The Script

So, the tunable that we’re going to be overriding/reassigning is the category tunable on the given interactions, which we can access directly as properties on the classes. This is a fancy way of saying, once we have the class, we simply need to do cls.category = category.

As we saw previously when defining TunableReferences, to get the InstanceManager object, we can use the following method from the services module: get_instance_manager. InstanceManagers have a useful method that we will be exploiting here and throughout the rest of this tutorial, add_on_load_complete, which takes a function and will invoke it (i.e. execute it) as a callback after all of the Instances of that type have been loaded. These functions will be passed as a parameter the instance manager that you called it with. With this information in mind, we can invoke a callback that will take the PieMenuCategory InstanceManager, load the Vampire pie menu, load the Interactions, and then set all of the interactions’ category as the Vampire pie menu.

One of the best ways of organizing the tunable IDs you need together is to use an Enum, which we have already covered in some depth before. One benefit of Enums is that you can iterate over them to get all of the IDs contained therein.

So, let’s begin with laying out the Enum for our interactions and the ID of the Vampire pie menu category (since it’s only one value, we can just declare it as a constant value).

Since Interactions is a class, we can add functions and methods to it. Here, I’m going to add a class method (a method that works on the class itself instead of an instance of the class) that will attempt to get all of the interactions from the game’s Interaction InstanceManager.

I’ll try to explain what I’m doing here since this is the first time we’re going to be seeing something like this. After this example, I am going to brush a little bit over some of the bits where I load data from the game and focus more on the injection part.

  1. Since the function is a class method, we need to use the @classmethod decorator. Since it’s a class method, by convention we use a parameter named cls instead of self. The -> tuple part is called a type declaration and while it is optional, it can be useful if you are coding in something like Pycharm. It tells your IDE what the type of the function’s return value will be, which can be useful.
  2. We get the interaction instance manager and declare an empty list where we will store the interactions we fetch. We do this so that, if for whatever reason the interactions cannot be found (they should all in this case if you have the Vampires GP, but this template is good for when you’re attempting to load interactions and not all of them are expected to be found by the game, like in the case of pack-specific add-ons).
  3. We loop through all of the values in the Interactions enum class, which will give you all of the instance IDs that you defined. We will then attempt to fetch the interaction using the get method of the interaction instance manager, and then add it to the interactions list only if the interaction was found — i.e. not None; Python will convert anything that is None, 0, "" (the empty str), or empty (i.e. has a len of 0) to False automatically for you when using if statements, and everything else to True. This is the same as if potential_interaction is not None:.
  4. Finally, we convert the list to a tuple so that it’s immutable and return it.

Now, we create the callback function. Since its first and only parameter will be an InstanceManager, I call it manager but it can be called whatever (self is also a good name). In this case, the instance manager will be for the PieMenuCategory tunable type.

  1. We surround the method in a try/except block so that if any errors happen for whatever reason, we just ignore the method. This will prevent the InstanceManager from getting really angry at you and making Blue Squares of Death or kicking you back to the World Selection menu (I talk from personal experience).
  2. If the Vampire pie menu cannot be loaded, we exit out of the function since we need it to do what we need and thus it cannot be None.
  3. We loop through all of the loaded interactions from the Interactions enum using the class method we just created and update the category property on the class so that it now is the Vampire category.

All that’s left to do is to get the code running. We will do this by using the Instance Managers add_on_load_complete method.

Here we simply define the method that will execute the code (do_injections) and then immediately invoke it. We fetch the instance manager for Pie Menus and pass it the method we declared earlier (change_interaction_categories) that does the actual injection.

Finished result tested on Caleb Vatore

And voilà, we are done. We have successfully replaced the pie menu for the given interactions! You can view the full code here.

TunableLists

If you recall from the last article, TunableLists get loaded into the game as tuples. Since they are inherently immutable, you will need to work around this fact. There are two main ways you can go about this:

  1. Use the += operator. This operator expects the right hand side to be an iterable and not a single value. This means you can use a list, another tuple, or anything else that is iterator to join the values together. Since the += operator returns a new tuple, you will not be updating the tuple so much as simply reassigning the value of the variable.
  2. Declare a list that will hold the values of the tuple. Since it is now a list, we can directly add values to it, or use something like the extends method which works like the += operator. You then need to convert the value back to a list and update/reassign the tunable.

For this example, I will show how to add a _loot_on_addition to a buff, which is something that you may find yourself often needing to do.

Foreword

_loot_on_addition TunableList on Buffs

As we can see, Buffs have a property called _loot_on_addition, which is a list of loots that will be applied to the Sim when the Buff gets added to them. _loot_on_instance is for when the Sim gets loaded with the Buff (for example, Vampires will load with the core buff of their Vampire trait; only new vampires will have the _loot_on_addition list be executed). The _loot_on_removal list does as you’d expect, and is a list of loots that are applied when the Buff is removed from the Sim.

Note that you may often want to add both to the addition and to the instance lists when injecting into Buffs so that no matter how the Sim received the buff, the loot list will be applied to the Sim.

The Situation

Say we wanted Vampire Sims who are bitten to have a random chance of dying from Elder Exhaustion.

There are many ways we could go about this, but one of the easiest would be to inject into the Buff that Sims get when they are bitted and add a loot that triggers a reaction (in this case, the Elder Exhaustion interaction).

First, we will need to find the buff in game.

  • buff_Vampire_RecentlyBitten / 150749
  • buff_Vampire_RecentlyBitten_Strength2 / 151763
  • buff_Vampire_RecentlyBitten_Strength3 / 151764

We will also need to make the ActionLoot:

reaction Loot Operation

A reaction Loot Operation forces an interaction upon the Sim. We will be using this to push the ElderExhaustion death interaction upon Sims who get the buffs we found above, at around a 20% chance — we don’t want Sims to just be dropping dead!

LootAction that pushes the death_ElderExhaustion interaction upon the Actor Sim with a chance of 20%

The Script

This script will follow a lot of what the previous one did, but instead will need to use the Buff and Action instance managers. In this case, I will use the Action instance manager as the “base” one and the Buffs will be loaded from a Buffs enum.

Here, I show another way of doing the same thing as before, but this time I use a more functional programming style. buffs is populated using a list comprehension which is a very compact way of creating a list in Python. I then use the filter function to remove all of the loaded buffs that were unable to be found (i.e. were returned as None, which is a falsy type value).

Here we see the general method that can be employed to inject into TunableLists. First, I store the buff’s _loot_on_addition in a variable called loots_on_addition, converting buff’s loot list tuple into a list. I also use this idiom here: value or default which is an idiom that can be used in Python to ensure that you get a valid default value. If the buff does not have a _loot_on_addition tunable (i.e. it is None), then the expression evaluates to [] which is an empty list. Therefore, we can ensure that the list function is always passed an iterable (if you passed None you would receive an exception:

TypeError exception when attempting to run the list function with None as an argument.

We then append the loaded loot to the loot list and reassign the _loot_on_addition tunable to the loots_on_addition list after converting it back to a tuple.

Putting this all together, we get the following:

And here we can see lilsimsie unfortunately dying after being bitten by a vampire.

As an aside, you may also want to tune the reloot_on_add tunable on buffs you add loots to if they did not already have loots on them before. This tunable ensures that if a Sim is pushed the buff again but they already have the buff, the loot list is still fired. For this case, a Sim cannot be bitten again after they have a Recently Bitten buff, but in general this is something you may want to consider.

TunableSets

So, this one will be a silly and contrived example, but it will demonstrate the method that can be employed on TunableSets. For this, we are going to take a loot at the Day and Night tracking of Traits, in particular, the Day and Night tracking of the Young Adult trait (trait_youngAdult/34318).

Here, I’m going to add a buff to Young Adults that triggers in the day between the hours of 8am and 10am where the Sim will crave coffee.

The Tuning — Buff Base

First of all, let’s get the tuning out of the way. We’re going to need the Coffee buff that Young Adults will get, and an extra buff that they’ll get when they drink coffee. And finally, we’ll add a mixer interaction & buff that results from it that will run when they have the buff on them. So, without further ado, let’s get started.

Let’s start with just the buff that will be added to the Sim through the day/night tracking system.

The Script

The script for this — at least the actual part needed for the injection — is going to be rather minimal, as a lot of what we will be doing will actually be on the tuning side. Because of that, we are going to be exploring some other things that will be helpful to know. We are even going to take a sneak peak at how to deal with TunableTuples, but I won’t dwell on that until we cover them specifically.

Since we only have one trait and one buff, we do not need to be elaborate with them. Thus, we can have a rather simple script base:

Here I added a utility method that will get a tuning for us, get_tuning. We pass it the tuning ID and the tuning type and it will attempt to get the tuning from the proper instance manager.

Now, let’s get into the meat of the injection:

There’s a few things of note here. First, we have a method that creates a BuffReference. BuffReferences are what’s using in game when a buff can be added to a Sim with a tunable reason. BuffReferences take a buff reference and an optional localized String. To create a localized String, we can use the _create_localized_string method of the sims4.localization module, which takes a String ID and optionally tokens (which we in this case do not need, so they can be ignored).

At the time of writing this, the Young Adult trait does not have any Day/Night tracking buffs. Because of that, it is a None object, so we need to create the ImmutableSlots object ourselves. With that being said, we can treat it as thought it does exist, because for all we know, in the future, it might. This is an idiom that is good to follow, as it avoids potential breaking with patches.

Now, to explain the actual part of the script that’s pertinent to TunableSets:

  • In the variable buffs_to_add, we store a set of the BuffReferences we’re going to add. In this case, it’s only one, but in general we may be adding more.
  • We grab the set from the place we want it (the day_night_tracking ImmutableSlots object on the Young Adult trait) and we convert it into a set so that it can be modified.
  • We use the update method on the set, which joins the two sets together (this is also why we store the other value in a set object) and updates the set (it the same as the |= operator).
  • Now, with the modified set, we need to convert it back to a frozenset and then update the object it came from. I will explain in more detail how this is done when I cover TunableTuples.

The full script for this can be found here.

lilsimsie desperately needing coffee

TunableMappings

TunableMappings get loaded into the game as frozendicts, which are essentially dictionary objects that are immutable. If we remember from my last article, dictionaries have keys and values and the keys are used to retrieve the values. The keys also need to be hashable, but most tunables you won’t need to be concerned about as they are inherently hashable.

As before, in order to inject into TunableMappings, we will first need to convert the frozendict into a normal dict, add the values we want, and then convert back and update the tunable value.

The Tuning

Going back to the previous example, we are going to override some of the buffs that Young Adult Sims get when they drink coffee, namely the dazed buffs they receive when they drink too much (there is no such thing as too much coffee!).

To do so, we will first consult the TDESCs. As we can see, we have a classic example of the key/value pair in a TunableList, which is one of the hints that this is a TunableMapping (we can also click on the tunable and look for it to tell us what class it is).

buff_replacements TunableMapping on Traits
TDESCs description which demonstrates the class for the buff_replacements TunableMapping

This is a mapping between the buffs that will get replaced and the buffs that will replace them, with an optional priority (since multiple traits can have conflicting replacement buffs, and this is one of the ways the game uses to resolve which will be the ultimate outcome).

Tuning-wise, we will need two things: the list of buffs we want to replace, and the buffs we want to replace them with. Since we are going to create the replacement buffs, we must start by finding the IDs of the buffs we want to replace:

Footnotes

  1. * For Strings in particular, they often are backed by SimData on the Instance as well (think: Traits, Strings, Commodities, etc.). Because of this, you cannot modify the String used on these Instances using scripting, as the SimData will still reference the old code, and thus nothing will be visually different. SimData is, in a way, “stronger” than XML tuning.

--

--

Dominic M

A Sims 4 Modder, CS Student, & Designer of the Sims 4 Support Bot Bella Goth