Custom Tuning Tests (Sims 4 Script Modding)

Frankk
8 min readMay 25, 2021

--

This is an updated version of a tutorial originally written on Sims 4 Studio.

This tutorial will show you how to create your own tests and test sets, and how to use them in tuning. It is assumed that you are at least somewhat familiar with both tuning and script modding for The Sims 4.

NOTE: If you have followed this tutorial prior to July 20, 2021, something has changed. The from event_testing.test_events import cached_test import is now from caches import cached_test . Please make this change, or else your custom test will not work. This change has been made to all of the featured gists.

What’s the point?

Vanilla tuning tests cover almost all the bases you’ll ever need, but if you’re creating a larger mod, you’ll notice there are some tests you need that do not exist. Additionally, tests are limited in the logic they can perform: test_globals , for example, can only pass if all of their tests pass. If you need to use OR logic, you’ll have to use a test set — and nesting OR and AND logic in multiple test sets can be a pain.

For example, in my Language Barriers mod, sims can speak different languages, and their proficiency in that language is determined by the presence of a native speaker trait and/or their skill level in that language. In order to figure out if two sims have a particular language level in common, I would have to use one test set per language, in order to determine if they have the native speaker trait or a certain level in its skill. Then, I would have to use another test set to determine if at least one of these test sets passed.

This equates to 10(N+1) test sets, where N is the number of trait/skill pairs. Since there are 6 languages in my mod, this would come out to 70 test sets. No thanks. Using a custom test, I was able to avoid making 70 files, and use something like this for each level instead:

<V t="shared_language_level">
<U n="shared_language_level">
<V n="language" t="any_language" />
<V n="level" t="specific_level">
<T n="specific_level">10</T>
</V>
</U>
</V>

The only caveat to this method is that it requires you to make your own test set that can parse your tests, and then you can reference that test set wherever you need to. But, 10 test sets that can automatically handle the addition of new trait/skill pairs is better than 70 that would need to be manually updated every time you add a new one.

Overview of making your own test

At a high level, making custom tests consists of three scripting steps and two tuning steps. On the scripting side of things, you must:

  1. Create your test class(es)
  2. Create a test set that can parse your tests
  3. Create a test set instance that uses your test set

If you actually want to use your tests in tuning, then:

  1. Create a snippet for your test set instance
  2. Reference your test set snippet in other files

It is also possible to forgo using a standalone test set file if you have other custom tuning files, such as LootActions, modules, etc., but this is beyond the scope of this current tutorial.

How to create a test class

This is the part that gave me the most trouble in the beginning, but I’ve finally got it down to a science. This is the hard part, so make sure you read this section thoroughly.

There are five main components to every test class, and they are the FACTORY_TUNABLES , __slots__ , and test_events properties, as well as the __call__ and get_expected_args methods.

In order to explain this process visually, let’s imagine that we’re making a test that can check whether a sim’s household owns a certain type of business (a test I made for use with ChippedSim’s Life Challenges mod). I’ll simplify this test a bit so it’s easier to digest, but I will provide the full file, as well as my tests for Language Barriers, at the end of this tutorial to give you more in-depth working examples.

First, let’s define a class for the test, like so:

BusinessOwnershipTest class definition

FACTORY_TUNABLES

The FACTORY_TUNABLES is just a dictionary that defines the properties that your test can have. The properties we probably want for our business ownership test include a sim to test on, a type of business to check ownership of, whether we want to test for ownership or non-ownership, and whether the test should succeed for children (since business ownership is done by household, it would pass for a child if their parents own a business). Let’s call these properties subject , business , invert , and fail_if_child .

BusinessOwnershipTest with FACTORY_TUNABLES added

The above FACTORY_TUNABLES would allow you to do something like the following in tuning, which would check if the actor sim is an adult in a household that owns a retail business.

<E n="subject">Actor</E>
<T n="invert">False</T>
<V n="business" t="specific_business">
<E n="specific_business">RETAIL</E>
</V>
<T n="fail_if_child">True</T>

Another example would be to test that the target sim is any member of a household that does NOT own any businesses.

<E n="subject">TargetSim</E>
<T n="invert">True</T>
<V n="business" t="any"/>
<T n="fail_if_child">False</T>

If you’re new to custom tuning, you might feel overwhelmed by this, and that is perfectly OK, but I promise it’s not that scary. FACTORY_TUNABLES is just a dictionary of names to tunable types, which are just instances of classes. For example, the value of subject is just an instance of the TunableEnumEntry class, which will expect a type of ParticipantTypeSingle . If that value is not specified, Actor will be the default. The description is just for yourself.

If you do not fully understand how this works, I recommend following LeRoiDeTout’s Tuning 101 series, specifically this article. Understanding how to define your FACTORY_TUNABLES is paramount to making your own tests, and knowing what you can do in tuning.

__slots__

Defining __slots__ is a piece of cake. Really, more like a cake pop. It’s just a tuple of the keys in FACTORY_TUNABLES, so the slots for the previous example would just be__slots__ = ('subject', 'invert', 'business', 'fail_if_child') . That’s it.

