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

This is a continuation of my last article about XML tuning. In this section, I will focus on how the XML tuning is defined in the Python.

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 2: Defining Tunables

In my last article, I walked through how the XML tuning is defined and went briefly over how they function in the context of the XML itself. In this article, I am going to revisit each tag and explain the ways that these can be defined in Python. Bear in mind that their Python representation is just that — a representation. They are loaded into the game separately from how they are defined, a fact that is important to keep in mind if you want to do Python injection or make custom tuning. My next article will cover the way the game converts the XML it loads into Python objects the game uses, and how modders can manipulate them.

Foreword

So, before I dive into the tags, I want to take a minute to explain something that will be more important later on, but which doesn’t fit into another section well, either. Certain tunables, namely TunableTuples and TunableVariants, allow for named properties. While these names can be just about anything (that is a valid identifier in Python*), there are certain restrictions which mainly are put in place because the names are needed for other purposes and allowing them would cause the Python to get confused.

These 20 values are prohibited by the game, meaning you will never see an n or t with one of these names. Likewise, attempting to define a tunable with one of these names will cause an Exception.

Most of these names are pretty easy to avoid, but default has gotten me at least once, so it’s important to be aware of them.

Also, all tunables have a description field, which for the sake of brevity, will be left out in the code samples I will be giving here. In the game’s own Python, the description field is used to generate the description for the tunable in the exported TDESCs, but since modders must build their TDESCs by hand, this is largely more for self-documentation than anything else. It is still good to put a description when appropriate, but know that it will largely be ignored by the game’s Python.

Another shortcut that I will be using in the code snippets here for sake of brevity is assuming that the proper classes from the sims4.tuning.tunables module are imported for each snippet. When additional modules, however, are needed to be imported, I will include those.

The <T> Tag

I am going to start with one of the simplest tags, the <T> tag, and explain how it is defined and how it works. Do not be fooled, however: the <T> tag is actually a few different things all at once, which I briefly went over in the last section.

To recap, there are three main types of <T> tags:

  • Primitives,
  • References, and
  • Resources.

Each one is defined in the Python, unsurprisingly, in different ways. In this section, I will cover each of them.

Primitives

These values are primitive in the sense that they are “simple” values and mirror the primitives of Python.

As I alluded to in my last section, “primitive” <T> can hold the value of three types of Python primitive data structures:

  • Booleans (the bool object, True or False);
  • Numerical data (int and float objects); and
  • Text (the str object).

Let’s start by taking a look at the Python definition of the Tunable class, taken from sims4.tuning.tunable. This code is as of Game Version 1.74 and, while relatively unlikely, is subject to change.

So, let’s digest the __init__ function a little bit. For our purposes, we are most interested in two parameters here: tunable_type and default. They function more or less how you’d expect from their name: tunable_type defines the type of tunable, and default defines the default value, should you not provide one. The other parameters are more for the exporting of the TDESCs, which we don’t really as modders have the ability to do, so they can pretty much be ignored here.

Note that attempting to define a Tunable whose tunable_type is anything other than str, bool, int, float, or a Resource type (which will be covered more in depth in a bit) will result in an Exception.

References

These are the type of <T> we are all most familiar with. This type of <T> references another tuning class (an <I>), and the value between the tags matches the instance ID (s attribute) of the tuning file.

From a conceptual standpoint, Reference Tunables (called TunableReferences) are relatively easy to understand. For someone with an understanding of programming languages like C and C++, these can be thought of as pointers to other tuning files.

Now onto the Python definition of the TunableReference class, taken from sims4.tuning.tunable:

There are two main things to take note of here: manager and class_restrictions. Since TunableReference inherits from something called _TunableHasPackSafeMixin, TunableReference also has access to a parameter called pack_safe.

  • manager: This parameter informs the code of what InstanceManager to use; in other words, it defines the type of reference for this TunableReference object.
  • class_restrictions: The class restrictions parameter defines the types of classes that are allowed by this reference. Some Tuning types (that is, the <I>s) have many different subclasses, such as Interaction, which has subclasses like SuperInteraction, ImmediateSuperInteraction, MixerInteraction, DeathSuperInteraction, etc. Here, you can restrict which types of Tunings you want to allow, and attempting to use anything else will throw an Exception. By default, there is no restriction. Note that this must be an iterable type of classes**.
  • pack_safe: This is a Boolean (True/False) value that determines if the reference is “pack safe.” A pack safe reference is one that, when unable to be found by the game, will be skipped over. If a reference is not marked as pack safe, a failure by the game to find the reference will result in an Exception.

Let’s take a look at how to define a TunableReference object in Python:

Note that in the above example, I only have one class restriction, Skill, but you could have in theory as many as you want. These classes, however, must be Tunable classes. You will need to import these classes into your Python code in order to have access to them; you cannot simply pass the string 'Skill' for example and expect the game to know what you mean.

The manager parameter is provided an instance manager object. In most cases, you can do this by using the function get_instance_method of the services module and passing in the appropriate enum value from the Types enumeration, part of the sims4.resources module.

Resources

These are <T>s that reference values that are not other types of XML tuning (that is, not an <I>). Notable examples of this include Strings, Object Definition IDs, Icons, and CAS Parts.

Resource tunables are a whole thing unto themselves, but one of the most commonly encountered ones are String objects (not to be confused with Python strings, which are the str object, and can also be stored as Tunables). In this instance, String refers to the text seen on screen, and which is stored in String Tables.

Strings are can be defined by importing the TunableLocalizedString or TunableLocalizedStringFactory class from the sims4.localization module. Which one you will need will depend on if you want to allow for tokens to be used (such as the Sim’s name, gender, etc.).

Both String classes have a parameter of interest, allow_none, which as the name suggests, is a Boolean flag that determines if the String is allowed to be empty. Some Strings, such as the names of Interactions, can be none, but other Strings, especially ones you define yourself in your own code, you may not want to allow to be empty. By default, this flag is set to False.

Out of the three examples, Strings are defined really rather simplistically; there’s not much to them.

The <E> Tag

As stated before, Enums, or enumerations, are a named group of limited values, where each value in the enumeration is associated a specific name. And, unlike some of the other Tunables, Enums are particularly dependent on the Python notion of Enum, from which they are constructed.

The actual syntax of declaring a TunableEnumEntry object is the same for both dynamic and non-dynamic enums, and the behaviour between them is dependent on the enum class used to declare them. Since they share the same declaration tunable-wise, I will first cover that, and then talk about how to declare their enumeration classes after.

The Python: TunableEnumEntry

To start off with, let’s take a look at the __init__ function of the Python class. First thing to note is that this class inherits from the _TunableHasPackSafeMixin mixin class which means that you can pass it the pack_safe parameter when constructing them.

Apart from that, there are two parameters that are of note here:

  • tunable_type must be an Enum class or the Python will throw an Exception. As with other tunables, the tunable_type parameter is used to define what the tunable is made of.
  • invalid_enums is a tuple (the Python kind) of elements that defines which elements from the corresponding Enum class are not valid.

Non-Dynamic Enums

Non-Dyanmic Enums are in a sense the more “basic” of the two, as they are defined purely in Python and need no external tuning to back them up. Non-Dynamic Enums are good for when you know ahead of time every possible value, and are better suited for when the Enums are needed also in the Python scripting, as all of the possible values are known at run-time.

This does not mean that you cannot refer to the dynamic values in your Python, just that the values that are available to you will be defined outside of the source code where your Python enum is defined.

To define an enum class is relatively straightforward. I will use an example from the game’s own Python.

As we can see here, the basic components of an enum are relatively straightforward: you define the elements you want (these names are by convention often written in CAMEL_CASE) and give them a value.

In the XML, you will refer to the values by their name. For example, one might find <E>CONTAINS_ANY_IN_TAG_SET</E> from an TunableEnumEntry that used TagTestType as its enum base.

Dynamic Enums

Dynamic enumerations, such as those from the Tag enum, have their values loaded by the game and are described in tuning modules.

These are a little more complicated than the ones above, but they are, at least in the Python, defined similarly. I already in the previous article described and showed an example of how the dynamic elements are defined in the XML.

Dynamic Enums have special syntax, and uses tags we have already discussed: <M> and <C> to define the Python class and Enum name; an <L n="_elements"></L> declaration which defines the dynamically loaded elements, and a list of <T>s with the special attribute ev for enum value.

There are two main types of Dynamic Enums, Locked and Unlocked ones. Locked ones, as the name suggests, does not allow for the addition of new enum elements, and attempting to do so will cause an error. Unlocked (which is the default) Dynamic Enums will have no such qualms about being modified.

Using an example from the game’s Python:

In the above example, we can see how the static elements are delcared in the enum itself (in this case, and in many cases throughout the game’s code, Invalid).

The <L> Tag

The <L> tag is in essence the basis for pretty much all of the iterable tunables, iterable being a programming term that means you can go through all of the elements within them. For some more information on what an iterable is and what it even means to be iterable, check out this link here.

Since <L>ists are the basis for so many different tunables, it can be a little hard to know which type you are getting just from the XML alone, which is important when we discuss how these tunables are loaded into the Python in the next section of this series.

I will cover each of the main types of tunables that use the <L> tag below, and talk about what makes each of them unique. Before I do that though, I want to take a look at the prototype of the _TunableCollection's __init__ function, as _TunableCollection is the base class for all of the following tunables, and so some of these parameteters will be important/show up later.