You could even get away with __slots__ = FACTORY_TUNABLES.keys() for all of your tests, but I advise against this because then PyCharm will not be able to help you when trying to access the slots in your __call__ method.

test_events

All tests in the game have their results cached to save on computation time. However, a test's result must be uncached when events that may change its outcome occur. That’s where test_events comes in: it defines the events that should uncache a test’s result. All of the test event values can be found in the TestEvent enum (from event_testing.test_events), but here’s what it should look like for our business test:

test_events = (TestEvent.HouseholdChanged, TestEvent.BusinessClosed, TestEvent.AgedUp, TestEvent.LoadingScreenLifted,)

When determining your test events, you want to consider all of the events that might affect the outcome of your test. However, there might not always be an event defined for exactly when you need — for example, there is no BusinessOpened test event, which is why I’m using LoadingScreenLifted (you will always see a loading screen after buying a business).

get_expected_args

The get_expected_args method does exactly what the name suggests — it returns the arguments that the test’s __call__ method expects to receive. Typically, the only argument(s) you have to worry about here is/are the subject(s) of your test, since it will automatically get the sim_infos for you. If you just use your class’s self.subject property, it’ll just be their participant type, not their sim_info, which would be pretty useless.

When converting participant types to sim_infos, the get_expected_args will always return them in a tuple. This is because some participant types can involve multiple sims, such as ParticipantType.Listeners . Because of this fact, I usually like to re-name my expected argument to the plural version of the corresponding FACTORY_TUNABLES key, just to make it clear. This is what your get_expected_args should look like for this particular test, which only has a single subject.

def get_expected_args(self):
return {'subjects': self.subject}

Do NOT attempt to use self.subject[0] , because at this point, it’s still a participant type, not a tuple of sim_infos. Just accept that your argument is going to be wrapped in a tuple and move on to the test itself.

__call__

Here we go, the final step — this is where we actually define the behavior of the test using all of the previous components. The actual code in this part will vary wildly depending on your use case, but for the business ownership example, it would look something like this:

BusinessOwnershipTest __call__ method

The @cached_test decorator is necessary for the __call__ method, and its arguments must be kwargs that match the output of get_expected_args (or, you can just use **kwargs and get the values yourself).

I created a helper called _test_result — this is a private function (denoted by the leading underscore), meant to help the test more readable, since we’re using the invert property. This is where the bulk of the logic is.

Of course, this setup will vary tremendously depending on what your test does. Maybe it’s one line, maybe it’s dozens. This part is entirely up to you, your Python skills, and your knowledge of the TS4 codebase.

The completed test class

Whew, that was pretty involved, but now you have a functional test! It should look something like below:

BusinessOwnershipTest final class

If you have more than one test, you can make them smaller by creating a base class and inheriting from that. This is a bit more advanced, so I’ll leave it in the example file for you to reference at the end if you wish.

How to create a test set

If you found creating your test to be about as fun as competing in a triathlon with a broken leg during a thunderstorm, good news: making a test set is about as easy as it gets.

Your custom test set

Um, yeah. That’s it. Just name your test set something better than MyTestSet , and add your tests to the MY_TEST_VARIANTS dictionary and you’re all set. Be sure to come up with an original name for each test, or else you’ll overwrite the ones already the game — that’s right, your test set is not just limited to your tests, but can also use any other tests that already exist.

But now, since our test has a name that a test set can understand, we could write a test like this:

<V t="business_ownership">
<U n="business_ownership">
<E n="subject">Actor</E>
<T n="invert">False</T>
<V n="business" t="specific_business">
<E n="specific_business">RETAIL</E>
</V>
<T n="fail_if_child">True</T>
</U>
</V>

One problem, though. We just defined a test set, but not a test set instance, which means our test is still not connected to tuning. Let’s do that now.

How to create a test set instance

Oh, did I say that making a test set is as easy as it gets? Well, I must’ve forgotten what the next step was.

Yeah. That’s it, and you’re done. You can now create a MyTestSetInstance snippet file and use it wherever you want (but again, please make the name something better and more unique to you or your mod). Let’s go over that next.

How to create a test set snippet

Congrats! All of the heavy lifting is done, and you can now reap the rewards of your hard work. There is no more scripting involved, just tuning.

First, create a snippet (tuning type 7DF2169C), set the c= to whatever your test set instance is called (c="MyTestSetInstance" in this case), and the m= to whatever file name / path is required to locate your class (if you just have a single script file called myname_mymod_tests.py, set m="myname_mymod_tests").

Then, just proceed like normal, create a unique name and tuning ID for the file and you are all set. You can now use any tests you want in this test set, including any existing ones and your own.

Here’s an example of a test that checks that the actor owns a business:

Example test set snippet

How to use a test set snippet

Now that you have your test set created, use it wherever you want with the test_set_reference test, like so:

<V t="test_set_reference">
<T n="test_set_reference">0000<!--my_file_name--></T>
</V>

And that’s it! You can now use your own tests anywhere you please.

--

--