Here, the two most important parameters to the __init__ function are tunable and minlength.

The tunable parameter (which need not be referred to by name when creating instances of tunables***) defines the type of element the collection consists of. These can be Tunables, TunableReferences, resources, other tunables like TunableTuple; pretty much any other tunable can be the element in a collection.

The minlength parameter is optional, but when set, tells the Python code the minimum number of elements allowed in the collection; if the Python determines when loading the XML tuning that there is not at least that number of elements, it will throw an Exception.

TunableLists

These are just a group of items. Some lists have restrictions, such as the maximum or minimum amount of elements allowed within the list, but for the most part, they’re what you expect.

As TunableLists are sort of the “basic” one here, the only parameter worth mentioning is set_default_as_first_entry. If this flag is set, the default value for the list will be a singleton (i.e. one element list) with the default value as its first and only element. Otherwise, the default of the list will be an empty list with no elements.

Note that when using the set_default_as_first_entry flag, the default value that will populate the resulting list will be determined by the tunable the list is of. In the code example above, I have it set to 0, but if I had instead used Tunable(tunable_type=int, default=10), the default value of the list would be a list with the item 10.

TunableSets

Sets are a math term for a group of unordered elements (i.e. {1, 2, 3} and {3, 2, 1}, which are to be considered the same, even though the order is different). Each element in a set is by definition unique: multiple instances of the same value would be ignored.

TunableSets function must the same way TunableLists do, though there are some differences. The most notable difference is that unlike TunableLists like we saw above, there is no option to set_default_as_first_entry meaning that an empty TunableSet will be interpreted by the Python as an empty collection.

There is another caveat that, since the fundamental data type being used to construct TunableSets once the XML tuning gets loaded is a set object and not a list object, the constraints of sets in Python are imposed. What this means in practical purposes isn’t much, but this does impose the limitation of the elements needing to be hashable. Localized String objects, for example, are not hashable, but all Tunable objects including TunableReferences are.

TunableEnumSet

These are <L>s whose values are of <E>s. This means that the values you can put in are limited.

TunableEnumSets are far more interesting, however, than their base class, TunableSet.

Taking a look at its __init__ function, we see parameters that mirror those of TunableEnumEntry.

  • enum_type defines the enum class that will be used to fill the collection
  • enum_default is used to control what the default value of the TunableEnumEntry values that fill the collection will be. By default, if not set, this will be the first item of the Enumeration. For in-game enums, this will often be Invalid.
  • invalid_enums allows for determining enum values that you do not want to allow in the collection
  • allow_empty_set is a flag that determines of the set is allowed to be empty.

An example of a TunableEnumSet of DeathTypes can be pictured above.

TunableMappings

These are special <L>s that are made up of <U>s with two values, usually named key and value. A mapping, also called a dictionary, is an association between a name (the key) and a value (the value). When we discuss the Python side of things, we will talk more in-depth about dictionaries, which in Python are found in the form of dict objects.

TunableMappings are maybe one of the most interesting of the tunable collections, because their definition in XML (<L>s of <U>s) is counter to how the game actually loads them. This will be discussed more in depth in the next section, but suffice it to say, they are a bit more complicated than what might appear at first blush.

Taking a look at the __init__ function’s prototype for TunableMapping, we see that the parameters are largely split up into two main categories: those for the key and those for the value.

  • key_type defines the tunable used for the keys. They can be any valid tunable that is hashable. For all practical purposes, that means most things, but for example, Localized Strings are not valid keys.
  • key_name defines the name of the key. This will be the the n attribute for the tuning used in the <U>s that make up the key. By default, this is key but this can be changed to any of the valid identifiers discussed in the forward.
  • value_type defines the tunable used for the values. Unlike the keys, this has no such restriction on hashable, and can be any tunable object.
  • value_name defines the name of they value. This functions similar to the key_name but obviously is for the values of the mapping and not the keys.

For TunableMappings, I will use both examples of tuning and XML to demonstrate what I mean, since it may be a little abstract at first what the above describes.

First, let’s start with a “basic” mapping that does not define names for the keys or values and that maps strs to ints.

On the XML side of things, an example can look something like this:

This time, using more complex tunable types, a TunableReference to Traits as they key and a TunableLocalizedStringFactory as the value, here is an example with naming the key and value attributes in the TunableTuple:

In game, many of the TunableMappings you will encounter simply use the default names of key and value, and to be fair, that is a perfectly valid thing to do. However, as a bit of a personal choice, I like to give descriptive names to the keys and values, as it helps remind me what they are being used for.

The <U> Tag

“Data” Tuples

A TunableTuple object is basically a way to group related tunable values together, giving them a specific (and hopefully descriptive) name.

A basic, pure “data-based” TunableTuple object is essentially a grouping of tunables all under the same name. For those of you familiar with the C programming language, this can be thought of as a struct.

At first glance, there’s nothing really too special about TunableTuples in terms of their constructor. The argument of most note is the locked_args one, which I will come back to.

When defining a TunableTuple, you provide it with an arbitrary list of tunables as keyword arguments. Their names can be more or less anything, subject only to the restrictions already described in the forward section. We will take a closer look at what this means when we look at some examples in the Python.

The locked_args parameter can, as the name suggests, be used to “lock” values in the TunableTuple. What this means and how this can be useful will be explored more in the next article, but for now, the one I will talk about is how it can be used to prevent users from setting certain values. This can be useful for when you define a base TunableTuple that you want to reuse in multiple places in your code, but you don’t want every instance of the Tuple to allow for every option to be set, or you want certain instances of it to have specific values.

The locked_args parameter is a dict whose keys will be tunable options within the tuple that are locked, and the values will be what the tunables are set to. If the key is not already present in the TunableTuple’s list of tunable names, it will be added (but unable to be set).

In XML tuning, this will look something like the following:

Tunable Factories

I will cover TunableFactories more in depth in a later article, but the general gist of them is that, instead of an ImmutableSlots object — what you get by default with TunableTuples, something I will be covering more in depth in my next article — you instead get an instance of a specific class. Thus the name, TunableFactory, as these serve as ways of creating arbitrary Python objects from XML tuning. What class is returned to you depends on how you define the TunableFactory.

TunableFactories are often used in conjunction with TunableVariants.

The <V> Tag

As I discussed in my last article, TunableVariants allow for multiple types of values to be stored under the same name. For those of you familiar with C, this is akin to the union type.

Variants are often seen inside Lists, where they are unnamed (i.e. have no n attribute), and this is where their real power shines. Since <L>s have to all contain the same type, using a Variant type allows for disparate types of data to be stored in the same list.

A great example of this can be seen in LootActions, where the elements in the loot_list are all TunableVariants.

TunableVariants

Let’s start by taking a look at the constructor’s prototype for TunableVaraint:

Taking a look at the parameters, there’s not much there. That’s because, just like with TunableTuples, you use keyword arguments to define the names within the Variant.

  • default: This can be used to set the default value of the Variant. It should be noted that you pass into the parameter the name of the default variant option you want enabled, and that whatever the default for that variant option is, is what the variant will be when it’s not explicitly given a value, or defined with an empty tag like so: <V n="variant_name" t="variant_type" /> .
  • locked_args: As with TunableTuples, this can be useful for when you define a base TunableVariant that you want to reuse in multiple places in your code, but you don’t want every instance of the Variant to allow for every variant option. It is also a dict object, with the keys being the Variant types and the values being what they will be locked as.

In the above example, we have a TunableVariant that can take either a single TunableReference to a trait, or a list of them. In XML, this can look something like this:

OptionalTunables

OptionalTunables are special types of TunableVariants, where a value is either defined or not.

In Python, their __init__ function looks like so:

There are a few things of note here:

  • Like always, tunable describes the type of Tunable value this OptionalTunable can hold. In this case, it is the type of value that can be tuned when the OptionalTunable is enabled.
  • enabled_by_default is a flag that can be set to tell the game that, if you do not specifically set this tunable, whatever the default value of tunable is will be the value held by this OptionalTunable object.
  • disabled_value allows for setting what the OptionalTunable value will be when it is specifically disabled. By default this is None but it can be any Python object.
  • disabled_name and enabled_name are the names of the t attribute used to disable and enable the OptionalTunable, respectively. By default, they are "disabled" and "enabled", but they can be any valid identifier subject to the same restrictions as we have already discussed elsewhere.

An example of an OptionalTunable with its enabled and disabled name specifically set in Python is as follows:

In XML, this would look something like the following:

In the next part, I will cover how these Tunables are loaded from the XML into the game and converted into Python objects.

Footnotes

  1. * To be fair, I have seen Variants that have a type “Don’t Allow”, but if you are defining Tunables using the keyword arguments method (i.e. TunableVariant(variant_name=…), you must use a valid identifier. In either case, the restricted names detailed above cannot be used.
  2. ** To be more concrete, you must use the class and not an instance of that class. For all practical purposes, this doesn’t matter much, since you will primarily be working with classes, but let’s pretend we were allowed to use something other than tunables. You could use (int, str, ) but you could not use (1, "hello", ) as 1 and "hello" are instances of int and str. For some more information on the difference between a class and an instance, you can read here.
  3. *** Here I am referring to the subtle difference between keyword arguments and non-keyword arguments. Every argument can be referred to by its keyword (i.e. by the name it’s given in the parameter list), but positional arguments don’t need to be. For more information on keyword and positional arguments, you can refer to this article here.

Edited by the wonderful Kuttoe.